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.
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.
I started with quite a basic Golang recipe example, quite good to serve as a reference.
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…
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_LDFLAGS
variable 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