Build the runtime for FreeBSD - #149
Conversation
FreeBSD has neither of the two Linux mechanisms the runtime relies on to find and mount itself: - there is no /proc/self/exe to readlink(), so the path of the running AppImage comes from the KERN_PROC_PATHNAME sysctl instead, resolved once at startup; - FUSE file systems are mounted by mount_fusefs(8), which libfuse invokes itself, so there is no setuid fusermount helper to search $PATH for. Both are guarded behind capability macros, leaving the Linux code paths unchanged. The three readlink() call sites that turned /proc/self/exe into a real path are folded into one helper so the difference lives in a single place; that helper also reserves room for the NUL terminator, which the open-coded readlink() calls did not, so a path filling the buffer wrote one byte past its end. FreeBSD additionally does not guarantee the mount point is populated by the time libfuse reports the file system as mounted, so the parent polls for the entrypoint before exec'ing it.
Mirrors the existing per-environment entry points under scripts/docker and
scripts/chroot, with scripts/bsd/build.sh installing the packages and then
calling install-dependencies.sh and build-runtime.sh.
libfuse and squashfuse are built from source rather than taken from
packages: FreeBSD ships both (filesystems/fusefs-libs3 and
filesystems/fusefs-squashfuse) but as shared libraries only, and the
runtime is statically linked. libfuse is pinned to 3.18.2 rather than the
3.15.0 the Linux build uses, because the BSD fixes are in 3.17 and later;
the Linux pin is left alone. The fusermount patch in patches/libfuse does
not apply and is not needed, as it patches the Linux mount backend while
FreeBSD uses mount_bsd.c.
The link needs GNU ld from devel/binutils. lld cannot apply the INSERT
AFTER .interp in data_sections.ld, since a non-PIE static binary has no
.interp section:
ld: error: unable to insert .appimage after .interp
binutils is a build dependency for objcopy anyway, so this costs nothing
extra. No ELF OS/ABI correction is performed: it comes out as
ELFOSABI_FREEBSD on x86_64 and ELFOSABI_NONE on aarch64, and both run,
because what the kernel goes by is the FreeBSD ABI note, which ld.bfd
emits in either case.
The build scripts are separate from scripts/build-runtime.sh rather than
conditional inside it because that script assumes a GNU userland
throughout: nproc(1), bash at /bin/bash to detect the architecture from,
GNU make, and objcopy in the base system.
test-runtime.sh assembles a minimal AppImage around the freshly built
runtime and runs it, so that a build which links but cannot mount or
cannot find its own path is caught rather than shipped.
Runs the native FreeBSD build in a VM provided by vmactions/freebsd-vm, since GitHub does not offer FreeBSD runners, for x86_64 and aarch64. Each job builds the runtime and then runs it, so a runtime that cannot mount or cannot resolve its own path fails the build. Kept as its own workflow so the existing Linux pipeline is untouched. The artifacts are uploaded but not wired into the release job, as the FreeBSD runtime is a separate artifact rather than a replacement for the Linux one of the same architecture. aarch64 is emulated and takes roughly four times as long as the accelerated x86_64 job, though it stays well under ten minutes because only the libfuse library is built, not its utils, examples and tests. Closes AppImage#126
There was a problem hiding this comment.
Pull request overview
Adds native FreeBSD support to the type2 runtime build and CI by introducing OS-specific capability switches in the runtime, plus a new FreeBSD build/test toolchain and workflow to produce and smoke-test FreeBSD runtime artifacts.
Changes:
- Add FreeBSD-specific runtime behavior for resolving the running AppImage path (sysctl-based) and handling FUSE mounting differences.
- Introduce
scripts/bsd/*to build static dependencies and the runtime natively on FreeBSD, plus a smoke test that runs the produced AppImage. - Add a dedicated FreeBSD GitHub Actions workflow and document the FreeBSD runtime alongside the existing Linux build methods.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/runtime/runtime.c | Adds platform capability macros, a path-resolution helper, FreeBSD sysctl path lookup, and a FreeBSD-only mount-visibility wait. |
| src/runtime/Makefile | Adds FreeBSD-specific compiler/linker/includes/libs configuration and factors common flags. |
| scripts/bsd/test-runtime.sh | Adds a FreeBSD smoke test that builds a minimal AppImage and executes it to validate mount + self-path resolution. |
| scripts/bsd/install-dependencies.sh | Builds static libfuse3 and squashfuse from source for FreeBSD and installs required headers/libs into /usr/local. |
| scripts/bsd/build.sh | Adds a FreeBSD entrypoint script that installs build dependencies and drives the build. |
| scripts/bsd/build-runtime.sh | Builds/strips the runtime on FreeBSD and outputs runtime-freebsd-<arch> plus debug symbols. |
| README.md | Mentions the availability of a separate FreeBSD runtime. |
| BUILD.md | Documents how to build and test the FreeBSD runtime. |
| .github/workflows/build-freebsd.yaml | Adds CI to build and smoke-test FreeBSD runtimes in VMs and upload artifacts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pkgconf \ | ||
| zstd |
| ssize_t length = readlink(appimage_path, fullpath, sizeof(fullpath)); | ||
| if (length < 0) { | ||
| if (!resolve_appimage_path(appimage_path, fullpath, sizeof(fullpath))) { | ||
| fprintf(stderr, "Error getting realpath for %s\n", appimage_path); |
| for (int wait_round = 0; wait_round < 100; ++wait_round) { | ||
| struct stat mounted_st; | ||
| if (stat(filename, &mounted_st) == 0) | ||
| break; | ||
|
|
||
| const struct timespec ten_ms = {0, 10 * 1000 * 1000}; | ||
| nanosleep(&ten_ms, NULL); | ||
| } |
|
Build for testing: |
|
Thank you very much @neilpang. I know that some people will be very happy. For this to be merged, though, we'd need a FreeBSD CI workflow. @pkgdemon maybe we can leverage some of the work you have done for building or even cross-compiling for FreeBSD on GitHub CI. Can you point us to a suitable example you'd recommend to be used as a blueprintß |
Closes #126
This makes the runtime build and run natively on FreeBSD, and builds it in CI.
It is based on the patch @enzo1982 shared in #126, reworked so that the Linux
code paths stay in place rather than being replaced.
Independent of #127 and #128 -- it touches neither the Linux libfuse pin nor
the
TARGET_APPIMAGEhandling, so it can land in any order relative to them.What FreeBSD needs
FreeBSD lacks both of the Linux mechanisms the runtime uses to find and mount
itself:
/proc/self/exe. The path of the running AppImage comes from theKERN_PROC_PATHNAMEsysctl instead, resolved once at startup. The threereadlink()call sites that used to turn/proc/self/exeinto a real pathare folded into one
resolve_appimage_path()helper, so the differencelives in one place.
fusermount. FreeBSD mounts throughmount_fusefs(8), which libfuseinvokes itself, so there is no setuid helper to go looking for on
$PATH.Both sit behind capability macros (
APPIMAGE_HAVE_PROC_SELF_EXE,APPIMAGE_HAVE_FUSERMOUNT) set from__linux__, so nothing changes for Linux.One more difference is not a missing feature but a timing one: on FreeBSD the
mount point is not reliably populated by the time libfuse reports the file
system as mounted, and
execv()ofAppRunfails. The parent polls for theentrypoint for up to a second, exiting the loop as soon as it appears -- in
practice on the first round, so the cost on a system that does not need it is
a single
stat().Build
scripts/bsd/mirrors the existing per-environment entry points inscripts/docker/andscripts/chroot/:build.shinstalls the packages,then calls
install-dependencies.shandbuild-runtime.sh.libfuse and squashfuse are built from source. FreeBSD does package both
(
filesystems/fusefs-libs3,filesystems/fusefs-squashfuse) but ships sharedlibraries only, and the runtime is statically linked.
libfuse is pinned to 3.18.2 on FreeBSD, rather than the 3.15.0 the Linux
build uses; the Linux pin is untouched. @enzo1982 reported in #126 that the BSD
fixes needed are in 3.17 and later, and 3.18 is what the BSDs themselves now
ship: FreeBSD's
filesystems/fusefs-libs3is 3.18.1 and carries no patches atall, and pkgsrc's
filesystems/fuse3is 3.18.2 withONLY_FOR_PLATFORM = Linux-*-* FreeBSD-*-* NetBSD-*-*. Thepatches/libfuse/mount.c.diffin this repo is not applied: it patchesmount.c, which is the Linux backend (lib/mount_bsd.cis used on FreeBSD).The link needs GNU
ldfromdevel/binutils. lld rejects theINSERT AFTER .interpindata_sections.ld, because a non-PIE static binaryhas no
.interpsection:devel/binutilswas already needed forobjcopy, so this costs nothing extra.scripts/bsd/build-runtime.shis a separate script fromscripts/build-runtime.shrather than a branch inside it, because that scriptassumes a GNU userland throughout:
nproc(1),bashat/bin/bashto detectthe architecture from, GNU make, and
objcopyin the base system.Verification
CI does not just build the runtime, it runs it:
scripts/bsd/test-runtime.shassembles a minimal AppImage around the freshly built binary and executes it,
so a runtime that links but cannot mount, or cannot find its own path, fails
the build instead of being shipped.
Verified on a fork, at the exact commit proposed here:
https://github.com/neilpang/type2-runtime/actions/runs/30375939214
Both architectures build a static binary and then mount and execute it:
$APPIMAGEthere is what the runtime resolved for itself through the sysctl,so that path being correct is the check on the
/proc/self/exereplacement.aarch64 has no accelerated runner and is emulated, but still costs single-digit
minutes, because only the libfuse library is built, not its utils, examples and
tests. "Cold" above means no prepared-image cache, which is what the first run
after a change to the install step looks like;
cache-after-preparekeeps thepackage installation out of the runs after that.
The OS/ABI difference in the table is expected rather than a defect: FreeBSD
dispatches on the ABI note, which ld.bfd emits for both, and the smoke test
confirms both actually run. (@enzo1982 saw this too in #126 and corrected it
with
elfedit; with base clang plus ld.bfd that is not necessary.)Points for you to decide
runtime-freebsd-<arch>, sinceit is a different binary from the Linux
runtime-<arch>of the samearchitecture and must not overwrite it.
into the
uploadjob inbuild.yaml, since whether FreeBSD runtimes belongon the release page is your call.
untouched; happy to fold it into
build.yamlinstead if you prefer.readlink(appimage_path, fullpath, sizeof(fullpath))calls wrotefullpath[length] = '\0'afterwards, which is one byte past the end when thepath fills the buffer. The new helper passes
sizeof - 1. Not needed to makeFreeBSD work; say the word and I will pull it into its own PR.
Other BSDs
#126 also wondered about NetBSD, OpenBSD and DragonFly. None of them are in
this PR, and here is what I found when I went looking, so nobody has to repeat
it:
sys/sys/sysctl.hhas noKERN_PROC_PATHNAME-- no equivalenteither -- so a process cannot learn its own path, which the runtime
fundamentally depends on. There is also no libfuse3 port.
filesystems/fusefs-libs3carriesIGNORE = fusefs has not been implemented on DragonFly.3.18.2 does build statically on NetBSD 10.1 (I tried it: meson configures,
ninja installs a 2.0 MB
libfuse3.a),mksquashfsis available frompkgsrc's
squashfs, and NetBSD does haveKERN_PROC_PATHNAME-- as aKERN_PROC_ARGSsubtype, so the mib differs in shape from FreeBSD's andwould need its own branch. What stops it is mounting:
lib/mount_bsd.copens
/dev/fuseand execsmount_fusefs, and NetBSD 10.1 has neither.It reaches FUSE through perfused and PUFFS instead, which upstream libfuse's
BSD backend does not drive. That is a piece of work in libfuse, not
something this repository can paper over, so I have left NetBSD out rather
than add a job that cannot pass.