C1: strip store refs in shower FOD; autopatchelf wrapper

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) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-05-11 09:06:44 -07:00
commit f8598a6612

View file

@ -29,13 +29,13 @@ let
# dep into a single target dir. FODs get network access in exchange for # dep into a single target dir. FODs get network access in exchange for
# a pinned output hash, which means the whole dependency closure is # a pinned output hash, which means the whole dependency closure is
# immutable across rebuilds. # immutable across rebuilds.
pyDeps = pkgs.stdenv.mkDerivation { pyDepsFOD = pkgs.stdenv.mkDerivation {
pname = "shower-python-deps"; pname = "shower-python-deps-fod";
inherit version; inherit version;
dontUnpack = true; dontUnpack = true;
nativeBuildInputs = [ python pkgs.cacert ]; nativeBuildInputs = [ python pkgs.cacert pkgs.removeReferencesTo ];
buildPhase = '' buildPhase = ''
runHook preBuild runHook preBuild
@ -44,9 +44,6 @@ let
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export PIP_DISABLE_PIP_VERSION_CHECK=1 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" ${python}/bin/python -m venv "$TMPDIR/venv"
"$TMPDIR/venv/bin/pip" install --upgrade pip "$TMPDIR/venv/bin/pip" install --upgrade pip
"$TMPDIR/venv/bin/pip" install \ "$TMPDIR/venv/bin/pip" install \
@ -65,40 +62,84 @@ let
mkdir -p $out/lib/python3.14 $out/bin mkdir -p $out/lib/python3.14 $out/bin
cp -r "$TMPDIR/venv/lib/python3.14/site-packages" $out/lib/python3.14/site-packages 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 for script in "$TMPDIR/venv/bin/"*; do
[ -f "$script" ] || continue [ -f "$script" ] || continue
name=$(basename "$script") name=$(basename "$script")
case "$name" in case "$name" in
python*|pip*|activate*) continue ;; python*|pip*|activate*) continue ;;
esac esac
# Replace the venv python shebang with a path that resolves inside cp "$script" "$out/bin/$name"
# the docker image (where ${python} ends up in /nix/store).
sed -e "1 s|^#!.*python.*|#!${python}/bin/python3.14|" "$script" > "$out/bin/$name"
chmod +x "$out/bin/$name" chmod +x "$out/bin/$name"
done 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 # Strip bytecode entirely — pyc files embed compile-time paths.
# the image at first run, with paths matching the image filesystem.
postInstall = ''
find $out -type f -name '*.pyc' -delete find $out -type f -name '*.pyc' -delete
find $out -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true 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"; outputHashMode = "recursive";
outputHashAlgo = "sha256"; outputHashAlgo = "sha256";
# Computed by setting to pkgs.lib.fakeHash and reading the failure. # 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; outputHash = pkgs.lib.fakeHash;
dontFixup = true; 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"; sitePackages = "${pyDeps}/lib/python3.14/site-packages";
# Settings shim — config/settings.py's `BASE_DIR = parent.parent` would # Settings shim — config/settings.py's `BASE_DIR = parent.parent` would