From 6d3f9455d3f4633ab6bbe321e260501f43ab31d5 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 08:56:34 -0400 Subject: [PATCH 1/8] Fix gnutls cross build: drop man/devdoc outputs when disabling docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cross overlay forces --disable-doc on gnutls (its doc build runs generated target binaries, which fails under cross-compilation), but left the "man" and "devdoc" outputs declared. With docs disabled neither output is populated, so Nix fails: "failed to produce output path for output 'devdoc'" (and then 'man'). Upstream couples these — it only passes --disable-doc for MinGW and drops both outputs in the same case. Mirror that by filtering man/devdoc out of the outputs list. This was the next failure after the cpython-runtime/dist-root fixes let the build progress to the gdbserver (gdb -> elfutils -> libmicrohttpd -> gnutls) dependency chain. Verified gnutls aarch64-linux-musl now builds. --- src/cross-overlays.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cross-overlays.nix b/src/cross-overlays.nix index c56dd3e..d6a1c84 100644 --- a/src/cross-overlays.nix +++ b/src/cross-overlays.nix @@ -21,9 +21,15 @@ }) # GnuTLS docs builds run generated target binaries such as lt-errcodes. + # --disable-doc skips the doc build, so neither the "man" nor "devdoc" + # outputs get populated. Upstream couples these: it only passes + # --disable-doc for MinGW and drops both outputs in the same case. Mirror + # that here, or Nix fails with "failed to produce output path for output + # 'devdoc'" (then 'man'). (self: super: { gnutls = super.gnutls.overrideAttrs (o: { configureFlags = (o.configureFlags or [ ]) ++ [ "--disable-doc" ]; + outputs = builtins.filter (x: x != "man" && x != "devdoc") (o.outputs or [ "out" ]); }); }) From d99767d64ef8fc040168c97a0c523acf0def6fb4 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 09:23:27 -0400 Subject: [PATCH 2/8] Fix argp-standalone cross build: disable stackprotector elfutils pulls in argp-standalone on musl. Its meson build links testsuite example binaries (ex3/ex4) with stack-protector on, but some 32-bit musl toolchains (e.g. powerpc) don't provide __stack_chk_fail_local, so linking fails: "undefined reference to __stack_chk_fail_local". Only the static lib is consumed by elfutils, so disable the stackprotector hardening flag, which removes the reference from both the lib and the examples. Next failure after the gnutls output fix let the build reach the powerpc gdbserver (gdb -> elfutils -> argp-standalone) chain. --- src/cross-overlays.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cross-overlays.nix b/src/cross-overlays.nix index d6a1c84..0348e89 100644 --- a/src/cross-overlays.nix +++ b/src/cross-overlays.nix @@ -48,4 +48,15 @@ nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ super.buildPackages.pkg-config ]; }); }) + + # argp-standalone (pulled in by elfutils on musl) builds testsuite example + # binaries with stack-protector on. On some 32-bit musl targets (e.g. + # powerpc) the toolchain doesn't provide __stack_chk_fail_local, so linking + # the examples fails with "undefined reference to __stack_chk_fail_local". + # Only the static lib is consumed downstream, so just drop the hardening. + (self: super: { + argp-standalone = super.argp-standalone.overrideAttrs (o: { + hardeningDisable = (o.hardeningDisable or [ ]) ++ [ "stackprotector" ]; + }); + }) ] From bcc75466e6da32aad8eaf31ff810e488b1b5c8a8 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 09:54:56 -0400 Subject: [PATCH 3/8] Drop 32-bit PowerPC (ppc) arch; revert argp stackprotector workaround Penguin only supports ppc64, so remove the 32-bit powerpc-linux-musl arch from the build matrix instead of working around its broken toolchain. The 32-bit PowerPC musl toolchain lacks __stack_chk_fail_local, which broke any stack-protector static link (argp-standalone, libipt, ...) in the gdbserver dependency tree. ppc64/ppc64el are unaffected and stay. This also reverts the argp-standalone hardeningDisable workaround added for that arch, which is no longer needed. --- src/archs.nix | 8 -------- src/cross-overlays.nix | 11 ----------- 2 files changed, 19 deletions(-) diff --git a/src/archs.nix b/src/archs.nix index c8e6e74..c682c32 100644 --- a/src/archs.nix +++ b/src/archs.nix @@ -55,14 +55,6 @@ }; }; - ppc = { - penguinName = "powerpc"; - compatNames = [ "powerpcle" ]; - crossSystem = { - config = "powerpc-linux-musl"; - }; - }; - ppc64 = { penguinName = "powerpc64"; crossSystem = { diff --git a/src/cross-overlays.nix b/src/cross-overlays.nix index 0348e89..d6a1c84 100644 --- a/src/cross-overlays.nix +++ b/src/cross-overlays.nix @@ -48,15 +48,4 @@ nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ super.buildPackages.pkg-config ]; }); }) - - # argp-standalone (pulled in by elfutils on musl) builds testsuite example - # binaries with stack-protector on. On some 32-bit musl targets (e.g. - # powerpc) the toolchain doesn't provide __stack_chk_fail_local, so linking - # the examples fails with "undefined reference to __stack_chk_fail_local". - # Only the static lib is consumed downstream, so just drop the hardening. - (self: super: { - argp-standalone = super.argp-standalone.overrideAttrs (o: { - hardeningDisable = (o.hardeningDisable or [ ]) ++ [ "stackprotector" ]; - }); - }) ] From c652a8218d8af34819a4c6c25eb531acd8a1afa6 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 12:15:52 -0400 Subject: [PATCH 4/8] Fix gdb cross build on musl: user_gcs redefinition and termios fields Two gdb-17.1-on-musl compile failures, surfaced once the ppc32 drop let CI reach the gdb builds: - aarch64: gdb defines its own `struct user_gcs` unless GCS_MAGIC is defined, but kernel headers >=6.13 put user_gcs in while GCS_MAGIC is in (not included by ptrace.h), so the struct is redefined. Guard on ptrace.h's own include guard (__ASM_PTRACE_H) instead: files that include ptrace.h use the kernel struct, aarch64-linux-tdep.c (which doesn't) keeps gdb's fallback. - all musl arches: ser-unix.c's custom-baudrate path uses .c_ispeed/.c_ospeed, but musl names those fields __c_ispeed/__c_ospeed (the struct termios2 path needs TCGETS2, which musl never defines). Map the field accesses to the musl names. Verified gdb-host-cpu-only-aarch64-linux-musl now builds to completion. --- src/pkgs/gdbserver.nix | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/pkgs/gdbserver.nix b/src/pkgs/gdbserver.nix index 94f9bc8..82215df 100644 --- a/src/pkgs/gdbserver.nix +++ b/src/pkgs/gdbserver.nix @@ -7,6 +7,25 @@ pkgs.gdbHostCpuOnly.overrideAttrs (prev: { substituteInPlace gdb/mips-linux-nat.c \ --replace '' '' \ --replace '_ABIO32' '1' + + # gdb only skips its own "struct user_gcs" definition when GCS_MAGIC is + # defined, but recent aarch64 kernel headers (>=6.13) put user_gcs in + # while GCS_MAGIC lives in (which + # ptrace.h does not include), so the guard never fires and the struct is + # redefined. Key the guard off ptrace.h's own include guard instead: the + # files that include get the kernel's struct, and the one + # that doesn't (aarch64-linux-tdep.c) keeps gdb's fallback definition. + substituteInPlace gdb/arch/aarch64-gcs-linux.h \ + --replace '#ifndef GCS_MAGIC' '#ifndef __ASM_PTRACE_H' + + # gdb's Linux custom-baudrate path uses struct termios2 when TCGETS2 is + # defined, otherwise plain struct termios with .c_ispeed/.c_ospeed. musl + # never defines TCGETS2, so the fallback is taken, but musl names those + # fields __c_ispeed/__c_ospeed. All our targets are musl, so map the two + # field accesses to the musl names. + substituteInPlace gdb/ser-unix.c \ + --replace 'tio.c_ospeed = rate;' 'tio.__c_ospeed = rate;' \ + --replace 'tio.c_ispeed = rate;' 'tio.__c_ispeed = rate;' ''; meta.mainProgram = "gdbserver"; From 52776daba90cf9f905d8404dbd9f460b6d8b92d7 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 13:40:54 -0400 Subject: [PATCH 5/8] Pin gdb to stable 16.3 for musl cross instead of 17.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gdb 17.1 (from the nixpkgs pin) does not build against musl + modern kernel headers across our arch set: aarch64 hit a struct user_gcs redefinition, all arches hit ser-unix.c's custom-baudrate code using glibc-only termios fields, and gdbserver's in-process agent configure errors out on targets like armv7l. These are gdb-17-era additions. Rather than forward-port an open-ended tail of musl fixes, pin gdb to 16.3 — the version Alpine and Buildroot ship on musl across all these arches. 16.3 predates the GCS and custom-baudrate code, so those failures disappear entirely. Keep the mips sgidefs fixup, and disable the in-process agent (unsupported on some targets, and unused by us). --- src/pkgs/gdbserver.nix | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/pkgs/gdbserver.nix b/src/pkgs/gdbserver.nix index 82215df..3173e71 100644 --- a/src/pkgs/gdbserver.nix +++ b/src/pkgs/gdbserver.nix @@ -1,32 +1,28 @@ pkgs: +# gdb is pinned to 16.3 rather than tracking nixpkgs' latest: 16.3 is the +# version Alpine/Buildroot ship on musl across our arch set, and it predates +# the gdb-17 GCS (struct user_gcs) and custom-baudrate (termios c_ispeed) code +# that does not compile against musl + modern kernel headers. Stable over new. pkgs.gdbHostCpuOnly.overrideAttrs (prev: { + version = "16.3"; + src = pkgs.fetchurl { + url = "mirror://gnu/gdb/gdb-16.3.tar.xz"; + hash = "sha256-vPzQlVKKmHkXrPn/8/FnIYFpSSbMGNYJyZ0AQsACJMU="; + }; + postPatch = (prev.postPatch or "") + '' substituteInPlace gdb/mips-linux-nat.c \ --replace '' '' \ --replace '_ABIO32' '1' - - # gdb only skips its own "struct user_gcs" definition when GCS_MAGIC is - # defined, but recent aarch64 kernel headers (>=6.13) put user_gcs in - # while GCS_MAGIC lives in (which - # ptrace.h does not include), so the guard never fires and the struct is - # redefined. Key the guard off ptrace.h's own include guard instead: the - # files that include get the kernel's struct, and the one - # that doesn't (aarch64-linux-tdep.c) keeps gdb's fallback definition. - substituteInPlace gdb/arch/aarch64-gcs-linux.h \ - --replace '#ifndef GCS_MAGIC' '#ifndef __ASM_PTRACE_H' - - # gdb's Linux custom-baudrate path uses struct termios2 when TCGETS2 is - # defined, otherwise plain struct termios with .c_ispeed/.c_ospeed. musl - # never defines TCGETS2, so the fallback is taken, but musl names those - # fields __c_ispeed/__c_ospeed. All our targets are musl, so map the two - # field accesses to the musl names. - substituteInPlace gdb/ser-unix.c \ - --replace 'tio.c_ospeed = rate;' 'tio.__c_ospeed = rate;' \ - --replace 'tio.c_ispeed = rate;' 'tio.__c_ispeed = rate;' ''; + # gdbserver's in-process agent (fast tracepoints) is not supported on every + # target and its configure makes that a hard error (e.g. armv7l-musl). We + # don't use the IPA, so disable it everywhere. + configureFlags = (prev.configureFlags or [ ]) ++ [ "--disable-inprocess-agent" ]; + meta.mainProgram = "gdbserver"; }) From 1ec83e171cf0ad3bc204f1eabe8c63e7c1a62133 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 14:43:11 -0400 Subject: [PATCH 6/8] Fix ltrace cross build on ppc musl The PowerPC backend tripped three glibc-isms under musl: - The injected error() macro (replacing ) called strerror/exit without their prototypes, so strerror was assumed to return int and -Werror=format killed regs.c. Include //. - program_invocation_name is a glibc-only global; hardcode the "ltrace" prefix instead. - trace.c uses the kernel PT_R0/PT_NIP/PT_LNK ptrace offsets, which glibc's pulls in transitively but musl's does not. Add in the ppc ptrace.h shim. Confirmed building locally for ppc64 and ppc64el. --- src/pkgs/ltrace.nix | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pkgs/ltrace.nix b/src/pkgs/ltrace.nix index 21c10c3..4aecce1 100644 --- a/src/pkgs/ltrace.nix +++ b/src/pkgs/ltrace.nix @@ -4,10 +4,13 @@ let errorImpl = '' #define _GNU_SOURCE #include + #include + #include + #include #define error(status, errnum, ...) \ do { \ fflush(stdout); \ - fprintf(stderr, "%s: ", program_invocation_name); \ + fprintf(stderr, "ltrace: "); \ fprintf(stderr, __VA_ARGS__); \ if (errnum != 0) { \ fprintf(stderr, ": %s", strerror(errnum)); \ @@ -38,6 +41,13 @@ pkgs.ltrace.overrideAttrs (prev: { substituteInPlace sysdeps/linux-gnu/{mips/plt.c,ppc/regs.c} \ --replace '#include ' ${pkgs.lib.escapeShellArg errorImpl} + + # The ppc backend uses the kernel's PT_R0/PT_NIP/PT_LNK ptrace offsets. + # glibc's pulls these in transitively; musl's does not, so + # include the kernel that actually defines them. + substituteInPlace sysdeps/linux-gnu/ppc/ptrace.h \ + --replace '#include ' '#include +#include ' ''; configureFlags = [ "--datadir=/igloo" ]; From 78de23eaf0370d7421c64c674a4df5d12e8ee51d Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 15:18:16 -0400 Subject: [PATCH 7/8] Scrub build-time /nix/store paths from staged bundle The bundle's validate_tree step forbids any /nix/store reference, but several tools embed build-time store paths that survive ELF normalization: - CPython bakes its PREFIX (the build python3-minimal store path) into libpython's rodata via getpath; ltrace bakes its SYSCONFDIR into the binary. These are compile-time fallbacks, overridden at runtime, and point at paths that don't exist on the guest regardless. - _sysconfigdata*.py records the cross toolchain used to build the interpreter (build python, coreutils), only relevant for building extensions on-target, which we never do. Rewrite the literal "/nix/store" to "/igloo_nix" across the staged tree right before validation. The replacement is the same length, so the substitution is length-preserving and leaves ELF section offsets intact. Also fix CPython's hardcoded default shell (subprocess.py / ctypes fetch_macholib) to point at the guest's /bin/sh, and drop the unused EXTERNALLY-MANAGED pip marker. Confirmed: penguin-tools-armel now builds and passes validate_tree, with no /nix/store references and valid ELF binaries. --- src/mk-arch-bundle.nix | 10 ++++++++++ src/pkgs/python.nix | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/mk-arch-bundle.nix b/src/mk-arch-bundle.nix index 70b7567..56cd786 100644 --- a/src/mk-arch-bundle.nix +++ b/src/mk-arch-bundle.nix @@ -229,5 +229,15 @@ pkgs.runCommand "penguin-tools-${penguinArch}" ln -sfn "$arch" "$out/igloo_static/dylibs/${compatArch}" '') compatNames} + # Some tools bake their build-time /nix/store prefix into the binary's + # rodata (e.g. CPython's PREFIX in libpython, ltrace's SYSCONFDIR) or into + # leftover text config. These are compile-time fallbacks, overridden at + # runtime, and point at paths that do not exist on the guest anyway. Rewrite + # the store prefix everywhere so the tree carries no /nix/store references. + # "/igloo_nix" is exactly as long as "/nix/store", so the substitution is + # length-preserving and leaves ELF section offsets intact. + find "$out/igloo_static" -type f -print0 \ + | xargs -0r sed -i 's|/nix/store|/igloo_nix|g' + validate_tree '' diff --git a/src/pkgs/python.nix b/src/pkgs/python.nix index 4094e51..87eeefc 100644 --- a/src/pkgs/python.nix +++ b/src/pkgs/python.nix @@ -40,4 +40,15 @@ pkgs.runCommand "cpython-runtime-${version}" if grep -Irl -- ${python} "$out" >/dev/null 2>&1; then grep -IrlZ -- ${python} "$out" | xargs -0r sed -i "s|${python}|$out|g" fi + + # subprocess.py and ctypes' fetch_macholib hardcode the build sysroot's + # shell. On the guest the shell lives at /bin/sh. (Other residual + # build-time /nix/store references in this tree -- sysconfigdata, the + # libpython PREFIX baked into rodata, etc. -- are neutralized generically + # by the bundle's store-path scrub.) + grep -IrlZ -aP '/nix/store/[a-z0-9]{32}-[^/]*/bin/sh' "$out" | xargs -0r \ + sed -i -E 's,/nix/store/[a-z0-9]{32}-[^/]*/bin/sh,/bin/sh,g' + + # pip's EXTERNALLY-MANAGED marker is meaningless on the guest. + rm -f "$out/lib/python${version}/EXTERNALLY-MANAGED" '' From 014536dea1d11be0683993d73f7ed7b6b96d3715 Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 16:25:34 -0400 Subject: [PATCH 8/8] Skip openssl test suite on the native musl build openssl's checkPhase runs only on the "native" x86_64-musl build (the cross builds skip tests). Its 04-test_bio_dgram datagram-socket test fails in the sandboxed Nix builder, breaking the curl -> elfutils -> gdb dependency chain. openssl is only a transitive dependency here, so disable its checks, mirroring the existing p11-kit treatment. Verified the openssl derivation now carries doCheck="" and configures with disable-tests. --- src/cross-overlays.nix | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/cross-overlays.nix b/src/cross-overlays.nix index d6a1c84..fefbb9c 100644 --- a/src/cross-overlays.nix +++ b/src/cross-overlays.nix @@ -20,6 +20,16 @@ }); }) + # openssl's test suite runs only on the "native" x86_64-musl build (cross + # builds skip it). Its 04-test_bio_dgram datagram-socket test fails in the + # sandboxed builder. We only use openssl as a transitive dependency, so skip + # the checks. + (self: super: { + openssl = super.openssl.overrideAttrs (_: { + doCheck = false; + }); + }) + # GnuTLS docs builds run generated target binaries such as lt-errcodes. # --disable-doc skips the doc build, so neither the "man" nor "devdoc" # outputs get populated. Upstream couples these: it only passes