Cross-compiling to ARM Linux (e.g. raspberry pis) and compatible linux binary builds for OpenD via Zig. cgi.d now auto-detects all supported protocols

Posted 2026-05-05

You can now download the zig tarball on Linux, and make OpenD use it via opend install xpack-zig optional_path_to_zig_binary to set up, then build your program with opend --target=compat args... to make a more compatible linux binary.

Or, once you have the zig tarball, you can also do opend install xpack-rpi optional_path_to_zig_binary to do automatic setup and druntime download, then opend --target=rpi args... to build for a arm64 Linux system, like a raspberry pi.

How the OpenD/Zig thing works

Linux compat

$ opend install xpack-zig ~/opend/zig-x86_64-linux-0.16.0/zig
Installation complete, build with opend --target=linux-compat <other args>
$ opend --target=linux-compat hellocore
$ ssh ubuntu16
me@ubuntu:~$ cat /etc/issue
Ubuntu 16.04.6 LTS \n \l

me@ubuntu:~$ scp me@arsdnet.net:/home/me/test/hellocore . me@ubuntu:~$ ./hellocore hello world

Contrast with a standard, non-compat build on the same systems:

./hellocore: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by ./hellocore)
./hellocore: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./hellocore)

The --target=compat build avoids this by preferring the older glibc symbols. Newer versions of glibc stay compatible with older versions, but the default when linking is to use the newest symbols available. Since these newest symbols are not available on older systems, you get these "version not found" errors when you copy the binary.

The opend xpack uses zig to do this, because zig can create linker stubs on-demand for system libraries, including specific versions like this. Alternative options are to use different sysroots, or to reimplement zig's solution, but really, it is more practical to just use what they already have.

Worth noting this zig thing does not work for all libraries, but any D libraries compiled in, including druntime+phobos, which are static linked by default in the build, will work fine and you have plenty of options for working with other libraries already. It is really glibc that is the hard one to handle, and zig made that easy.

The xpack-install will generate a small shell script that calls the zig binary as zig cc [other arguments...]. The shell script can now be passed to ldc as a -gcc= argument. Basically, ldc thinks of the zig wrapper as being a linker driver. You don't have to write any zig build files and it doesn't create any zig code, it just uses their binary to invoke a linker. The install also modifies ldc2.conf for this target to pass that argument automatically when using the target. The opend binary, upon seeing the --target argument, automatically switches to ldc and passes the appropriate mtriple to trigger this configuration.

You must download and untar zig from their website. I tested with version 0.16 and previously, with 0.14.1. If you put it in your path, it should just work, and if not, you can specify its full path as the second arg to opend install xpack-zig /full/path/to/zig/binary/here.

rpi cross

me@arsd:~/test$ opend install xpack-rpi ~/opend/zig-x86_64-linux-0.16.0/zig
Downloading...
inflating xpack file opend-latest-xpack-rpi/lib/libdruntime-ldc-debug.a
inflating xpack file opend-latest-xpack-rpi/lib/libdruntime-ldc.a
inflating xpack file opend-latest-xpack-rpi/lib/libphobos2-ldc-debug.a
inflating xpack file opend-latest-xpack-rpi/lib/libphobos2-ldc.a
Installation complete, build with opend --target=rpi <other args>
me@arsd:~/test$ opend --target=rpi hellocore
me@arsd:~/test$ file hellocore
hellocore: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, with debug_info, not stripped
me@arsd:~/test$ scp hellocore pi@192.168.1.175:/home/pi
hellocore                                                                                  100% 2964KB  10.8MB/s   00:00
me@arsd:~/test$ ssh pi@192.168.1.175
Last login: Mon May  4 09:28:45 2026 from 192.168.1.10

pi@raspberrypi:~ $ ./hellocore hello world

Just like with the linux compat target, this uses zig as a linker driver, modifying ldc2.conf and using a small shell script to wrap the zig binary for use as a -gcc= argument. Zig then uses its on-demand library stub generation to do the linking without requiring a full cross compiler sysroot. The only real difference between the linux compat implementation and the rpi one is it needs a separate build of druntime+phobos, which it downloads from the github CI artifact. The CI script, after building the Linux package, also builds a cross-compiled standard library. It does this almost the same as the xpack does on your computer: generate a little shell script to wrap zig cc for the target, then pass it as CC=that environment variable when calling ldc-build-druntime. It then packages the pre-build library on the website for easy downloading later.

If cross-compiling druntime+phobos is as easy as setting CC=zig and calling ldc-build-druntime, why not do it on the user's computer, on demand? Certainly possible, but it isn't *quite* that easy: ldc-build-druntime also requires the full source package to be downloaded, for cmake to be available, and the importc dependencies druntime uses (much to my chagrin, but I haven't gotten around to fixing yet). So while it can be made to work, and can expand to more than just linux-compat and rpi that I set up here, it isn't as likely to Just Work as a simple download from the internet. So I preferred to go with the prebuilt download route.

Again, not all libraries will work with this. You might want to dynamic load them. I do this with arsd.simpledisplay and my test with minigui succeeded; I can cross compile gui applications and run them on the pi! nanovega, however, did not work:

error: unable to find dynamic system library 'freetype' using strategy 'paths_first'. searched paths:

And this is because I used pragma(lib, "freetype") in there, and the cross compiler does not offer stubs for that library. It must be deferred to runtime, or be copied in from the pi and offered as part of a sysroot. So it isn't turnkey for everything, but works well for a lot of things. (Note that minigui also uses freetype, but since it dynamic loads with dlopen, it doesn't need any files at link time to support that.)

You must download and untar zig from their website. I tested with version 0.16 and previously, with 0.14.1. If you put it in your path, it should just work, and if not, you can specify its full path as the second arg to opend install xpack-rpi /full/path/to/zig/binary/here.

One binary, multi protocols: http, cgi, scgi, fastcgi

Speaking of dynamic loading, I finally made cgi.d capable of dynamic loading the C fastcgi library too. This joins its built-in http, cgi, and scgi implementations to enable one binary to support all these web interface, without having an extra build-time dependency. If you don't use it, the interface code is tiny and the C lib is unnecessary, but if you do use it, it is right there ready to go.

Moreover, I checked the cgi and fastcgi specs, and see how it can auto-detect a request to launch with those protocols! (If you use the library's provided cgiMainImpl, whether by calling it directly yourself or using one of the mixin GenricMain etc options.)) This means it can print a message if nothing is suggested (whereas before it would assume cgi and print out some spam to stdout, or arbitrarily pick a port if compiled to serve http), or auto-launch cgi and fastcgi if requested by the environment.

The result:

// one program, not special code
import arsd.cgi;

void handler(Cgi cgi) {
        cgi.write(cgi.getCurrentCompleteUri ~ "Hello, world!", true);
}

mixin GenericMain!handler;
# one build, no special arguments
$ opend hello
$ ./hello
To start a local-only http server, use thisprogram --listen http://localhost:PORT_NUMBER
To start a externally-accessible http server, use thisprogram --listen http://:PORT_NUMBER
To start a scgi server, use thisprogram --listen scgi://localhost:PORT_NUMBER
To test a request on the command line, use thisprogram REQUEST /path arg=value
Or copy this program to your web server's cgi-bin folder to run it that way.
If you need FastCGI, you can listen with fcgi:// but note it requires the C fcgi library available in your dynamic load path.

Learn more at https://opendlang.org/library/arsd.cgi.html#Command-line-interface

$ cp hello /var/www/cgi-bin # works as a cgi binary $ curl http://localhost/cgi-bin/hello http://localhost/cgi-bin/helloHello, world! $ cp hello /var/www/htdocs/test.fcgi # works as a fastcgi binary http://localhost/test.fcgiHello, world! $ ./hello --listen http://localhost:3329 # or it has the embedded_httpd too $ ./hello --listen scgi://localhost:3330 # or scgi you can proxy $ ./hello --listen fcgi://localhost:3331 # or explicit fcgi server $ ./hello GET / # or you can still do command line tests yourself Status: 200 OK Cache-Control: private, no-cache="set-cookie" Expires: 0 Pragma: no-cache Content-Type: text/html; charset=utf-8 Content-Length: 21

http:///Hello, world!

Only one build there, no more need to recompile with -version=embedded_httpd or similar to have these options, it is all available at once. You do still need the fastcgi C library installed for the fcgi options to work at runtime, but no more need for it at compile time.

Of course, the old build versions do still work and I intend to keep them working, but this is the new default. Since the code to support the extra gateway protocols is miniscule, including them all is cheap: 4.9M for the combined binary vs 4.8M for a single protocol, so I'm happy to pay that for the simplified build and deployment.

Once I push this library update, you can combine this with the new opend build options to cross compile a web server:

$ time opend --target=rpi hello

real 0m4.072s user 0m3.852s sys 0m0.225s $ scp hello pi@192.168.1.175:/home/pi/ hello 100% 4716KB 10.9MB/s 00:00 ## in other window $ ssh pi@192.168.1.175 -Y pi@raspberrypi:~ $ ./hello --listen http://0.0.0.0:3332 ### back to the other computer

$ curl http://192.168.1.175:3332/hello http://192.168.1.175:3332/helloHello, world!

Notice the build was slow - opend hello on that same file, using standard dmd, finishes in 1.1s, and here it was about 4.1s. Most of that speed difference is just ldc vs dmd, but the zig thing is a bit slow the first time too as it generates the library stubs - but not nearly as slow as doing the compile on the raspberry pi itself.

But anyway, it just worked and i could copy the binary straight over and run it and it all works. So pretty convenient. I'm quite happy with all this.

It currently only lets you specify one protocol in --listen at a time. In the future, I'll let you serve http on one port and scgi on another, etc.