Builds n3xd-ocp end to end and publishes 7.9.3.1.dev1 to the Gitea registry, where it installs anonymously and passes its suite. - occt/Dockerfile: OCCT 7.9.3 compiled once into a manylinux_2_28 builder image (base digest + tarball sha256 pinned), Draw/VTK/Tk/Xlib/OpenGL off, FreeType on, -O2 without fast-math or march=native. A final layer asserts TKService/TKV3d exist with no libGL/libX11 DT_NEEDED, which is what lets the app image drop libgl1/libx11-6. Mounted into, never built FROM. - scikit-build-core + nanobind STABLE_ABI -> one cp312-abi3 extension that registers every OCP.* submodule via PyImport_AddModule, so `import OCP.TopoDS` needs no shim and cls.__module__ is right. Version <occt>.N is asserted against the OCCT found, keeping occt_version() truthful. - occt_handle.h: type caster for opencascade::handle<T> over OCCT's intrusive refcount. Wrappers are non-owning instances holding exactly one handle in their keep-alive list, reusing an existing wrapper so identity survives a round trip. Transient constructors go through ocp_new (never nb::init<>, which would let OCCT delete nanobind's storage); the caster refuses a refcount-0 object rather than corrupt the heap. Verified under ASAN with no memory-safety errors, plus an RSS bound over 50k create/destroy cycles. - Sub-shapes are returned by value everywhere, making the TShape lifetime class that segfaulted a process-global face memo unrepresentable. - Standard_Failure derives RuntimeError, with ~20 concrete types dispatched on the dynamic OCCT type (cad_pool marshals failures home by type name). - Inc 0 surface: gp subset, TopAbs, TopoDS (+ downcasts), TopExp, TopLoc, TopTools, BRep, BinTools, Poly, Standard. 34 of the app's 139 symbols. - n3xd_ocp: additive APIs kept out of the OCP namespace so parity testing stays meaningful. bintools (shape <-> bytes, GIL-free, byte-identical) and _debug. Two findings worth the record, both verified against the stock wheel rather than assumed: upstream binds __hash__ but leaves __eq__ at identity, which is exactly what geom_memo.py's hash-bucket + IsSame scan is built around, so we match it instead of "fixing" it; and BinTools can release the GIL after all, by slurping the file-like object instead of bridging a streambuf that would call back into Python. Gate: BREP round-trips are byte-identical to cadquery-ocp-novtk across six fixtures (the generator asserts stock idempotency first). That matters beyond IPC — derive.py content-addresses BREP payloads by sha256 and stores the ref.
4.2 KiB
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.
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
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:
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
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:
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.
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.