From f8598a66124dbbdb9e1c7836f44e395f4a42bc18 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 11 May 2026 09:06:44 -0700 Subject: [PATCH] C1: strip store refs in shower FOD; autopatchelf wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 534 failed with 'fixed-output derivations must not reference store paths: ... gcc-14.3.0-lib' because pip-installed wheels pulled stdenv into the venv (Python's setup, gcc-lib runtime references). Adapts authentik's two-stage pattern: - pyDepsFOD: pip-installs into the venv, then strips every nix store ref it can find (find+remove-references-to). Output is fully self-contained — pinned by outputHash. - pyDeps (non-FOD wrapper): copies the FOD output and runs autoPatchelfHook against runtime buildInputs (libstdc++, zlib, image libs for pillow). This restores RPATHs on the .so files that pillow and scipy ship, against the real on-image library locations. outputHash still fakeHash — next build prints the real one. Co-Authored-By: Claude Opus 4.7 (1M context) --- containers/shower/default.nix | 77 +++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/containers/shower/default.nix b/containers/shower/default.nix index cb64ca8..fa1f07f 100644 --- a/containers/shower/default.nix +++ b/containers/shower/default.nix @@ -29,13 +29,13 @@ let # dep into a single target dir. FODs get network access in exchange for # a pinned output hash, which means the whole dependency closure is # immutable across rebuilds. - pyDeps = pkgs.stdenv.mkDerivation { - pname = "shower-python-deps"; + pyDepsFOD = pkgs.stdenv.mkDerivation { + pname = "shower-python-deps-fod"; inherit version; dontUnpack = true; - nativeBuildInputs = [ python pkgs.cacert ]; + nativeBuildInputs = [ python pkgs.cacert pkgs.removeReferencesTo ]; buildPhase = '' runHook preBuild @@ -44,9 +44,6 @@ let export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt export PIP_DISABLE_PIP_VERSION_CHECK=1 - # Install into a venv first so pip's bytecode-compile + entry-point - # generation pick up the right interpreter, then copy site-packages - # + bin into $out at a stable layout. ${python}/bin/python -m venv "$TMPDIR/venv" "$TMPDIR/venv/bin/pip" install --upgrade pip "$TMPDIR/venv/bin/pip" install \ @@ -65,40 +62,84 @@ let mkdir -p $out/lib/python3.14 $out/bin cp -r "$TMPDIR/venv/lib/python3.14/site-packages" $out/lib/python3.14/site-packages - # Copy console scripts (gunicorn, django-admin, etc.) but drop the - # venv-specific shebang prefix that points at $TMPDIR/venv/bin/python. - # Rewrite shebangs to the eventual on-image python path. for script in "$TMPDIR/venv/bin/"*; do [ -f "$script" ] || continue name=$(basename "$script") case "$name" in python*|pip*|activate*) continue ;; esac - # Replace the venv python shebang with a path that resolves inside - # the docker image (where ${python} ends up in /nix/store). - sed -e "1 s|^#!.*python.*|#!${python}/bin/python3.14|" "$script" > "$out/bin/$name" + cp "$script" "$out/bin/$name" chmod +x "$out/bin/$name" done - runHook postInstall - ''; + # --- Strip Nix store references (FOD outputs must be self-contained) --- + # The wrapper derivation below restores them via autoPatchelfHook + a + # python wrapper that points pyc-less imports at the on-image python. - # Bytecode files embed absolute paths; deletion forces re-compile inside - # the image at first run, with paths matching the image filesystem. - postInstall = '' + # Strip bytecode entirely — pyc files embed compile-time paths. find $out -type f -name '*.pyc' -delete find $out -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true + + # Dynamically discover all nix store references and strip them. We + # don't have a static list because pip pulls in stdenv via Python's + # build env (gcc-lib, libstdc++, etc.) and the closure is opaque. + { find $out -type f -print0 \ + | xargs -0 grep -aohE '/nix/store/[a-z0-9]{32}-[^/"[:space:]]+' 2>/dev/null \ + || true; } | sort -u > $TMPDIR/store-refs.txt + echo "Found $(wc -l < $TMPDIR/store-refs.txt) unique store path references to strip" + + refs_args="" + while IFS= read -r ref; do + refs_args="$refs_args -t $ref" + done < $TMPDIR/store-refs.txt + + if [ -n "$refs_args" ]; then + find $out -type f -exec remove-references-to $refs_args {} + 2>/dev/null || true + fi + + remaining=$({ find $out -type f -print0 | xargs -0 grep -cl '/nix/store/' 2>/dev/null || true; } | wc -l) + echo "Files with remaining store references: $remaining" + + runHook postInstall ''; outputHashMode = "recursive"; outputHashAlgo = "sha256"; # Computed by setting to pkgs.lib.fakeHash and reading the failure. - # Pin the dep closure — rebuilds are reproducible until the version bumps. outputHash = pkgs.lib.fakeHash; dontFixup = true; }; + # Non-FOD wrapper: re-applies RPATHs to pre-built .so files (pillow, + # scipy) so they find libstdc++ / libz / etc. at runtime. autoPatchelfHook + # discovers needed libraries from buildInputs. + pyDeps = pkgs.stdenv.mkDerivation { + pname = "shower-python-deps"; + inherit version; + + dontUnpack = true; + + nativeBuildInputs = [ pkgs.autoPatchelfHook ]; + + buildInputs = with pkgs; [ + python + stdenv.cc.cc.lib # libstdc++, libgcc_s + zlib + libjpeg + libwebp + libtiff + openjpeg + lcms2 + freetype + ]; + + installPhase = '' + cp -r ${pyDepsFOD} $out + chmod -R u+w $out + ''; + }; + sitePackages = "${pyDeps}/lib/python3.14/site-packages"; # Settings shim — config/settings.py's `BASE_DIR = parent.parent` would