C2(authentik-source-build): impl Python backend derivation (WIP)

Two-phase build: FOD (uv sync + strip store refs) and main derivation
(autoPatchelfHook + workspace packages + patches). uv sync completes
successfully; 6 residual store refs remain in FOD output to fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-03-01 09:14:39 -08:00
commit effe80c0a7
2 changed files with 276 additions and 0 deletions

View file

@ -0,0 +1,143 @@
# Authentik Python/Django backend
#
# Assembles the final package from:
# 1. python-deps FOD (venv with stripped store references)
# 2. opencontainers git dependency (fetched via Nix)
# 3. Workspace packages (ak-guardian, django-channels-postgres, etc.)
# 4. Authentik application source
# 5. Lifecycle scripts, blueprints, manage.py
#
# autoPatchelfHook restores RPATHs that were stripped in the FOD.
#
# Output:
# $out/bin/python3.14 venv python (symlink to nix python314)
# $out/lib/python3.14/site-packages/ all Python packages
# $out/lifecycle/ lifecycle scripts (symlink)
# $out/blueprints/ YAML blueprints
# $out/manage.py Django management script
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
let
python-deps = import ./python-deps.nix { inherit pkgs sources; };
# opencontainers is a git dependency not on PyPI — fetch separately
opencontainers-src = pkgs.fetchFromGitHub {
owner = "vsoch";
repo = "oci-python";
rev = "ceb4fcc090851717a3069d78e85ceb1e86c2740c";
hash = pkgs.lib.fakeHash;
};
sp = "$out/lib/python3.14/site-packages";
in
pkgs.stdenv.mkDerivation {
pname = "authentik-django";
version = sources.version;
inherit (sources) meta;
src = sources.src;
nativeBuildInputs = with pkgs; [
autoPatchelfHook # restores RPATHs stripped in the FOD
];
# Libraries that autoPatchelfHook resolves NEEDED entries against
buildInputs = with pkgs; [
python314
stdenv.cc.cc.lib # libstdc++, libgcc_s
libxml2
libxslt
xmlsec
openssl
libpq
krb5.lib
libtool.lib
libffi
zlib
];
dontBuild = true;
installPhase = ''
runHook preInstall
# --- Copy venv from FOD ---
cp -r ${python-deps} $out
chmod -R +w $out
# Restore python path in pyvenv.cfg (was replaced with @python@ in FOD)
sed -i "s|@python@|${pkgs.python314}|g" $out/pyvenv.cfg
# Recreate bin/ (was removed in FOD to strip python store refs)
mkdir -p $out/bin
ln -s ${pkgs.python314}/bin/python3.14 $out/bin/python3.14
ln -s python3.14 $out/bin/python3
ln -s python3.14 $out/bin/python
# Recreate entry point scripts that were in the venv's bin/
# (gunicorn, etc. — use python from this venv)
for ep in gunicorn uvicorn dramatiq dumb-init; do
if [ -e ${sp}/$ep ] || $out/bin/python3.14 -c "import $ep" 2>/dev/null; then
cat > $out/bin/$ep << SCRIPT
#!$out/bin/python3.14
import sys
from importlib.metadata import entry_points
eps = entry_points(group='console_scripts', name='$ep')
if eps:
sys.exit(eps[0].load()())
SCRIPT
chmod +x $out/bin/$ep
fi
done 2>/dev/null || true
# --- opencontainers (git dependency, pure Python) ---
cp -r ${opencontainers-src}/opencontainers ${sp}/opencontainers
# --- Workspace packages (pure Python — direct copy) ---
# ak-guardian: hatch config maps to "guardian" package
cp -r packages/ak-guardian/guardian ${sp}/guardian
cp -r packages/django-channels-postgres/django_channels_postgres ${sp}/
cp -r packages/django-dramatiq-postgres/django_dramatiq_postgres ${sp}/
cp -r packages/django-postgres-cache/django_postgres_cache ${sp}/
# --- Authentik application + lifecycle ---
cp -r authentik ${sp}/authentik
cp -r lifecycle ${sp}/lifecycle
chmod +x ${sp}/lifecycle/ak
# --- Patches for Nix store paths ---
# BASE_DIR: point to $out instead of computing from settings.py's location
substituteInPlace ${sp}/authentik/root/settings.py \
--replace-fail \
'BASE_DIR = Path(__file__).absolute().parent.parent.parent' \
"BASE_DIR = Path(\"$out\")"
# blueprints_dir: point to $out/blueprints
substituteInPlace ${sp}/authentik/lib/default.yml \
--replace-fail 'blueprints_dir: /blueprints' \
"blueprints_dir: $out/blueprints"
# Web asset paths: placeholder @webui@ for Go server card to resolve
substituteInPlace ${sp}/authentik/stages/email/utils.py \
--replace-fail 'Path("web/icons/icon_left_brand.png")' \
'Path("@webui@/icons/icon_left_brand.png")' \
--replace-fail 'Path("web/dist/assets/icons/icon_left_brand.png")' \
'Path("@webui@/dist/assets/icons/icon_left_brand.png")'
# Lifecycle bash script: use Nix store bash (no /usr/bin/env in containers)
substituteInPlace ${sp}/lifecycle/ak \
--replace-fail '#!/usr/bin/env -S bash' '#!${pkgs.bash}/bin/bash'
# --- Top-level structure ---
ln -s ${sp}/lifecycle $out/lifecycle
cp -r blueprints $out/blueprints
cp manage.py $out/manage.py
runHook postInstall
'';
# autoPatchelfHook runs in fixupPhase — don't disable it
dontPatchShebangs = true;
}

View file

@ -0,0 +1,133 @@
# Fixed-output derivation (FOD): download and install all external Python
# dependencies into a venv using uv sync.
#
# FODs get network access because the output hash is declared upfront.
# However, FODs must not reference other Nix store paths in their output.
# Compiled .so files (from sdist builds) contain RPATHs to system libraries
# (libxml2, krb5, etc.) which are Nix store paths. We strip these references
# here; authentik-django.nix restores them via autoPatchelfHook.
#
# The venv's bin/ and pyvenv.cfg also reference the python store path, so we
# replace them with placeholders that the main derivation restores.
#
# When uv.lock changes, reset outputHash to pkgs.lib.fakeHash, build to
# get the correct hash from the error message, then update.
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
let
# All store paths that may end up referenced in the venv output.
# remove-references-to will replace each hash with 'eeee...' bytes.
refTargets = with pkgs; [
python314
stdenv.cc.cc.lib
libxml2.out
libxml2.dev
libxslt.out
libxslt.dev
xmlsec.out
openssl.out
openssl.dev
libpq.out
libpq.dev
krb5.out
krb5.dev
krb5.lib
libtool.out
libtool.lib
libffi.out
libffi.dev
zlib.out
zlib.dev
readline.out
ncurses.out
glibc.out
];
removeRefsArgs = builtins.concatStringsSep " "
(map (t: "-t ${t}") refTargets);
in
pkgs.stdenv.mkDerivation {
pname = "authentik-python-deps";
version = sources.version;
src = sources.src;
nativeBuildInputs = with pkgs; [
python314
uv
git # opencontainers is a git dependency in uv.lock
cacert # HTTPS verification for PyPI + GitHub
pkg-config
removeReferencesTo
# Build tools on PATH for sdist compilation
postgresql.pg_config # pg_config for psycopg-c
krb5 # krb5-config for gssapi
];
# System libraries for packages that must build from sdist:
# lxml, xmlsec — pyproject.toml [tool.uv] no-binary-package
# psycopg-c — sdist only on PyPI
# gssapi — no Linux wheels on PyPI
buildInputs = with pkgs; [
libxml2
libxslt
xmlsec
openssl
libpq # psycopg-c links against libpq
libtool # libltdl for xmlsec dynamic crypto backend loading
libffi
zlib
];
buildPhase = ''
runHook preBuild
export HOME=$TMPDIR
export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export GIT_SSL_CAINFO=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
export UV_PYTHON=${pkgs.python314}/bin/python3.14
export UV_LINK_MODE=copy
# gssapi's pre-generated C code uses S4U functions declared in gssapi_ext.h
# but doesn't include it — force-include via compiler flag
export NIX_CFLAGS_COMPILE="''${NIX_CFLAGS_COMPILE:-} -include gssapi/gssapi_ext.h"
uv sync \
--frozen \
--no-install-project \
--no-install-workspace \
--no-dev
runHook postBuild
'';
installPhase = ''
runHook preInstall
mv .venv $out
# --- Strip Nix store references (FODs must be self-contained) ---
# autoPatchelfHook in authentik-django.nix restores correct RPATHs.
# Replace python store path in pyvenv.cfg with placeholder
sed -i "s|${pkgs.python314}|@python@|g" $out/pyvenv.cfg
# Remove bin/ entirely — main derivation recreates it
rm -rf $out/bin
# Strip store path references from shared objects
find $out -type f \( -name '*.so' -o -name '*.so.*' \) \
-exec remove-references-to ${removeRefsArgs} {} + 2>/dev/null || true
# Strip store refs from .pyc files (contain embedded paths)
find $out -type f -name '*.pyc' -delete
runHook postInstall
'';
outputHashMode = "recursive";
outputHashAlgo = "sha256";
outputHash = pkgs.lib.fakeHash;
dontFixup = true;
}