# Building Everything runs through `make`; `make help` lists the targets. All compilation happens inside the OCCT builder image, so the only host requirements are Docker and (for publishing) `uv`. ## The builder image `occt/Dockerfile` compiles OCCT 7.9.3 once inside `quay.io/pypa/manylinux_2_28_x86_64` (both the base digest and the source tarball's sha256 are pinned) and installs it to `/opt/occt`. ```bash make image # ~40 min on 16 cores make image-push # needs: docker login git.stroblme.de ``` It is a **compiler appliance**: wheel builds mount the repo into it rather than `FROM` it, so iterating on the binding never re-layers the kernel. Add new packages at the *end* of the Dockerfile — earlier layers stay cached and the kernel is not recompiled. The configuration turns Draw, VTK, Tk, Xlib, OpenGL and GLES off and FreeType on, and the last layer asserts the result: TKService and TKV3d exist (text emboss reaches `Font_BRepFont` through them), neither carries a `libGL`/`libX11` `DT_NEEDED`, and freetype is linked. That is what lets the app image eventually drop `libgl1` and `libx11-6`. Flags are `-O2`, no `-ffast-math`, no `-march=native`: OCCT's version is a determinism input for assay's goldens, so the binding must not introduce a different FP contract than the kernel it wraps. **Production never compiles OCCT.** The host has 4 cores and `make update` is a `git pull` plus a compose build; the kernel arrives prebuilt inside the wheel, which is the whole reason this image exists. ## Build cache Object files, ccache, the pip cache and the build venv live under `$(CACHE)`, default `/mnt/cache/n3xd/ocp` — off the root filesystem, which is tight on the dev box. Change it per invocation with `make CACHE=/somewhere wheel`, or reset it with `make clean-cache`. ## The loop ```bash make dev # incremental compile + pytest — the inner loop, seconds make test-asan # handle-model memory-safety check make wheel # full build: compile, stubs, repack, auditwheel, smoke test ``` `make wheel` compiles twice on purpose: stubs are produced by importing the freshly built extension, so they cannot exist before the first compile, and the wheel is packed from the source tree. The second pass is incremental. The smoke test installs the repaired wheel into a bare venv and imports it with `LD_LIBRARY_PATH` unset — the only honest proof that `auditwheel` made it self-contained. ## Fixtures `tests/data/*.brep` are the byte-identity references and are generated under the **stock** wheel, from the app checkout: ```bash cd ../app && .venv/bin/python ../ocp/tools/gen_fixtures.py ``` The generator asserts stock is idempotent for each fixture before recording its digest — otherwise the gate would compare against a moving target. Regenerate only when deliberately re-blessing (e.g. the OCCT 8.0 bump). ## Publishing ```bash make version # confirm what you are about to publish make publish # uv publish -> https://git.stroblme.de/api/packages/N3XD/pypi ``` Credentials come from `.secrets` (gitignored) as `UV_PUBLISH_USERNAME` / `UV_PUBLISH_PASSWORD`; the username is a real Gitea username, not the PyPI `__token__` convention, and the token needs `package: Read and Write`. A version can never be republished. Iteration builds therefore carry a `.devN` suffix and are the only ones the registry's cleanup rule collects; bump `N` in `pyproject.toml` for each upload. Consumers read anonymously — the N3XD org is public: ```bash uv pip install --index-url https://git.stroblme.de/api/packages/N3XD/pypi/simple/ \ --prerelease=allow n3xd-ocp ``` Gitea serves no root `/simple/` listing (404), only the per-package path, which is all pip and uv ask for. ## Parity `tools/parity_venv.sh` builds a side environment where the app runs against this wheel instead of the stock one. The app's manifests are never edited: both distributions own the `OCP/` import path and a process can hold only one OCCT build, so a swap is per-environment and reversible by re-syncing. ```bash tools/parity_venv.sh # from the registry tools/parity_venv.sh --local # from wheelhouse/ python tools/inventory.py --check ``` Coverage is expected to be partial until the increments land — `--check` prints what is still missing, grouped by module, which is the work queue.