diff --git a/cross-js.nix b/cross-js.nix index 79e90bf5..31cd4914 100644 --- a/cross-js.nix +++ b/cross-js.nix @@ -12,9 +12,12 @@ let tool-version-map = (import ./tool-map.nix) self; # add a trace helper. This will trace a message about disabling a component despite requesting it, if it's not supported in that compiler. compiler-not-in = compiler-list: name: (if __elem compiler-nix-name compiler-list then __trace "No ${name}. Not yet compatible with ${compiler-nix-name}" false else true); + writers = import ./writers.nix { inherit pkgs; }; + # * wrapped tools: - # A cabal-install wrapper that sets the appropriate static flags - wrapped-cabal = pkgs.writeShellApplication { + # A cabal-install wrapper that sets the appropriate static flags. + # See writers.nix for why writeShellApplicationWithRuntime is needed. + wrapped-cabal = writers.writeShellApplicationWithRuntime { name = "cabal"; runtimeInputs = [ cabal-install pkgs.curl ]; text = with pkgs; '' @@ -86,7 +89,6 @@ pkgs.mkShell ({ buildInputs = []; nativeBuildInputs = [ wrapped-hsc2hs wrapped-cabal compiler ] ++ (with pkgs; [ - curl nodejs # helpful to evaluate output on the commandline. cabal-install (pkgs.pkg-config or pkgconfig) diff --git a/cross-windows.nix b/cross-windows.nix index 64a243e6..5220525b 100644 --- a/cross-windows.nix +++ b/cross-windows.nix @@ -72,9 +72,14 @@ let tool-version-map = (import ./tool-map.nix) self; kill $RISERV_PID ''; + # Use pkgsBuildBuild for the writers helper — cross-windows wrappers must be + # build-platform executables, not target-platform (which is Windows). + writers = import ./writers.nix { pkgs = pkgs.pkgsBuildBuild; }; + # * wrapped tools: - # A cabal-install wrapper that sets the appropriate static flags - wrapped-cabal = pkgs.pkgsBuildBuild.writeShellApplication { + # A cabal-install wrapper that sets the appropriate static flags. + # See writers.nix for why writeShellApplicationWithRuntime is needed. + wrapped-cabal = writers.writeShellApplicationWithRuntime { name = "cabal"; runtimeInputs = [ cabal-install curl ]; text = '' diff --git a/dynamic.nix b/dynamic.nix index 568f53a4..10132865 100644 --- a/dynamic.nix +++ b/dynamic.nix @@ -13,6 +13,8 @@ let tool-version-map = (import ./tool-map.nix) self; # add a trace helper. This will trace a message about disabling a component despite requesting it, if it's not supported in that compiler. compiler-not-in = compiler-list: name: (if __elem compiler-nix-name compiler-list then __trace "No ${name}. Not yet compatible with ${compiler-nix-name}" false else true); + writers = import ./writers.nix { inherit pkgs; }; + # * wrapped tools: # fixup-nix-deps allows us to drop dylibs from macOS executables that can be # linked directly. @@ -40,7 +42,9 @@ let tool-version-map = (import ./tool-map.nix) self; # have in the static configuration, and we may imagine needing to inject # some flags into cabal (temporarily), hence we'll keep this functionality # here. - wrapped-cabal = pkgs.writeShellApplication { + # + # See writers.nix for why writeShellApplicationWithRuntime is needed. + wrapped-cabal = writers.writeShellApplicationWithRuntime { name = "cabal"; runtimeInputs = [ cabal-install pkgs.curl ]; text = '' @@ -109,7 +113,6 @@ pkgs.mkShell { stdenv.cc.cc.lib ] ++ (with pkgs; [ - curl openssl pcre pkg-config diff --git a/static.nix b/static.nix index bc5407be..e642f318 100644 --- a/static.nix +++ b/static.nix @@ -10,6 +10,8 @@ let tool-version-map = (import ./tool-map.nix) self; # add a trace helper. This will trace a message about disabling a component despite requesting it, if it's not supported in that compiler. compiler-not-in = compiler-list: name: (if __elem compiler-nix-name compiler-list then __trace "No ${name}. Not yet compatible with ${compiler-nix-name}" false else true); + writers = import ./writers.nix { inherit pkgs; }; + # * wrapped tools: # fixup-nix-deps allows us to drop dylibs from macOS executables that can be # linked directly. @@ -29,8 +31,9 @@ let tool-version-map = (import ./tool-map.nix) self; done ''; }; - # A cabal-install wrapper that sets the appropriate static flags - wrapped-cabal = pkgs.writeShellApplication { + # A cabal-install wrapper that sets the appropriate static flags. + # See writers.nix for why writeShellApplicationWithRuntime is needed. + wrapped-cabal = writers.writeShellApplicationWithRuntime { name = "cabal"; runtimeInputs = [ cabal-install pkgs.curl ]; text = with pkgs; '' @@ -148,7 +151,6 @@ pkgs.mkShell (rec { # products to be static. (compiler.override { enableShared = true; }) ] ++ (with pkgs; [ - curl (pkgs.pkg-config or pkgconfig) stdenv.cc.cc.lib ]) ++ (with pkgs.buildPackages; [ ]) diff --git a/writers.nix b/writers.nix new file mode 100644 index 00000000..6f9a8304 --- /dev/null +++ b/writers.nix @@ -0,0 +1,71 @@ +# writeShellApplicationWithRuntime — a drop-in replacement for writeShellApplication +# that also propagates runtimeInputs to dependent environments. +# +# ─── The problem ─────────────────────────────────────────────────────────────── +# +# nixpkgs' writeShellApplication accepts a `runtimeInputs` parameter and +# compiles it into the wrapper script as: +# +# export PATH="${lib.makeBinPath runtimeInputs}:$PATH" +# +# This means the runtime inputs are only on PATH **inside the wrapper script +# itself**. They are NOT exposed as derivation attributes — not in +# propagatedNativeBuildInputs, not in passthru, nowhere. +# +# When a wrapper built this way is placed into a mkShell's buildInputs or +# nativeBuildInputs, `nix develop` reconstructs the environment by evaluating +# the shell derivation and walking all inputs. The Nix C++ evaluator (used by +# `nix develop` / `nix print-dev-env`) follows the full derivation closure, so +# it discovers everything transitively — including the runtimeInputs baked into +# wrapper scripts. +# +# However, devx generates `-env` container scripts at **evaluation time** using +# devShellTools. These scripts export raw derivation attributes and then +# `source "$stdenv/setup"` to reconstruct the environment. stdenv's setup.sh +# walks dependencies via the `findInputs` function (stdenv/setup lines 715-806), +# which reads `$pkg/nix-support/propagated-*` metadata files to discover +# transitive inputs. It does NOT look inside wrapper scripts. +# +# The result: any program in the -env container that is NOT the wrapper itself +# (e.g. GHC's stage0 bootstrap cabal, or any script that shells out to curl) +# cannot find the wrapper's runtimeInputs on PATH. +# +# ─── The fix ─────────────────────────────────────────────────────────────────── +# +# We use overrideAttrs to add propagatedNativeBuildInputs to the wrapper +# derivation. When stdenv's setup.sh processes the wrapper from +# nativeBuildInputs, it: +# +# 1. Adds $wrapper/bin to PATH (the wrapper itself) +# 2. Reads $wrapper/nix-support/propagated-native-build-inputs +# 3. Recursively calls findInputs on each propagated input +# 4. Adds $curl/bin, $cabal-install/bin, etc. to PATH +# +# This makes runtimeInputs visible to the ENTIRE shell environment, not just +# inside the wrapper. The wrapper still has its own inline PATH injection +# (via writeShellApplication), so it works in both contexts. +# +# ─── Why not upstream? ───────────────────────────────────────────────────────── +# +# Ideally, nixpkgs' writeShellApplication would set propagatedNativeBuildInputs +# from runtimeInputs by default. Until that happens, this wrapper bridges +# the gap. If upstream adopts this, this file becomes a no-op passthrough +# and can be removed. +# +# ─── Usage ───────────────────────────────────────────────────────────────────── +# +# let writers = import ./writers.nix { inherit pkgs; }; +# in writers.writeShellApplicationWithRuntime { +# name = "cabal"; +# runtimeInputs = [ cabal-install pkgs.curl ]; +# text = ''...''; +# } +# +{ pkgs }: +{ + writeShellApplicationWithRuntime = args@{ runtimeInputs ? [], ... }: + (pkgs.writeShellApplication args).overrideAttrs (old: { + propagatedNativeBuildInputs = + (old.propagatedNativeBuildInputs or []) ++ runtimeInputs; + }); +}