From d4f1fedcb8f7988f37219b26610982a385bfa4bb Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 1 Apr 2026 14:09:37 +0200 Subject: [PATCH] Unblock Fruix checkout runtime on FreeBSD --- docs/PROGRESS.md | 51 +++++ .../phase5-checkout-runtime-freebsd.md | 102 +++++++++ .../patches/phase5-checkout-runtime.patch | 78 +++++++ .../patches/phase5-guix-daemon-freebsd.patch | 58 +++++ tests/guix/run-phase5-checkout-runtime.sh | 98 +++++++++ tests/guix/setup-phase5-checkout.sh | 199 ++++++++++++++++++ 6 files changed, 586 insertions(+) create mode 100644 docs/reports/phase5-checkout-runtime-freebsd.md create mode 100644 tests/guix/patches/phase5-checkout-runtime.patch create mode 100644 tests/guix/patches/phase5-guix-daemon-freebsd.patch create mode 100755 tests/guix/run-phase5-checkout-runtime.sh create mode 100755 tests/guix/setup-phase5-checkout.sh diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index f4e3df5..4920dec 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1564,3 +1564,54 @@ Current assessment: - first make the upstream-derived checkout runnable on FreeBSD, - then introduce a deliberate `fruix` command boundary, - rather than destabilizing the codebase with a whole-tree `guix`/`gnu` rename too early + +## 2026-04-01 — Phase 5.1 completed: checkout runtime unblocked and first `fruix` frontend boundary established + +Completed work: + +- added a reusable phase-5 checkout setup helper: + - `tests/guix/setup-phase5-checkout.sh` +- added a checkout runtime patch queue for the upstream-derived source tree: + - `tests/guix/patches/phase5-checkout-runtime.patch` +- added a FreeBSD daemon/build patch queue needed for later phase-5 work: + - `tests/guix/patches/phase5-guix-daemon-freebsd.patch` +- added a runtime validation harness: + - `tests/guix/run-phase5-checkout-runtime.sh` +- wrote the Phase 5.1 report: + - `docs/reports/phase5-checkout-runtime-freebsd.md` +- ran the runtime harness successfully and captured metadata under: + - `/tmp/phase5-runtime-metadata.txt` + +Important findings: + +- the earlier checkout blocker + - `./pre-inst-env guix --version` + - `Wrong type to apply: #` + is now explained by top-level definition ordering in `guix/ui.scm`: + - `show-version-and-exit` called `leave-on-EPIPE` before the syntax transformer was defined later in the file + - on this FreeBSD path, that became a runtime application of a syntax-transformer object instead of a macro expansion site +- the phase-5 runtime patch fixes this by: + - making `(guix ui)` explicitly non-declarative + - rewriting `show-version-and-exit` to use direct `catch 'system-error` handling + - parameterizing `program-name` in `guix-main` + - deriving the top-level version banner name from `program-name` + - making `(guix scripts repl)` explicitly non-declarative as well +- the checkout now successfully runs the following commands on FreeBSD: + - `./pre-inst-env guix --version` + - `./pre-inst-env guix repl --help` + - `./pre-inst-env guix build --help` +- the first user-facing Fruix command boundary is now implemented in the checkout setup via: + - `scripts/fruix` + as a front-end alias next to `scripts/guix` +- observed runtime metadata confirmed: + - `first_guix_version_line=guix (GNU Guix) ...` + - `first_fruix_version_line=fruix (GNU Guix) ...` +- this matches the agreed naming policy: + - Fruix at the user-facing boundary + - stable upstream-derived internal `guix`/`gnu` names unless there is a concrete reason to rename them + +Current assessment: + +- Phase 5.1 is now satisfied on the current FreeBSD prototype track +- the key boundary has shifted from “the checkout still crashes immediately” to “the checkout runs, and can now be used as the basis for real derivation/store experiments” +- the next step is to prove that a real derivation can be emitted against `/frx/store` from the now-runnable checkout diff --git a/docs/reports/phase5-checkout-runtime-freebsd.md b/docs/reports/phase5-checkout-runtime-freebsd.md new file mode 100644 index 0000000..f9c65c2 --- /dev/null +++ b/docs/reports/phase5-checkout-runtime-freebsd.md @@ -0,0 +1,102 @@ +# Phase 5.1: Unblocked the checkout command path on FreeBSD and established a first `fruix` frontend boundary + +Date: 2026-04-01 + +## Summary + +This step moves the FreeBSD port from “the checkout configures and builds generated scripts” to “the checkout actually runs useful commands”. + +Added files: + +- `tests/guix/patches/phase5-checkout-runtime.patch` +- `tests/guix/patches/phase5-guix-daemon-freebsd.patch` +- `tests/guix/setup-phase5-checkout.sh` +- `tests/guix/run-phase5-checkout-runtime.sh` + +## Root cause of the `leave-on-EPIPE` failure + +The earlier failure: + +- `./pre-inst-env guix --version` +- `Wrong type to apply: #` + +was traced to the ordering of top-level definitions in `guix/ui.scm`. + +More specifically: + +- `show-version-and-exit` called `leave-on-EPIPE` +- `leave-on-EPIPE` was defined later in the file as a syntax transformer +- on this FreeBSD path, the earlier call site was compiled as a value application rather than as a macro expansion site +- when invoked at runtime, Guile attempted to apply the syntax transformer as a procedure + +This produced the observed error: + +- `Wrong type to apply: #` + +## Runtime patch strategy + +The runtime patch takes a deliberately small approach: + +1. make `(guix ui)` explicitly non-declarative for this command-path use case +2. rewrite `show-version-and-exit` to use a direct `catch 'system-error` block instead of relying on the later syntax binding +3. parameterize `program-name` in `guix-main` +4. derive the version-banner command name from `program-name` +5. make `(guix scripts repl)` explicitly non-declarative as well, since it also uses `load` + +This resolves the FreeBSD checkout runtime failure without forcing a broad internal rename. + +## FreeBSD/Fruix frontend policy implemented + +The step also establishes the first user-facing `fruix` checkout boundary. + +Current policy implemented by the harness: + +- retain upstream-derived internal `guix` module and script structure +- generate a `scripts/fruix` symlink next to `scripts/guix` +- rely on the new `program-name` handling so: + - `./pre-inst-env guix --version` reports `guix ...` + - `./pre-inst-env fruix --version` reports `fruix ...` + +This is intentionally a **front-end boundary**, not a whole-tree rename. + +## Validation command + +Run command: + +```sh +METADATA_OUT=/tmp/phase5-runtime-metadata.txt \ +./tests/guix/run-phase5-checkout-runtime.sh +``` + +## Observed results + +The runtime harness validated all of the following successfully: + +1. `./pre-inst-env guix --version` +2. `./pre-inst-env guix repl --help` +3. `./pre-inst-env guix build --help` +4. `./pre-inst-env fruix --version` + +Observed metadata included: + +- `first_guix_version_line=guix (GNU Guix) ...` +- `first_fruix_version_line=fruix (GNU Guix) ...` +- `repl_usage_line=Usage: guix repl [OPTIONS...] [-- FILE ARGS...]` +- `build_usage_line=Usage: guix build [OPTION]... PACKAGE-OR-DERIVATION...` + +## Important notes + +- the first-step `fruix` boundary currently changes the top-level version banner correctly +- subcommand help output still exposes upstream-derived `guix` wording internally +- this is acceptable for the current transition stage and matches the agreed naming policy: + - Fruix at the user-facing boundary + - stable upstream-derived internals unless there is strong reason to rename them + +## Conclusion + +Phase 5.1 is satisfied on the current FreeBSD prototype track: + +- the checkout command path now runs on FreeBSD +- the `leave-on-EPIPE` blocker is resolved in the checkout patch queue +- at least one additional non-trivial checkout subcommand works +- a first user-facing `fruix` checkout frontend boundary now exists without a destabilizing blanket rename diff --git a/tests/guix/patches/phase5-checkout-runtime.patch b/tests/guix/patches/phase5-checkout-runtime.patch new file mode 100644 index 0000000..c5a679a --- /dev/null +++ b/tests/guix/patches/phase5-checkout-runtime.patch @@ -0,0 +1,78 @@ +--- a/guix/ui.scm ++++ b/guix/ui.scm +@@ -37,6 +37,7 @@ + ;;; along with GNU Guix. If not, see . + + (define-module (guix ui) ;import in user interfaces only ++ #:declarative? #f + #:use-module (guix i18n) + #:use-module (guix colors) + #:use-module (guix diagnostics) +@@ -562,20 +563,25 @@ + + (define* (show-version-and-exit #:optional (command (car (command-line)))) + "Display version information for COMMAND and `(exit 0)'." +- (leave-on-EPIPE +- (simple-format #t "~a (~a) ~a~%" +- command %guix-package-name %guix-version) +- (format #t "Copyright ~a 2026 ~a" +- ;; TRANSLATORS: Translate "(C)" to the copyright symbol +- ;; (C-in-a-circle), if this symbol is available in the user's +- ;; locale. Otherwise, do not translate "(C)"; leave it as-is. */ +- (G_ "(C)") +- (G_ "the Guix authors\n")) +- (display (G_"\ ++ (catch 'system-error ++ (lambda () ++ (simple-format #t "~a (~a) ~a~%" ++ command %guix-package-name %guix-version) ++ (format #t "Copyright ~a 2026 ~a" ++ ;; TRANSLATORS: Translate "(C)" to the copyright symbol ++ ;; (C-in-a-circle), if this symbol is available in the user's ++ ;; locale. Otherwise, do not translate "(C)"; leave it as-is. */ ++ (G_ "(C)") ++ (G_ "the Guix authors\n")) ++ (display (G_"\ + License GPLv3+: GNU GPL version 3 or later + This is free software: you are free to change and redistribute it. + There is NO WARRANTY, to the extent permitted by law. +-"))) ++"))) ++ (lambda args ++ (if (= EPIPE (system-error-errno args)) ++ (primitive-_exit 0) ++ (apply throw args)))) + (exit 0)) + + (define (show-bug-report-information) +@@ -2388,7 +2394,7 @@ + ((or ("-h") ("--help")) + (leave-on-EPIPE (show-guix-help))) + ((or ("-V") ("--version")) +- (show-version-and-exit "guix")) ++ (show-version-and-exit (basename (or (program-name) "guix")))) + (((? option? o) args ...) + (format (current-error-port) + (G_ "guix: unrecognized option '~a'~%") o) +@@ -2404,8 +2410,9 @@ + args))))) + + (define (guix-main arg0 . args) +- (initialize-guix) +- (apply run-guix args)) ++ (parameterize ((program-name (basename arg0))) ++ (initialize-guix) ++ (apply run-guix args))) + + ;;; Local Variables: + ;;; eval: (put 'guard* 'scheme-indent-function 2) +--- a/guix/scripts/repl.scm ++++ b/guix/scripts/repl.scm +@@ -19,6 +19,7 @@ + ;;; along with GNU Guix. If not, see . + + (define-module (guix scripts repl) ++ #:declarative? #f + #:use-module (guix ui) + #:use-module (guix scripts) + #:use-module (guix repl) diff --git a/tests/guix/patches/phase5-guix-daemon-freebsd.patch b/tests/guix/patches/phase5-guix-daemon-freebsd.patch new file mode 100644 index 0000000..b6c14ba --- /dev/null +++ b/tests/guix/patches/phase5-guix-daemon-freebsd.patch @@ -0,0 +1,58 @@ +--- a/nix/nix-daemon/nix-daemon.cc ++++ b/nix/nix-daemon/nix-daemon.cc +@@ -169,7 +169,11 @@ + + static void handleSignal(int signum) + { ++#if defined(__FreeBSD__) ++ string name = getprogname(); ++#else + string name = program_invocation_short_name; ++#endif + auto message = name + ": PID " + std::to_string(getpid()) + + " caught signal " + std::to_string(signum) + "\n"; + writeFull(STDERR_FILENO, (unsigned char *) message.c_str(), message.length()); +@@ -940,12 +944,12 @@ + + /* If we're on a TCP connection, disable Nagle's algorithm so that + data is sent as soon as possible. */ +- (void) setsockopt(remote, SOL_TCP, TCP_NODELAY, ++ (void) setsockopt(remote, IPPROTO_TCP, TCP_NODELAY, + &enabled, sizeof enabled); + + #if defined(TCP_QUICKACK) + /* Enable TCP quick-ack if applicable; this might help a little. */ +- (void) setsockopt(remote, SOL_TCP, TCP_QUICKACK, ++ (void) setsockopt(remote, IPPROTO_TCP, TCP_QUICKACK, + &enabled, sizeof enabled); + #endif + } +--- a/nix/libutil/spawn.cc ++++ b/nix/libutil/spawn.cc +@@ -31,6 +31,10 @@ + #include + #include + #include ++ ++#if defined(__FreeBSD__) ++extern char **environ; ++#endif + + #if HAVE_SYS_MOUNT_H + #include +--- a/nix/libutil/archive.cc ++++ b/nix/libutil/archive.cc +@@ -298,9 +298,10 @@ + errno = posix_fallocate(fd, 0, len); + /* Note that EINVAL may indicate that the underlying + filesystem doesn't support preallocation (e.g. on +- OpenSolaris). Since preallocation is just an +- optimisation, ignore it. */ +- if (errno && errno != EINVAL) ++ OpenSolaris). On FreeBSD, EOPNOTSUPP/ENOTSUP can be ++ returned for the same reason. Since preallocation is ++ just an optimisation, ignore those cases. */ ++ if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOTSUP) + throw SysError(std::format("preallocating file of {} bytes", len)); + } + #endif diff --git a/tests/guix/run-phase5-checkout-runtime.sh b/tests/guix/run-phase5-checkout-runtime.sh new file mode 100755 index 0000000..0e3f7a9 --- /dev/null +++ b/tests/guix/run-phase5-checkout-runtime.sh @@ -0,0 +1,98 @@ +#!/bin/sh +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +metadata_target=${METADATA_OUT:-} + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-phase5-runtime.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +cleanup_workdir() { + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +setup_metadata=$workdir/setup-metadata.txt +setup_env=$workdir/setup-env.sh +runtime_log=$workdir/runtime-command.log +metadata_file=$workdir/phase5-runtime-metadata.txt + +WORKDIR=$workdir/setup KEEP_WORKDIR=1 \ + METADATA_OUT=$setup_metadata ENV_OUT=$setup_env \ + "$repo_root/tests/guix/setup-phase5-checkout.sh" >"$workdir/setup.log" 2>&1 + +# shellcheck disable=SC1090 +. "$setup_env" + +cd "$PHASE5_BUILDDIR" +export LD_LIBRARY_PATH GUILE_LOAD_PATH GUILE_LOAD_COMPILED_PATH GUILE_EXTENSIONS_PATH +export GUILE_AUTO_COMPILE=0 + +./pre-inst-env guix --version >"$workdir/guix-version.log" 2>&1 +./pre-inst-env guix repl --help >"$workdir/guix-repl-help.log" 2>&1 +./pre-inst-env guix build --help >"$workdir/guix-build-help.log" 2>&1 +./pre-inst-env fruix --version >"$workdir/fruix-version.log" 2>&1 + +first_guix_version_line=$(sed -n '1p' "$workdir/guix-version.log") +first_fruix_version_line=$(sed -n '1p' "$workdir/fruix-version.log") +repl_usage_line=$(grep -m1 '^Usage:' "$workdir/guix-repl-help.log" || true) +build_usage_line=$(grep -m1 '^Usage:' "$workdir/guix-build-help.log" || true) + +printf '%s\n' "$first_guix_version_line" | grep -q '^guix (GNU Guix) ' || { + echo "unexpected guix version output: $first_guix_version_line" >&2 + exit 1 +} +printf '%s\n' "$first_fruix_version_line" | grep -q '^fruix (GNU Guix) ' || { + echo "unexpected fruix version output: $first_fruix_version_line" >&2 + exit 1 +} +printf '%s\n' "$repl_usage_line" | grep -q '^Usage: guix repl ' || { + echo "unexpected guix repl help output: $repl_usage_line" >&2 + exit 1 +} +printf '%s\n' "$build_usage_line" | grep -q '^Usage: guix build' || { + echo "unexpected guix build help output: $build_usage_line" >&2 + exit 1 +} + +cat >"$metadata_file" <&2 + exit 1 +fi +if [ ! -d "$source_repo/guix" ]; then + echo "Guix source tree not found at $source_repo" >&2 + exit 1 +fi +for tool in git patch "$make_bin" gm4 "$gsed_bin"; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Required tool not found: $tool" >&2 + exit 1 + fi +done +if [ ! -f /usr/local/include/argp.h ]; then + echo "argp-standalone headers not found at /usr/local/include/argp.h" >&2 + exit 1 +fi + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-phase5-checkout.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +cleanup_workdir() { + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +srcclone=$workdir/guix-src +builddir=$workdir/build +metadata_file=$workdir/phase5-checkout-setup-metadata.txt +env_file=$workdir/phase5-checkout-env.sh +bootstrap_log=$workdir/bootstrap.log +configure_log=$workdir/configure.log +make_scripts_log=$workdir/make-scripts.log +daemon_build_log=$workdir/make-guix-daemon.log + +runtime_patch=$repo_root/tests/guix/patches/phase5-checkout-runtime.patch +daemon_patch=$repo_root/tests/guix/patches/phase5-guix-daemon-freebsd.patch + +guile_bindir=$(CDPATH= cd -- "$(dirname "$guile_bin")" && pwd) +guile_prefix=$(CDPATH= cd -- "$guile_bindir/.." && pwd) +guile_lib_dir=$guile_prefix/lib +guile_version=$(LD_LIBRARY_PATH="$guile_lib_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$guile_bin" -c '(display (effective-version))') +extra_site_dir=$guile_extra_prefix/share/guile/site/$guile_version +extra_site_ccache_dir=$guile_extra_prefix/lib/guile/$guile_version/site-ccache +extra_extensions_dir=$guile_extra_prefix/lib/guile/$guile_version/extensions + +tool_bindir=$workdir/guile-tools-bin +mkdir -p "$tool_bindir" +ln -sf "$guile_bin" "$tool_bindir/guile" +ln -sf "$guile_bin" "$tool_bindir/guile-3.0" +ln -sf "$guile_bindir/guild" "$tool_bindir/guild" +ln -sf "$guile_bindir/guild" "$tool_bindir/guild-3.0" +ln -sf "$guile_bindir/guile-config" "$tool_bindir/guile-config" +ln -sf "$guile_bindir/guile-config" "$tool_bindir/guile-config-3.0" +ln -sf "$guile_bindir/guile-snarf" "$tool_bindir/guile-snarf" + +export PATH="$tool_bindir:$guile_bindir:/usr/local/bin:$PATH" +export ACLOCAL_PATH=/usr/local/share/aclocal${ACLOCAL_PATH:+:$ACLOCAL_PATH} +export PKG_CONFIG_PATH=$guile_extra_prefix/lib/pkgconfig:$guile_extra_prefix/libdata/pkgconfig:$guile_prefix/lib/pkgconfig:/usr/local/libdata/pkgconfig:/usr/local/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH} +export CPPFLAGS="-I$guile_prefix/include -I$guile_extra_prefix/include -I/usr/local/include" +export LDFLAGS="-L$guile_lib_dir -L$guile_extra_prefix/lib -L/usr/local/lib -Wl,-rpath,$guile_lib_dir -Wl,-rpath,$guile_extra_prefix/lib -Wl,-rpath,/usr/local/lib" +export LD_LIBRARY_PATH="$guile_extra_prefix/lib:$guile_lib_dir:/usr/local/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +if [ -d "$extra_site_dir" ]; then + export GUILE_LOAD_PATH="$extra_site_dir${GUILE_LOAD_PATH:+:$GUILE_LOAD_PATH}" +fi +if [ -d "$extra_site_ccache_dir" ]; then + export GUILE_LOAD_COMPILED_PATH="$extra_site_ccache_dir${GUILE_LOAD_COMPILED_PATH:+:$GUILE_LOAD_COMPILED_PATH}" +fi +if [ -d "$extra_extensions_dir" ]; then + export GUILE_EXTENSIONS_PATH="$extra_extensions_dir${GUILE_EXTENSIONS_PATH:+:$GUILE_EXTENSIONS_PATH}" +fi + +printf 'Working directory: %s\n' "$workdir" +printf 'Cloning source from: %s\n' "$source_repo" +rm -rf "$srcclone" "$builddir" +git clone --shared "$source_repo" "$srcclone" >/dev/null 2>&1 + +( + cd "$srcclone" + M4=gm4 ./bootstrap +) >"$bootstrap_log" 2>&1 + +( + cd "$srcclone" + patch -p1 < "$runtime_patch" + patch -p1 < "$daemon_patch" +) >/dev/null + +mkdir -p "$builddir" +( + cd "$builddir" + MAKE="$make_bin" \ + GUILE="$guile_bin" \ + GUILE_EFFECTIVE_VERSION=3.0 \ + SED="$gsed_bin" \ + "$srcclone/configure" \ + --with-courage \ + --with-store-dir="$store_dir" \ + --localstatedir="$localstatedir" \ + --sysconfdir="$sysconfdir" +) >"$configure_log" 2>&1 + +( + cd "$builddir" + "$make_bin" -j1 scripts/guix +) >"$make_scripts_log" 2>&1 + +( + cd "$builddir" + "$make_bin" -j1 LIBS='-L/usr/local/lib -largp -lintl' nix/libstore/schema.sql.hh guix-daemon +) >"$daemon_build_log" 2>&1 + +ln -sf guix "$builddir/scripts/fruix" + +cat >"$env_file" <"$metadata_file" <