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