Fil-C on Nix

Filnix wiki

optfil.md

/opt/fil without Nix

nix build .#optfil makes optfil-0.686-filnix-x86_64-linux.tar.xz, a tarball of /opt/fil in the layout of upstream Fil-C's binary distribution. It holds the Fil-C compiler, its runtime, glibc, libc++ and headers, and memory-safe builds of common programs and libraries. It runs on x86-64 Linux whatever the host's C library, without Nix:

sudo tar -C /opt -xJf optfil-0.686-filnix-x86_64-linux.tar.xz
export PATH=/opt/fil/bin:$PATH
filcc -O2 -o hello hello.c && ./hello

The tree must be at /opt/fil. packages/optfil/README.md is the user documentation shipped in the tarball.

Upstream's /opt/fil

Upstream builds /opt/fil with optfil/build_opt.sh and ships it as fil.tar.xz together with an installer, setup.sh, that extracts it to /opt and can set up the Fil-C sshd. Its layout:

Pizlix, in pizlix/, is the other packaging: a Linux From Scratch system in which /usr is built with Fil-C. It is a whole OS image, not a directory to add to a host.

This tree

The Filnix tree has the same layout for the toolchain, so the driver's /opt/fil mode does the work that the Nix compiler wrapper does with flags. The rest is the Nix closure of the toolchain and the packages listed in packages/optfil/default.nix:

Path Contents
bin, lib, include, libexec, share, etc the toolchain and the listed packages, all outputs except documentation, merged
lib/clang/20 Clang's resource directory
s/HASH-NAME every other store path of the closure
etc/ssl/cert.pem Mozilla's root certificates; etc/ssl/certs links to the host's /etc/ssl/certs

Programs from the listed packages are in /opt/fil/bin; the other programs in the closure are still runnable from /opt/fil/s.

How it is made

mkprefix.py copies the closure and then rewrites every reference to a store path, in the bytes of every file:

Because no length changes, this is safe in ELF files: the interpreters and RUNPATHs of the programs point into /opt/fil without patchelf, and so do compiled-in paths such as glibc's character set modules and locales, the loader's default library directory, OpenSSL's directory, terminfo, shebangs and pkg-config files. The merge keeps the first of two files at one path and warns when they differ; with the current package list there are no such conflicts. Files with the same bytes become hard links.

Why a fixed prefix

A program's interpreter is an absolute path, so programs compiled with filcc name the loader's location. A relocatable compiler would make programs that break when the tree moves, or that differ from machine to machine. A fixed /opt/fil makes compiled programs portable between machines that have the tree, like upstream's.

php-dist is relocatable instead: it starts each program with a small launcher that runs it through the bundled loader, and sets variables for the paths that glibc and PHP otherwise take from /nix. That fits one application, not a compiler whose output has to run on its own. filroot is a relocatable variant of this tree that finds itself through a file descriptor instead.

What is not Fil-C

Adding packages

Add the package to packages in packages/optfil/default.nix. Its dependencies come along in /opt/fil/s. Look for warning: lines in the build log of .#optfil.tree: two packages that install the same file, or references that mkprefix.py could not map.

Testing

packages/optfil/test/run.sh unpacks the tarball and runs inside.sh in podman containers without Nix, as root and as an unprivileged user, with the tree mounted read-only at /opt/fil:

nix build .#optfil
packages/optfil/test/run.sh result debian:12 debian:13 ubuntu:22.04 alpine:3

It compiles and runs C and C++ programs, shared libraries, programs that use zlib, OpenSSL, iconv and locales, builds with make and pkg-config, checks that an out-of-bounds write and a use-after-free stop with a Fil-C panic, and runs the bundled programs. OPTFIL_TEST_NET=1 adds HTTPS fetches with curl and git.