Skip to content

Build the runtime for FreeBSD - #149

Open
neilpang wants to merge 3 commits into
AppImage:mainfrom
neilpang:freebsd-support
Open

Build the runtime for FreeBSD#149
neilpang wants to merge 3 commits into
AppImage:mainfrom
neilpang:freebsd-support

Conversation

@neilpang

Copy link
Copy Markdown

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_APPIMAGE handling, 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:

  • No /proc/self/exe. The path of the running AppImage comes from the
    KERN_PROC_PATHNAME sysctl instead, resolved once at startup. The three
    readlink() call sites that used to turn /proc/self/exe into a real path
    are folded into one resolve_appimage_path() helper, so the difference
    lives in one place.
  • No fusermount. FreeBSD mounts through mount_fusefs(8), which libfuse
    invokes 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() of AppRun fails. The parent polls for the
entrypoint 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 in
scripts/docker/ and scripts/chroot/: build.sh installs the packages,
then calls install-dependencies.sh and build-runtime.sh.

libfuse and squashfuse are built from source. FreeBSD does package both
(filesystems/fusefs-libs3, filesystems/fusefs-squashfuse) but ships shared
libraries 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-libs3 is 3.18.1 and carries no patches at
all, and pkgsrc's filesystems/fuse3 is 3.18.2 with
ONLY_FOR_PLATFORM = Linux-*-* FreeBSD-*-* NetBSD-*-*. The
patches/libfuse/mount.c.diff in this repo is not applied: it patches
mount.c, which is the Linux backend (lib/mount_bsd.c is used on FreeBSD).

The link needs GNU ld from devel/binutils. lld rejects the
INSERT AFTER .interp in data_sections.ld, because a non-PIE static binary
has no .interp section:

ld: error: unable to insert .appimage after .interp

devel/binutils was already needed for objcopy, so this costs nothing extra.

scripts/bsd/build-runtime.sh is a separate script from
scripts/build-runtime.sh rather than a branch 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.

Verification

CI does not just build the runtime, it runs it: scripts/bsd/test-runtime.sh
assembles 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:

Both architectures build a static binary and then mount and execute it:

x86_64   runtime: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD),
                  statically linked, for FreeBSD 14.3, FreeBSD-style
aarch64  runtime: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV),
                  statically linked, for FreeBSD 14.3, FreeBSD-style

AppRun running from /tmp//.mount_smoketG6xpY9
APPIMAGE is /tmp/type2-runtime-test.KoMd2S5JNa/smoketest.AppImage
smoke test ok
FreeBSD runtime smoke test passed

$APPIMAGE there is what the runtime resolved for itself through the sysctl,
so that path being correct is the check on the /proc/self/exe replacement.

arch runtime size job, warm job, cold
x86_64 1388936 1 min 16 s 2 min 16 s
aarch64 1335264 3 min 49 s 8 min 29 s

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-prepare keeps the
package 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

  • Artifact naming. The FreeBSD runtime is runtime-freebsd-<arch>, since
    it is a different binary from the Linux runtime-<arch> of the same
    architecture and must not overwrite it.
  • Releases. The workflow uploads artifacts but is deliberately not wired
    into the upload job in build.yaml, since whether FreeBSD runtimes belong
    on the release page is your call.
  • Separate workflow. Kept in its own file so the Linux pipeline is
    untouched; happy to fold it into build.yaml instead if you prefer.
  • A drive-by fix you may want split out. The open-coded
    readlink(appimage_path, fullpath, sizeof(fullpath)) calls wrote
    fullpath[length] = '\0' afterwards, which is one byte past the end when the
    path fills the buffer. The new helper passes sizeof - 1. Not needed to make
    FreeBSD 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:

  • OpenBSD: sys/sys/sysctl.h has no KERN_PROC_PATHNAME -- no equivalent
    either -- so a process cannot learn its own path, which the runtime
    fundamentally depends on. There is also no libfuse3 port.
  • DragonFly: filesystems/fusefs-libs3 carries
    IGNORE = fusefs has not been implemented on DragonFly.
  • NetBSD: the closest of the three, but not close enough yet. libfuse
    3.18.2 does build statically on NetBSD 10.1 (I tried it: meson configures,
    ninja installs a 2.0 MB libfuse3.a), mksquashfs is available from
    pkgsrc's squashfs, and NetBSD does have KERN_PROC_PATHNAME -- as a
    KERN_PROC_ARGS subtype, so the mib differs in shape from FreeBSD's and
    would need its own branch. What stops it is mounting: lib/mount_bsd.c
    opens /dev/fuse and execs mount_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.

neilpang added 3 commits July 28, 2026 23:57
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
Copilot AI review requested due to automatic review settings July 29, 2026 02:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread scripts/bsd/build.sh
Comment on lines +31 to +32
pkgconf \
zstd
Comment thread src/runtime/runtime.c
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);
Comment thread src/runtime/runtime.c
Comment on lines +1920 to +1927
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);
}
@github-actions

Copy link
Copy Markdown

Build for testing:
artifacts i686
artifacts x86_64
artifacts aarch64
artifacts armhf
Use at your own risk.

@probonopd

Copy link
Copy Markdown
Member

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ß

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider adding support for FreeBSD

3 participants