Making a Simple Service App for a Yocto-Based Linux Distro

Written by xt | Published 2021/08/05
Tech Story Tags: golang | yocto | software-development | programming | coding | app | linux | go

TLDR Yocto project, based on OpenEmbedded, has served us as solid base for creating exciting projects for embedded systems. Recently I decided to make the first steps in software development and make a simple service app. I started with quite a basic Golang recipe example, quite good to serve as a reference. So the recipe finally looked like this: "Yet another example of yet another Golang example" It was supposed to be an easy-peasy task turned into several hours of research.via the TL;DR App

For several years Yocto project, based on OpenEmbedded, has served us as solid base for creating exciting projects for embedded systems.

So recently I decided to make the first steps in Yocto-oriented software development and make a rather simple service app for Yocto-based Linux distro, which was supposed to be an easy-peasy task turned into several hours of research. But we are finally there…

So let me share my findings with you.

Golang Project

package main

import (
	"database/sql"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"

	"github.com/bmizerany/pat"
	_ "github.com/mattn/go-sqlite3"
)

var GitCommit string

type ConfigurationEntry struct {
	ID   int64  `json:"id"`
	Name string `json:"name"`
}

type Configuration []ConfigurationEntry

var mainDB *sql.DB

func main() {
	if GitCommit != "" {
		fmt.Printf("Symbiote\ncommit_%s\n", GitCommit)
	}

	var dbFile string
	flag.StringVar(&dbFile, "db", "/tmp/symbiote.db", "SQLite database filename")

	os.MkdirAll(filepath.Dir(dbFile), os.ModePerm)

	db, errOpenDB := sql.Open("sqlite3", dbFile)
	checkErr(errOpenDB)
	mainDB = db
	createTable(db) // create table if doesn't exist

	r := pat.New()
	r.Del("/config/:id", http.HandlerFunc(deleteByID))
	r.Get("/config/:id", http.HandlerFunc(getByID))
	r.Put("/config/:id", http.HandlerFunc(updateByID))
	r.Get("/config", http.HandlerFunc(getAll))
	r.Post("/config", http.HandlerFunc(insert))

	http.Handle("/", r)

	log.Print(" Running on 3000")
	err := http.ListenAndServe("127.0.0.1:3000", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

func getAll(w http.ResponseWriter, r *http.Request) {
  // some logic
}

func getByID(w http.ResponseWriter, r *http.Request) {
// some logic
}

func insert(w http.ResponseWriter, r *http.Request) {
  // some logic
}

func updateByID(w http.ResponseWriter, r *http.Request) {
  // some logic
}

func deleteByID(w http.ResponseWriter, r *http.Request) {
  // some logic
}

func createTable(db *sql.DB) {
// some logic
}

func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}

So, as you see, nothing special.

Bitbake Recipe

I started with quite a basic Golang recipe example, quite good to serve as a reference.

https://github.com/xorde-repos/yoctoproject-poky/blob/master/meta/recipes-extended/go-examples/go-helloworld_0.1.bb

So the recipe finally looked like this:

DESCRIPTION = "Yet another Yocto Golang example"

LICENSE = "CLOSED"

SRC_URI = "git://${GO_IMPORT}"
SRCREV = "${AUTOREV}"
UPSTREAM_CHECK_COMMITS = "1"

## TT_REPO is set in local.conf
GO_APP = "symbiote"
GO_IMPORT = "${TT_REPO}/${GO_APP}"
GO_INSTALL = "${GO_IMPORT}"
GO_WORKDIR = "${GO_INSTALL}"
export GO111MODULE="off"

inherit go

do_install_append() {
    mv ${D}${bindir}/${GO_APP} ${D}${bindir}/${BPN}
}

do_configure_apend() {
    #this one fixes permissions problem with temporary files
    chmod -R +w ${WORKDIR}
}

So, again, nothing special…

Building

Depends on how you run Bitbake, I use a pretty complex set of scripts that bake my image simultaneously for multiple platforms, but in the general case it could be just the usual something-setup-env and then bitbake whatever-you-want-to-build.

So the first run returned pretty interesting error:

| build/src/github.com/mattn/go-sqlite3/backup.go:14:10: fatal error: stdlib.h: No such file or directory

| 14 | #include <stdlib.h>

Turns out go.bbclass tries to build go-sqlite3 with native-sysroot, and it is no surprise that native-sysroot is not fully populated.

So lets provide proper populated sysroot to compiler:

CGO_CFLAGS += "-I${WORKDIR}/recipe-sysroot/usr/include"

I get a feeling this build process will become more interesting, since it started so nice…

So I started building again and now I get a linker error:

ld: cannot find crtn.o: No such file or directory

and then:

| sqlite3-binding.c:30964: error: undefined reference to 'pthread_join'

Well, it is another sysroot related problem. Let’s fix it with this:

CGO_LDFLAGS += "--sysroot=${WORKDIR}/recipe-sysroot -pthread"

Ok, let me build again… Now what? Yes, another error:

recipe-sysroot/usr/include/gnu/stubs-32.h:7:11: fatal error: gnu/stubs-soft.h: No such file or directory

The build process decided to use software-based floating point, but I know my ARM system has hardware-based floating point. Hmm, it is weird… And it seems go.bbclass that defines build process does nothing about it to properly configure the build.

So merely adding this line to our recipe .bb file solves the problem:

CGO_CFLAGS_arm += "-mfloat-abi=hard -mfpu=neon "

However it was a sort of workaround, I wanted something more stable, automagic so to say. And I am going to write a small inline Python script, luckily “CC” environment variable has all necessary parameters for proper build configuration:

CGO_FLAGS += "${@' '.join( filter( lambda x: x.startswith(( '-mfpu=', '-mfloat-abi=', '-mcpu=' )), d.getVar('CC').split(' ') ) )}"

To provide you a better view on this Python one-liner let me format it for you:

# 1. Get CC environment variable and split it by space:
d.getVar('CC').split(' ')

# 2. Filter resulting array with lambda function that returns "true" if it starts with any of the elements of the tupple:
filter(lambda x: x.startswith(( '-mfpu=', '-mfloat-abi=', '-mcpu=' )), d.getVar('CC').split(' ')
)

# 3. join filtered results with space.

So to say it compiles now OK. But I want to go further by providing Git commit hash to my app.

This way my app knows its commit hash and can display it when needed:

GO_RPATH += "-X main.GitCommit=$(cd ${GOPATH}/src/${GO_IMPORT} && [ `git rev-parse HEAD 2>/dev/null` ] && git rev-parse HEAD || echo undefined)"

Now, this thing is a hackish way of doing that. You see, I actually wanted to run golang compiler with -ldflags=-X main.GitCommit=$(cd ${GOPATH}/src/${GO_IMPORT} && [ `git rev-parse HEAD 2>/dev/null` ] && git rev-parse HEAD || echo undefined)", but when I use

CGO_LDFLAGSvariable of the go.bbclass my code gets into -ldextflags which is really counter-intuitive.

After looking into go.bbclass I figured that I can get into -ldflags by using GO_RPATH


Written by xt | Barnard's star, a rather dim red dwarf about six light-years away from Earth. Extraterrestrial?
Published by HackerNoon on 2021/08/05