#!/usr/bin/env bash # Copyright (c) 2026 The Anuvani-Diarization authors # SPDX-License-Identifier: MIT OR Apache-2.0 # Build the self-contained wheel: CPython >= 3.10 (abi3), manylinux_2_28 x86_64, inside a pinned container. # # tools/build_wheel.sh -> artifacts/wheels/anuvani_diar--cp310-abi3-manylinux_2_28_x86_64.whl # artifacts/wheels/.build.json (what was built, from what, and the checks) # # Pinned: the build image by digest, rustup-init 1.29.1 by sha256, rustc 1.94.1 (minimal profile), maturin 1.15.0, # the crates by Cargo.lock (--locked), file times by SOURCE_DATE_EPOCH (the commit's time). # # 1. tools/stage_wheel_files.py stages the bundled libraries, their licence texts and the assets (they come from the # reference environment) and verifies them. # 2. The build context is the files git tracks for the build (working-tree content) plus the staged files, copied # to artifacts/wheel_build/anuvani_diar_rs, so nothing else in this tree (target/, .venv/, a stray _core.so) can # enter the wheel. The build starts from an empty target directory. # 3. In the container: maturin build --release --locked --strip --compatibility manylinux_2_28, with every source # path remapped (no host or build path in _core), and --auditwheel check (verifies the tag, changes nothing). # 4. Checks, also in the container: `auditwheel show` (never `repair`: it would rewrite the bundled libraries and # break their sha256); the bundled files' sha256 in the wheel; `strings` on _core for host or build # paths; the glibc symbol versions _core needs (<= 2.28) and its NEEDED libraries. # # The image is pulled once (docker pull); rustup, the toolchain, the crates and maturin are cached in # artifacts/wheel_build/cache (git-ignored) after the first build. set -euo pipefail PROJ=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) IMAGE="quay.io/pypa/manylinux_2_28_x86_64@sha256:39df0042d5cc900b085aa25a0659368b42a0006c54c474299b785b44c1b4ff82" RUSTUP_VERSION=1.29.1 RUSTUP_SHA256=dda7234360b7f578ca8b0ddcb80145646fa61a67c1720a5abc7051b35c9fcb71 RUST_VERSION=1.94.1 MATURIN_VERSION=1.15.0 WORK="$PROJ/artifacts/wheel_build" OUT="$PROJ/artifacts/wheels" cd "$PROJ" python3 tools/stage_wheel_files.py "$@" COMMIT=$(git rev-parse HEAD) DIRTY=$(git status --porcelain -- . | grep -q . && echo true || echo false) SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) rm -rf "$WORK/anuvani_diar_rs" "$WORK/target" "$WORK/dist" mkdir -p "$WORK/anuvani_diar_rs" "$WORK/cache" "$OUT" git ls-files -z -- Cargo.toml Cargo.lock pyproject.toml crates python tools/wheel_files.json assets/MANIFEST.json \ LICENSE-MIT LICENSE-APACHE THIRD_PARTY_NOTICES.md LICENSES \ | tar --null -T - -cf - | tar -xf - -C "$WORK/anuvani_diar_rs" cp -a python/anuvani_diar/_libs python/anuvani_diar/assets "$WORK/anuvani_diar_rs/python/anuvani_diar/" START=$(date +%s) docker run --rm -i --user "$(id -u):$(id -g)" -v "$WORK:/build" -v "$WORK/cache:/cache" -v "$OUT:/out" \ -e IMAGE="$IMAGE" -e RUSTUP_VERSION="$RUSTUP_VERSION" -e RUSTUP_SHA256="$RUSTUP_SHA256" -e RUST_VERSION="$RUST_VERSION" \ -e MATURIN_VERSION="$MATURIN_VERSION" -e SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" -e COMMIT="$COMMIT" -e DIRTY="$DIRTY" \ "$IMAGE" bash -s <<'IN_CONTAINER' set -euo pipefail export HOME=/tmp/home RUSTUP_HOME=/cache/rustup CARGO_HOME=/cache/cargo export PATH=/cache/cargo/bin:$PATH mkdir -p "$HOME" if [[ ! -x /cache/cargo/bin/rustup ]]; then curl -sSf -o /tmp/rustup-init "https://static.rust-lang.org/rustup/archive/$RUSTUP_VERSION/x86_64-unknown-linux-gnu/rustup-init" echo "$RUSTUP_SHA256 /tmp/rustup-init" | sha256sum -c --quiet - chmod +x /tmp/rustup-init /tmp/rustup-init -y -q --no-modify-path --profile minimal --default-toolchain none fi rustup toolchain install "$RUST_VERSION" --profile minimal >/dev/null 2>&1 || rustup toolchain install "$RUST_VERSION" --profile minimal export RUSTUP_TOOLCHAIN="$RUST_VERSION" M=/cache/maturin-$MATURIN_VERSION if [[ ! -x $M/bin/maturin ]]; then /opt/python/cp312-cp312/bin/python -m venv "$M" "$M/bin/pip" install -q "maturin==$MATURIN_VERSION" fi cd /build/anuvani_diar_rs # no path of the build machine in the binary: sources, the crate registry, the toolchain export RUSTFLAGS="--remap-path-prefix=/build/anuvani_diar_rs=/anuvani_diar_rs --remap-path-prefix=/cache/cargo=/cargo --remap-path-prefix=/cache/rustup=/rustup" export CARGO_TARGET_DIR=/build/target if ! "$M/bin/maturin" build --release --locked --strip --compatibility manylinux_2_28 --auditwheel check \ -i /opt/python/cp310-cp310/bin/python3.10 --out /build/dist > /build/maturin.log 2>&1; then tail -40 /build/maturin.log exit 1 fi grep -v "^\s*\(Compiling\|Downloaded\|Downloading\) " /build/maturin.log | tail -8 WHEEL=$(ls /build/dist/*.whl) cp "$WHEEL" /out/ W=/out/$(basename "$WHEEL") # ---- checks auditwheel show "$W" > /build/auditwheel_show.txt 2>&1 || true /opt/python/cp312-cp312/bin/python - "$W" <<'PY' import hashlib, json, os, re, subprocess, sys, zipfile from pathlib import Path wheel = Path(sys.argv[1]) z = zipfile.ZipFile(wheel) names = z.namelist() spec = json.loads(Path("tools/wheel_files.json").read_text()) arrays = json.loads(Path("assets/MANIFEST.json").read_text())["arrays"] bad = [] for name, e in spec["libs"].items(): member = f"anuvani_diar/_libs/{name}" if member not in names: bad.append(f"missing {member}") elif e["sha256"] and hashlib.sha256(z.read(member)).hexdigest() != e["sha256"]: bad.append(f"{member}: sha256 differs from tools/wheel_files.json") for name in spec["assets"]: member = f"anuvani_diar/assets/{name}" if member not in names: bad.append(f"missing {member}") elif name.endswith(".bin") and hashlib.sha256(z.read(member)).hexdigest() != arrays[name[:-4]]["sha256"]: bad.append(f"{member}: sha256 differs from assets/MANIFEST.json") core = "anuvani_diar/_core.abi3.so" Path("/build/_core.abi3.so").write_bytes(z.read(core)) strings = subprocess.run(["strings", "-a", "/build/_core.abi3.so"], capture_output=True, text=True, check=True).stdout # a host or build path: one of these prefixes where a path starts (std's own "/rustc//library/std/ # src/io/..." is not a hit), or a tool directory anywhere leaks = sorted({m.group(0) for m in re.finditer( r"((? (2, 28): bad.append(f"_core.abi3.so needs GLIBC_{glibc[-1]} (> 2.28)") needed = re.findall(r"\(NEEDED\).*\[(.*)\]", subprocess.run(["readelf", "-d", "/build/_core.abi3.so"], capture_output=True, text=True, check=True).stdout) rustc = subprocess.run(["rustc", "-vV"], capture_output=True, text=True, check=True).stdout maturin = subprocess.run([f"/cache/maturin-{os.environ['MATURIN_VERSION']}/bin/maturin", "--version"], capture_output=True, text=True, check=True).stdout.strip() record = dict( wheel=wheel.name, sha256=hashlib.sha256(wheel.read_bytes()).hexdigest(), bytes=wheel.stat().st_size, commit=os.environ["COMMIT"], dirty=os.environ["DIRTY"] == "true", source_date_epoch=int(os.environ["SOURCE_DATE_EPOCH"]), image=os.environ["IMAGE"], rustc=rustc.splitlines()[0], rustc_host=re.search(r"host: (.*)", rustc).group(1), maturin=maturin, core_bytes=len(z.read(core)), core_glibc_max=glibc[-1] if glibc else None, core_needed=needed, files=len(names), path_strings_in_core=leaks, path_prefixes_in_core=dict(sorted(prefixes.items())), auditwheel_show=Path("/build/auditwheel_show.txt").read_text(), problems=bad) Path(f"/out/{wheel.name}.build.json").write_text(json.dumps(record, indent=1) + "\n") print(json.dumps({k: v for k, v in record.items() if k != "auditwheel_show"}, indent=1)) print(record["auditwheel_show"]) sys.exit(1 if bad else 0) PY IN_CONTAINER # the container must have written this run's record (a container that ran nothing would also exit 0) RECORD=$(ls -t "$OUT"/*.build.json 2>/dev/null | head -1) if [[ -z $RECORD || $(stat -c %Y "$RECORD") -lt $START ]]; then echo "build_wheel.sh: no wheel was built" >&2 exit 1 fi echo "built ${RECORD%.build.json} (record: $RECORD)"