The Gerbil Build Tool
Building complex libraries and executables by invoking gxc
quickly gets tedious. When you reach that point of complexity and you need a build tool, you can use the :std/make
library module which provides a modest build tool that can handle reasonably complex project building.
A Trivial Project
For illustration purposes, we'll make a hello world library module and an executable that uses it.
$ cat gerbil.pkg
(package: example)
$ cat util.ss
(export #t)
(def (hello who)
(displayln "hello, " who))
$ cat hello.ss
(import :example/util)
(export main)
(def (main . args)
(for-each hello args))
The Standard Build Script Template
The recommended way to write a build script is to use the template provided by the standard library.
You can do this by importing :std/build-script
and using the defbuild-script
macro.
The macro defines a main function suitable for building packages either directly or through gpxkg. The syntax is
(defbuild-script build-spec . settings)
Using this, the build script for our project is the following:
$ cat build.ss
#!/usr/bin/env gxi
(import :std/build-script)
(defbuild-script
'("util"
(exe: "hello")))
And we can build by invoking the script:
$ chmod +x build.ss
$ ./build.ss
...
Intermediate Build Scripts
Here is a build script that uses an environment variable to determine whether to build an optimized fully static binary or a normally linked binary:
$ cat build.ss
#!/usr/bin/env gxi
(import :std/build-script)
(defbuild-script
`("util"
,(if (getenv "GERBIL_BUILD_RELEASE" #f)
'(optimized-static-exe: "hello")
'(exe: "hello"))))
If you are in your development environment and building executables for your host, then you can just invoke it as
$ ./build.ss
On the other hand, if you are building inside a docker container that supports fully static binaries (say alpine or void linux), you can just use the following to build an optimized fully static binary:
GERBIL_BUILD_RELEASE=t ./build.ss
Note
You may need to pass some linker flags in your build spec when using
(optimized-static-exe: <module> "-ld-options" "...")
. This may be
necessary because the compiler cannot tell in advance what the tree
shaker will eliminate and thus it is not prudent to automatically
link all stdlib foreign dependencies.
Using the Gerbil build tool
Normally, you should not run build.ss
directly but you use the
gerbil build
tool insted. This will run it for you with the proper build
environment:
$ gerbil build
...
Also note, you don't have to use optimized-exe:
or static-exe:
explicitly in your build scripts, you can use exe:
which is context
dependent. If you invoke the build tool with the --optimized
and/or
--release
flags it will automatically translate exe:
build specs
to optimized-exe:
, static-exe:
or optimized-static-exe:
as
applicable. You can also define GERBIL_BUILD_OPTIMIZED
and
GERBIL_BUILD_RELEASE
environment variables, which has the same effect.
So to build executables with full program optimization:
$ gerbil build --optimized
And to build optimized release executables, you can do this inside your docker build container:
$ gerbil build --release --optimized
Dependency Management and Build Isolation
So far we have illustrated projects without any package dependencies;
things get more interesting when we factor those in. The build tool
provides functionality to manage your project dependencies and build
your project cleanly in an isolated environment irrespective of the
current global state in ~/.gerbil
.
All this is best explained with an example, but first let's explicitly state the problem so that you can understand what follows:
- The Gerbil build environment is dictated by the
GERBIL_PATH
environment variable. - If you don't set this variable, it will default to
~/.gerbil
. - This is totally fine for casual or interactive use, where you want to install things globally to access libraries in the interpreter and have binaries in your path.
- However, it is entirely inappropriate when building and assembling
your project, as a dirty
~/.gerbil
can break the build and generally have unintended side effects because of state. - Prior to Gerbil v0.18, the recommended best practice was to
manually set
GERBIL_PATH
on a per project basis to isolate your builds. - This works, but it is poor developer UX; so in Gerbil v0.18 we have
systematized it and unless you explicitly set
GERBIL_PATH
(you can still do that if you want full control of the build environment), when building a project locally the build tool will automatically create a build environment for your project and setGERBIL_PATH
for relevant commands.
A Simple Project with an External Dependency
The Project Structure Source Code
So let's start over again: this time we'll build a primitive web
scrapper: it is a command line tool that takes a URL, makes an http
request and parses the html output using parse-html
from the
gerbil-libxml
package.
First, let's create the project:
$ mkdir scrape-it
$ cd scrape-it
$ gerbil new -n scraper
$ ls -lR
.:
total 16
-rwxr-xr-x 1 vyzo vyzo 144 Sep 24 11:33 build.ss
-rw-rw-r-- 1 vyzo vyzo 16 Sep 24 11:33 gerbil.pkg
-rw-rw-r-- 1 vyzo vyzo 478 Sep 24 11:33 Makefile
drwxrwxr-x 2 vyzo vyzo 4096 Sep 24 11:33 scraper
./scraper:
total 8
-rw-rw-r-- 1 vyzo vyzo 109 Sep 24 11:33 lib.ss
-rw-rw-r-- 1 vyzo vyzo 791 Sep 24 11:33 main.ss
Now let's add our dependency:
$ gerbil deps -a -i github.com/mighty-gerbils/gerbil-libxml
... cloning github.com/mighty-gerbils/gerbil-libxml
... pulling
... build github.com/mighty-gerbils/gerbil-libxml
... compile foreign xml/_libxml
... copy ssi xml/_libxml
... compile loader xml/_libxml
... compile xml/libxml
... tagging packages
Next, we add the code for the scrapper:
cat scraper/lib.ss
;;; -*- Gerbil -*-
(import :std/error
:std/sugar
:std/net/request
:clan/xml/libxml)
(export #t)
(def (scrape url)
(let (req (http-get url redirect: #t))
(unless (= (request-status req) 200)
(error "HTTP request did not succeed" status: (request-status-text req)))
(let (content-type (assget "Content-Type"(request-headers req)))
(unless (string-prefix? "text/html" content-type)
(error "HTTP response did not return html" content-type: content-type)))
(parse-html (request-text req))))
$ cat scraper/main.ss
;;; -*- Gerbil -*-
(import :std/error
:std/sugar
:std/cli/getopt
:gerbil/gambit
./lib)
(export main)
;; build manifest; generated during the build
;; defines version-manifest which you can use for exact versioning
(include "../manifest.ss")
(def (main . args)
(call-with-getopt scraper-main args
program: "scraper"
help: "A simple web scraper"
(argument 'url help: "URL to scrape")))
(def* scraper-main
((opt)
(scraper-main/options opt))
((cmd opt)
(scraper-main/command cmd opt)))
;;; Implement this if your CLI doesn't have commands
(def (scraper-main/options opt)
(let (sxml (scrape (hash-ref opt 'url)))
(pretty-print sxml)))
;;; Implement this if your CLI has commands
(def (scraper-main/command cmd opt)
(error "Implement me!"))
$ cat build.ss
#!/usr/bin/env gxi
;;; -*- Gerbil -*-
(import :std/build-script :std/make)
(defbuild-script
`("scraper/lib"
(exe: "scraper/main" bin: "scraper"
"-cc-options" ,(shell-config "xml2-config" "--cflags")
"-ld-options" ,(shell-config "xml2-config" "--libs"))))
And let's build it and run it:
$ gerbil build
... build in current directory
... compile scraper/main
... compile exe scraper/main -> /home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper
/tmp/gxc.1695545021.0991077/clan__xml___libxml.scm:
/tmp/gxc.1695545021.0991077/clan__xml__libxml.scm:
/tmp/gxc.1695545021.0991077/vyzo__scraper__lib.scm:
/tmp/gxc.1695545021.0991077/vyzo__scraper__main.scm:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper.scmx:
/tmp/gxc.1695545021.0991077/clan__xml___libxml.c:
/tmp/gxc.1695545021.0991077/clan__xml__libxml.c:
/tmp/gxc.1695545021.0991077/vyzo__scraper__lib.c:
/tmp/gxc.1695545021.0991077/vyzo__scraper__main.c:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper.c:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper_.c:
$ gerbil env scraper http://hackzen.org
(*TOP* (html (head (title "(hackzen.org)")
(link (@ (rel "stylesheet") (type "text/css") (href "style.css"))))
(body "\n "
(h1 (@ (id "header")) "(hackzen.org)")
"\n "
"\n "
(div (a (@ (href "http://xkcd.com/297/")) (img (@ (src "parens.png")))))
"\n "
(br)
(div (a (@ (href "robots.html")) "(robots)"))
"\n "
(div (a (@ (href "gerbil/index.html")) "(gerbils)"))
"\n "
(div (a (@ (href "humans.html")) "(humans)"))
"\n "
(div (a (@ (href "nic9/index.html")) "[N1C#09]"))
"\n "
(br)
(script (@ (src "harhar.js"))))))
So everything worked smoothly with the build, and the program works; let's look at what happend under the hood.
The Build Environment
The first thing that you should notice is that the build artifacts are
placed in a local .gerbil
directory and not the global user
~/.gerbil
.
Now let's look at what's in there:
$ ls -lR .gerbil/
.gerbil/:
total 12
drwxr-xr-x 2 vyzo vyzo 4096 Sep 24 11:43 bin
drwxr-xr-x 5 vyzo vyzo 4096 Sep 24 11:42 lib
drwxr-xr-x 3 vyzo vyzo 4096 Sep 24 11:34 pkg
.gerbil/bin:
total 220
-rwxrwxr-x 1 vyzo vyzo 222312 Sep 24 11:43 scraper
.gerbil/lib:
total 12
drwxr-xr-x 3 vyzo vyzo 4096 Sep 24 11:34 clan
drwxr-xr-x 2 vyzo vyzo 4096 Sep 24 11:43 static
drwxr-xr-x 3 vyzo vyzo 4096 Sep 24 11:42 vyzo
.gerbil/lib/clan:
total 4
drwxr-xr-x 2 vyzo vyzo 4096 Sep 24 11:34 xml
.gerbil/lib/clan/xml:
total 212
-rwxrwxr-x 1 vyzo vyzo 47448 Sep 24 11:34 libxml__0.o1
-rwxrwxr-x 1 vyzo vyzo 18656 Sep 24 11:34 libxml__1.o1
-rwxrwxr-x 1 vyzo vyzo 92472 Sep 24 11:34 _libxml.o1
-rwxrwxr-x 1 vyzo vyzo 17800 Sep 24 11:34 _libxml__rt.o1
-rwxrwxr-x 1 vyzo vyzo 18160 Sep 24 11:34 libxml__rt.o1
-rwxrwxr-x 1 vyzo vyzo 1543 Sep 24 11:34 _libxml.ssi
-rw-r--r-- 1 vyzo vyzo 4072 Sep 24 11:34 libxml.ssi
-rw-r--r-- 1 vyzo vyzo 1832 Sep 24 11:34 libxml.ssxi.ss
.gerbil/lib/static:
total 48
-rwxrwxr-x 1 vyzo vyzo 12419 Sep 24 11:34 clan__xml___libxml.scm
-rwxrwxr-x 1 vyzo vyzo 21371 Sep 24 11:34 clan__xml__libxml.scm
-rwxrwxr-x 1 vyzo vyzo 2109 Sep 24 11:42 vyzo__scraper__lib.scm
-rwxrwxr-x 1 vyzo vyzo 2404 Sep 24 11:43 vyzo__scraper__main.scm
.gerbil/lib/vyzo:
total 4
drwxr-xr-x 2 vyzo vyzo 4096 Sep 24 11:42 scraper
.gerbil/lib/vyzo/scraper:
total 64
-rwxrwxr-x 1 vyzo vyzo 19008 Sep 24 11:42 lib__0.o1
-rwxrwxr-x 1 vyzo vyzo 18488 Sep 24 11:42 lib__rt.o1
-rw-r--r-- 1 vyzo vyzo 293 Sep 24 11:42 lib.ssi
-rw-r--r-- 1 vyzo vyzo 108 Sep 24 11:42 lib.ssxi.ss
-rw-r--r-- 1 vyzo vyzo 2404 Sep 24 11:43 main__0.scm
-rw-r--r-- 1 vyzo vyzo 297 Sep 24 11:43 main__rt.scm
-rw-r--r-- 1 vyzo vyzo 738 Sep 24 11:43 main.ssi
-rw-r--r-- 1 vyzo vyzo 424 Sep 24 11:43 main.ssxi.ss
.gerbil/pkg:
total 8
drwxr-xr-x 3 vyzo vyzo 4096 Sep 24 11:34 github.com
-rw-rw-r-- 1 vyzo vyzo 3599 Sep 24 11:34 TAGS
.gerbil/pkg/github.com:
total 4
drwxr-xr-x 3 vyzo vyzo 4096 Sep 24 11:34 mighty-gerbils
.gerbil/pkg/github.com/mighty-gerbils:
total 8
drwxrwxr-x 4 vyzo vyzo 4096 Sep 24 11:34 gerbil-libxml
-rw-rw-r-- 1 vyzo vyzo 131 Sep 24 11:34 gerbil-libxml.manifest
.gerbil/pkg/github.com/mighty-gerbils/gerbil-libxml:
total 64
-rw-rw-r-- 1 vyzo vyzo 362 Sep 24 11:34 build-deps
-rwxrwxr-x 1 vyzo vyzo 306 Sep 24 11:34 build.ss
-rw-rw-r-- 1 vyzo vyzo 16 Sep 24 11:34 gerbil.pkg
-rw-rw-r-- 1 vyzo vyzo 11358 Sep 24 11:34 LICENSE-APACHE-2.0.txt
-rw-rw-r-- 1 vyzo vyzo 26430 Sep 24 11:34 LICENSE-LGPL.txt
-rw-rw-r-- 1 vyzo vyzo 172 Sep 24 11:34 manifest.ss
-rw-rw-r-- 1 vyzo vyzo 3535 Sep 24 11:34 README.md
drwxrwxr-x 2 vyzo vyzo 4096 Sep 24 11:34 xml
.gerbil/pkg/github.com/mighty-gerbils/gerbil-libxml/xml:
total 28
-rw-rw-r-- 1 vyzo vyzo 12419 Sep 24 11:34 _libxml.scm
-rw-rw-r-- 1 vyzo vyzo 6351 Sep 24 11:34 libxml.ss
-rw-rw-r-- 1 vyzo vyzo 1543 Sep 24 11:34 _libxml.ssi
.gerbil/bin
contains the binary output..gerbil/lib
contains the library build artifacts..gerbil/pkg
contains the packages involved
The most important one here is the .gerbil/pkg
directory, this is
where dependencies live.
Version Manifests
You will notice a salient new file that appeared in our directory:
$ ll manifest.ss
-rw-rw-r-- 1 vyzo vyzo 205 Sep 24 11:43 manifest.ss
$ cat manifest.ss
(def version-manifest
'(("scrape-it" . "unknown")
("Gerbil" . "0.17.0-309-g5ebf1095")
("Gambit" . "v4.9.5-40-g24201248")
("github.com/mighty-gerbils/gerbil-libxml" . "b08e5d8")))
This file provides exact versioning for all parts of the project
involved, getting information from git
. For gerbil-libxml
you'll
notice that the version is a commit hash, as at the time of writing
there are not any version tags in the package (see next section).
Note that the version of our project (scrape-it
) is unknow; that's
because we have not initialized a git repository for our project.
Once we do that, it stops being unknown and it points to the current commit:
$ git init
Initialized empty Git repository in /home/vyzo/src/vyzo/scratch/test/scrape-it/.git/
$ git add .
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: .gitignore
new file: Makefile
new file: build.ss
new file: gerbil.pkg
new file: scraper/lib.ss
new file: scraper/main.ss
$ git commit -m "initial commit"
[master (root-commit) 0ba7240] initial commit
6 files changed, 83 insertions(+)
create mode 100644 .gitignore
create mode 100644 Makefile
create mode 100755 build.ss
create mode 100644 gerbil.pkg
create mode 100644 scraper/lib.ss
create mode 100644 scraper/main.ss
$ gerbil clean
... clean current package
... remove /home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/lib/vyzo/scraper/lib.ssi
... remove /home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/lib/static/vyzo__scraper__lib.scm
... remove /home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper
... remove /home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/lib/static/vyzo__scraper__main.scm
$ gerbil build
... build in current directory
... compile scraper/lib
... compile scraper/main
... compile exe scraper/main -> /home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper
/tmp/gxc.1695546027.0358357/clan__xml___libxml.scm:
/tmp/gxc.1695546027.0358357/clan__xml__libxml.scm:
/tmp/gxc.1695546027.0358357/vyzo__scraper__lib.scm:
/tmp/gxc.1695546027.0358357/vyzo__scraper__main.scm:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper.scmx:
/tmp/gxc.1695546027.0358357/clan__xml___libxml.c:
/tmp/gxc.1695546027.0358357/clan__xml__libxml.c:
/tmp/gxc.1695546027.0358357/vyzo__scraper__lib.c:
/tmp/gxc.1695546027.0358357/vyzo__scraper__main.c:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper.c:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper_.c:
$ cat manifest.ss
(def version-manifest
'(("scrape-it" . "0ba7240")
("Gerbil" . "0.17.0-309-g5ebf1095")
("Gambit" . "v4.9.5-40-g24201248")
("github.com/mighty-gerbils/gerbil-libxml" . "b08e5d8")))
We can integrate the version manifest into our program's cli so that when a user reports a bug or there is some failure in your production environment, you can query the binary to find the exact version and know exactly what code was used to compile it.
Here, we add a -v/--version
flag to print the version and exit:
$ cat scraper/main.ss
;;; -*- Gerbil -*-
(import :std/error
:std/sugar
:std/cli/getopt
:gerbil/gambit
./lib)
(export main)
;; build manifest; generated during the build
;; defines version-manifest which you can use for exact versioning
(include "../manifest.ss")
(def (main . args)
(call-with-getopt scraper-main args
program: "scraper"
help: "A simple web scraper"
(flag 'version "-v" "--version" help: "display program version and exit")
(optional-argument 'url help: "URL to scrape")))
(def* scraper-main
((opt)
(scraper-main/options opt))
((cmd opt)
(scraper-main/command cmd opt)))
;;; Implement this if your CLI doesn't have commands
(def (scraper-main/options opt)
(when (hash-get opt 'version)
(pretty-print version-manifest)
(exit 0))
(let (sxml (scrape (hash-ref opt 'url)))
(pretty-print sxml)))
;;; Implement this if your CLI has commands
(def (scraper-main/command cmd opt)
(error "Implement me!"))
$ gerbil build
... build in current directory
... compile scraper/main
... compile exe scraper/main -> /home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper
/tmp/gxc.1695546226.3194306/clan__xml___libxml.scm:
/tmp/gxc.1695546226.3194306/clan__xml__libxml.scm:
/tmp/gxc.1695546226.3194306/vyzo__scraper__lib.scm:
/tmp/gxc.1695546226.3194306/vyzo__scraper__main.scm:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper.scmx:
/tmp/gxc.1695546226.3194306/clan__xml___libxml.c:
/tmp/gxc.1695546226.3194306/clan__xml__libxml.c:
/tmp/gxc.1695546226.3194306/vyzo__scraper__lib.c:
/tmp/gxc.1695546226.3194306/vyzo__scraper__main.c:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper.c:
/home/vyzo/src/vyzo/scratch/test/scrape-it/.gerbil/bin/scraper_.c:
And voila:
$ gerbil env scraper -v
(("scrape-it" . "0ba7240")
("Gerbil" . "0.17.0-309-g5ebf1095")
("Gambit" . "v4.9.5-40-g24201248")
("github.com/mighty-gerbils/gerbil-libxml" . "b08e5d8"))
Semantic Versioning
As you've probably noticed, version information comes from git
. The natural follow up question is "can we version packages".
The answer is "Yes, of course!". Gerbil uses tags for version and
implements semantic versioning to select the correct version of your
packages when there differing versions specified. You can request a
specific version of a package by simple appending @<version-tag>
to
the package name when specifying a dependency. This will ensure that
the correct version of the code is checked out according to the
dependencies in the transitive package list.
The rules for version selection when there are different version of the same package involved in the transitive dependency list are as follows:
- Always select the latest semantic version, with tags of the form
vX[.Y[.Z]]
parsed as major, minor, and patch version - The
master
andmain
branches are always considered versioned as higher than any semantic version tag. - If the package version specifies two different branches or commit hashes, then it is considered a hard conflict and the user has to intervene to resolve the issue.
Note that Gerbil's semantic versioning doesn't follow the strict
"different major versions are incompatible" rule. We considered this
and our long experience with developing production software has led us
to the conclusion that it simply doesn't work in practice -- see Go's
ugly required version appending once you are over v1 or the mess with
Rust. What we advocate instead is for you to make a v2
subpackage
within your package that implements forward functionality without
breaking the API of v1
and so on for higher versions.
Testing your package
So at this point you are naturally wondering how to run tests for your package, given the build isolation properties of the tooling.
This is actually very simple: the gerbil pkg env
command provides
you with the ability to run command with the local build GERBIL_PATH
set for you.
So in order to run your tests, all you have to do is:
$ gerbil pkg env gxtest ./...
Where to go from here
See the Gerbil Universal Binary and Tools page for more information about the Gerbil tooling.