From 89ffaea1bede78823c4bbc12262984addc5eebee Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Sat, 17 Oct 2026 02:17:35 +0900 Subject: [PATCH] Allow musl cross interpreters to load C dependencies Keep static Haskell libraries and fully static application outputs while planning ghc914-sh iserv projects with a dynamic musl C loader. Scope the policy to Linux-built musl Cabal v2 interpreters and check that scope in static Linux CI. Normal and profiled target plans preserve all unit IDs, source records and Haskell ways; only the iserv executable-static flag changes. The shared V2 project skeleton changes auxiliary derivation hashes. A small i686 musl probe reproduces static dlopen failure and verifies the dynamic loader under QEMU; full interpreter TH validation remains for canonical CI. --- .github/workflows/nix.yml | 5 ++++ flake.nix | 3 +- scripts/nix/README.md | 9 ++++++ scripts/nix/check-musl-iserv-scope.nix | 38 ++++++++++++++++++++++++++ scripts/nix/musl-iserv-overlay.nix | 27 ++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 scripts/nix/check-musl-iserv-scope.nix create mode 100644 scripts/nix/musl-iserv-overlay.nix diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index fe42647e85..e039d2fafa 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -93,6 +93,11 @@ jobs: timeout-minutes: 1 run: nix eval --json --file scripts/nix/check-android-iserv-scope.nix + - name: Check musl interpreter link policy scope + if: ${{ matrix.target == 'linux-static' }} + timeout-minutes: 1 + run: nix eval --json --file scripts/nix/check-musl-iserv-scope.nix + - name: Check native Linux GHC ELF section alignment if: ${{ matrix.system == 'x86_64-linux' && matrix.target == 'native' }} timeout-minutes: 120 diff --git a/flake.nix b/flake.nix index 7f227806b2..86470e1959 100644 --- a/flake.nix +++ b/flake.nix @@ -39,7 +39,8 @@ let nativePackages = import ./scripts/nix/native-packages.nix { inherit haskellNix; }; in let compilerCache = import ./scripts/nix/compiler-cache-overlay.nix { inherit haskellNix; }; in let androidEmulator = import ./scripts/nix/android-emulator-overlay.nix { inherit haskellNix; }; in - let pkgs = (nativePackages system).appendOverlays [android26 compilerCache androidEmulator]; in + let muslIserv = import ./scripts/nix/musl-iserv-overlay.nix; in + let pkgs = (nativePackages system).appendOverlays [android26 compilerCache androidEmulator muslIserv]; in let compilerSelection = import ./scripts/nix/compiler-selection.nix { inherit haskellNix; }; in let mobileRuntime = import ./scripts/nix/mobile-runtime.nix { inherit pkgs compilerSelection; }; in let drv' = { extra-modules, pkgs', package-flags ? {}, package-ghc-options ? {}, mobile ? false, ... }: diff --git a/scripts/nix/README.md b/scripts/nix/README.md index f5e18880b2..5929642932 100644 --- a/scripts/nix/README.md +++ b/scripts/nix/README.md @@ -33,6 +33,15 @@ Musl projects declare OpenSSL in Cabal's `extra-libraries-static` field for `direct-sqlcipher` and `simplexmq`. GHC's fully static link path reads that field, so an ordinary `extra-libraries` entry alone does not supply `libcrypto`. These source metadata patches run before planning and require Cabal specification 3.8. + +Linux-built musl `ghc914-sh` iserv projects use a package-specific +`executable-static: False` policy before Cabal v2 planning. The build-time +interpreter needs musl's dynamic C loader for Template Haskell dependencies; +a fully static musl executable has a `dlopen` stub. Its Haskell library ways +and profiling selection stay unchanged, and the final CLI remains fully static. +The pure scope check runs in the Linux static CI group: +`nix eval --json --file scripts/nix/check-musl-iserv-scope.nix`. + Windows patches Basement's `INVALID_HANDLE_VALUE` expression to `INT_PTR`, so hsc2hs receives the signed pointer-sized integer expected by the Haskell API. diff --git a/scripts/nix/check-musl-iserv-scope.nix b/scripts/nix/check-musl-iserv-scope.nix new file mode 100644 index 0000000000..3c98cafe88 --- /dev/null +++ b/scripts/nix/check-musl-iserv-scope.nix @@ -0,0 +1,38 @@ +# Pure policy regression: nix eval --json --file scripts/nix/check-musl-iserv-scope.nix +let + overlay = import ./musl-iserv-overlay.nix; + lib = { + optionalAttrs = condition: attrs: if condition then attrs else {}; + mkIf = condition: attrs: if condition then attrs else {}; + mkAfter = value: value; + }; + apply = { musl ? true, linux ? true, compiler ? "ghc914-sh", name ? "iserv-proxy", builderVersion ? 2 }: + let + prev = { + inherit lib; + stdenv = { hostPlatform.isMusl = musl; buildPlatform.isLinux = linux; }; + haskell-nix.cabalProject' = modules: modules; + }; + result = overlay prev prev; + in if result ? haskell-nix then + (builtins.head (result.haskell-nix.cabalProject' [])) { + inherit lib; + pkgs = prev; + config = { inherit name builderVersion; compiler-nix-name = compiler; }; + } + else {}; + selected = apply {}; +in +assert builtins.attrNames selected == [ "cabalProjectLocal" ]; +assert builtins.match ".*executable-static: False.*" (builtins.replaceStrings [ "\n" ] [ " " ] selected.cabalProjectLocal) != null; +assert apply { name = "simplex-chat"; } == {}; +assert apply { musl = false; } == {}; +assert apply { linux = false; } == {}; +assert apply { compiler = "ghc963"; } == {}; +assert apply { builderVersion = 1; } == {}; +{ + muslInterpreterCLoadingEnabled = true; + applicationPolicyUnchanged = true; + nonMuslAndDarwinBuildersUnchanged = true; + otherCompilersAndLegacyBuilderUnchanged = true; +} diff --git a/scripts/nix/musl-iserv-overlay.nix b/scripts/nix/musl-iserv-overlay.nix new file mode 100644 index 0000000000..a6a4eb3845 --- /dev/null +++ b/scripts/nix/musl-iserv-overlay.nix @@ -0,0 +1,27 @@ +final: prev: +prev.lib.optionalAttrs ( + prev.stdenv.hostPlatform.isMusl && prev.stdenv.buildPlatform.isLinux +) { + haskell-nix = prev.haskell-nix // { + cabalProject' = projectModule: prev.haskell-nix.cabalProject' ( + [ ({ config, lib, pkgs, ... }: + lib.mkIf ( + pkgs.stdenv.hostPlatform.isMusl + && pkgs.stdenv.buildPlatform.isLinux + && config.compiler-nix-name == "ghc914-sh" + && config.name == "iserv-proxy" + && config.builderVersion == 2 + ) { + # The build-time interpreter loads foreign libraries for TH. + # Fully static musl uses a dlopen stub; retain static Haskell + # libraries while allowing its C loader. Final CLI projects keep + # haskell.nix's default executable-static setting. + cabalProjectLocal = lib.mkAfter '' + package iserv-proxy + executable-static: False + ''; + }) ] + ++ (if builtins.isList projectModule then projectModule else [ projectModule ]) + ); + }; +}