diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e6b4d2..172db9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,10 +70,13 @@ target_link_libraries(_OCP PRIVATE TKDESTEP # STEPControl TKDEIGES # IGESControl, IGESData TKDESTL # RWStl - # Inc 4 — TKService carries the BRep font/text builders. The builder image - # asserts it links without libGL/libX11, so this costs the runtime image - # nothing. + # Inc 4 — the BRep font/text builders. OCCT 8.0 moved the StdPrs package + # from TKService to TKV3d (Font_BRepFont is now the typedef of + # StdPrs_BRepFont, not the other way round), so both are needed. The builder + # image asserts each links without libGL/libX11, so this costs the runtime + # image nothing. TKService + TKV3d ) target_compile_definitions(_OCP PRIVATE diff --git a/Makefile b/Makefile index 93410e1..cefd186 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -OCCT_VER ?= 7.9.3 +OCCT_VER ?= 8.0.1 IMG_N ?= 1 IMAGE ?= git.stroblme.de/n3xd/occt-build:$(OCCT_VER)-$(IMG_N) # Build cache lives off the root filesystem, which is tight on this host. diff --git a/README.md b/README.md index bc79f82..2c271ea 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,16 @@ backend uses — 138 symbols across 47 `OCP.*` modules, not all of OCCT. The package installs as a top-level `OCP`, so it is a drop-in replacement for `cadquery-ocp-novtk` and the app's 442 import sites stay untouched. -Status: **coverage complete** — all 138 symbols the app imports, across 53 -bound modules, published as `7.9.3.1.dev5`. The app's full backend suite passes -against it (1797 passed / 1 skipped, the same as the stock wheel), a sweep of -the whole project store reproduces every part's geometry exactly (4486 parts; -no change in statuses, volume, area, bbox, entity counts, triangles or anchor -digests), and BREP serialisation stays byte-identical, which the pools and the -content-addressed derive payloads depend on. The app is not swapped yet — that -is the cutover, roadmap 10C. +Status: **in production use** — all 138 symbols the app imports, across 53 +bound modules, published as `8.0.1.1` (OCCT 8.0.1). The app cut over on +2026-08-10 at `7.9.3.1` and took the kernel bump on 2026-08-11; its full +backend suite passes against both (1798 passed / 1 skipped, the same as the +stock wheel it replaced), a sweep of the whole project store reproduces every +part's geometry exactly (4486 parts; no change in statuses, volume, area, +bbox, entity counts, triangles or anchor digests), and BREP serialisation +stays byte-identical, which the pools and the content-addressed derive +payloads depend on — across the kernel bump as well, so nothing stored had to +be rewritten. Start with [docs/design.md](docs/design.md) for the decisions, [docs/building.md](docs/building.md) to build one, and @@ -24,7 +26,7 @@ phase plan lives in the app repo at `docs-private/reference/roadmap.md` ## Why -`cadquery-ocp` lags OCCT (it wraps 7.9.3; OCCT 8.0 shipped 2026-05), builds +`cadquery-ocp` lags OCCT (it still wraps 7.9.3; we are on 8.0.1), builds Windows and macOS wheels we never use, and until recently forced a 638 MB VTK dependency into the image. So this exists for version velocity, footprint, and two defects that a binding we control prevents by construction: @@ -54,7 +56,7 @@ production host (4 cores, and a kernel build is multi-hour). Wheels are built here on a dev box and published to the Gitea package registry. ```bash -make image # once, ~40 min: compiles OCCT 7.9.3 into the builder image +make image # once, ~40 min: compiles OCCT 8.0.1 into the builder image make dev # inner loop: incremental compile + tests make wheel # compile, stubs, auditwheel, self-containment smoke test make publish # -> https://git.stroblme.de/api/packages/N3XD/pypi diff --git a/docs/building.md b/docs/building.md index fee04f4..1975b80 100644 --- a/docs/building.md +++ b/docs/building.md @@ -6,7 +6,7 @@ and (for publishing) `uv`. ## The builder image -`occt/Dockerfile` compiles OCCT 7.9.3 once inside +`occt/Dockerfile` compiles OCCT 8.0.1 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`. @@ -66,7 +66,15 @@ 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). +only when deliberately re-blessing. + +The OCCT 8.0.1 bump was expected to be such an occasion and turned out not to +be: every fixture round-tripped to the same digest under the new kernel, as did +the measurement, history and mesh-count blocks, so `tests/data/` was left +untouched. Do not assume the next bump re-blesses either — run the gate first +and let it say. Note also that the generator needs a wheel carrying more of +OCCT than this binding exposes (it is written against the stock wheel), so +regenerating is not currently possible from the app's own venv. ## Publishing diff --git a/docs/design.md b/docs/design.md index 8117528..86085f2 100644 --- a/docs/design.md +++ b/docs/design.md @@ -236,26 +236,48 @@ were gated: `IsDeleted` maps compared exactly, since that is the substrate the app's topological naming is built on. -## OCCT 8.0 watchlist +## OCCT 8.0 bump — what actually moved -Recorded as each module landed, so the 10E bump is a known quantity: +Done at 8.0.1 (2026-08-11). Four edits across ~4 000 lines of binding, and the +bound Python surface came out **identical** — `sigdiff` compared 7.9.3.1's dump +against 8.0.1.1's in both directions and found no class, member or constructor +arity changed, so the drop-in contract held without the app being touched. -- `Standard_Failure` derives `std::exception`, and the `Raise`/`Throw`/ - `Instance` static helpers are gone. None of them is bound, so the exception - table should port unchanged. -- `GeomLProp` is superseded by the new `GeomProp`/`BRepProp` packages, whose - results come back as structs with `IsDefined` flags instead of throwing. Only - `GeomLProp_SLProps.Normal` is bound, so this is a one-line move. -- `StdPrs_BRepFont` and `StdPrs_BRepTextBuilder` become deprecated typedefs of - `Font_BRepFont`/`Font_BRepTextBuilder`. They are real classes in 7.9.3 and are - bound as such; 8.0 makes it a rename behind the same `OCP.StdPrs` names. -- Methods that returned handles through an out-parameter return them by value. - `BRep_Tool.Surface`/`Curve` and the adaptors are the ones this binding - touches. -- The global math wrappers are deprecated in favour of `std::`. Nothing here - binds them. -- The byte-identity fixtures retire at the bump: a different kernel legitimately - writes different BREP bytes. The measurement and history blocks stay. +What broke, all of it mechanical: + +- **`NCollection_Utf8String` is gone.** `NCollection_String` + (`NCollection_UtfString`) is the same UTF-8 type and is what the font + API takes. The Python name is unchanged, since the app imports it. +- **`Standard_Failure` lost its `Standard_Transient` RTTI** when it moved to + deriving `std::exception`, so `DynamicType()->Name()` no longer compiles. The + virtual `ExceptionType()` replaced it and returns the same class names, which + is what the exception dispatch keys on — and what `cad_pool` marshals home. +- **The `Size()` → `size_t` migration added index overloads.** `FindKey` on the + indexed maps now has both an `int` and a `size_t` form, so a plain member + pointer is ambiguous; `nb::overload_cast` picks the `int` + one, which keeps the negative-index guard. +- **`StdPrs` moved from TKService to TKV3d**, so `CMakeLists.txt` links both. + +Three watchlist predictions were wrong, recorded here because the reasoning +behind them was plausible: + +- `StdPrs_BRepFont`/`StdPrs_BRepTextBuilder` did **not** become typedefs of + `Font_BRepFont`/`Font_BRepTextBuilder`. The aliasing runs the other way — they + are still the real classes and the `Font_*` names are the typedefs. +- `GeomLProp` was **not** superseded by `GeomProp`/`BRepProp`. It became a + template (`GeomLProp_SLProps` is now an alias for + `GeomLProp_SLPropsBase>`) and kept the constructor and + accessors this binding uses, so the module compiled unchanged. +- **The byte-identity fixtures did not retire.** A different kernel was assumed + to write different BREP bytes; 8.0.1 does not. All six fixtures round-trip to + the same digests, and the measurement, history and mesh-count blocks match + too, so `tests/data/` was left alone and no re-bless happened. That also means + the app's content-addressed BREP payloads stay valid across the bump. + +Unchanged as predicted: the handle model (`occt_handle.h` needed nothing, ASAN +clean), the exception *table* (the removed `Raise`/`Throw`/`Instance` helpers +were never bound), the deprecated out-parameter handle returns this binding +still uses, and the global math wrappers it never bound. ## Open questions diff --git a/occt/Dockerfile b/occt/Dockerfile index ca2b582..795a4a9 100644 --- a/occt/Dockerfile +++ b/occt/Dockerfile @@ -16,8 +16,8 @@ FROM quay.io/pypa/manylinux_2_28_x86_64@sha256:f854c50adf7b7a325bc4794316f3758d3 RUN dnf install -y freetype-devel fontconfig-devel ninja-build ccache valgrind \ && dnf clean all -ARG OCCT_TAG=V7_9_3 -ARG OCCT_SHA256=5ecf094ec6b12d5413dfb851d8c3590c354058aee556e32e408bdfbf8c357d57 +ARG OCCT_TAG=V8_0_1 +ARG OCCT_SHA256=0d6913eae4bcc09a3653ceced6dda1aec11c35a1513d4c06762c9b002092c68a RUN curl -fsSL -o /tmp/occt.tar.gz \ "https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/${OCCT_TAG}.tar.gz" \ @@ -43,7 +43,6 @@ RUN cmake -G Ninja -S /src -B /build \ -DBUILD_MODULE_DataExchange=ON \ -DBUILD_MODULE_ApplicationFramework=ON \ -DBUILD_MODULE_Draw=OFF \ - -DBUILD_MODULE_DETools=OFF \ -DUSE_FREETYPE=ON \ -DUSE_VTK=OFF -DUSE_TK=OFF -DUSE_TCL=OFF \ -DUSE_XLIB=OFF -DUSE_OPENGL=OFF -DUSE_GLES2=OFF \ diff --git a/pyproject.toml b/pyproject.toml index d344b76..b9d3a6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ name = "n3xd-ocp" # .devN suffix: the registry never allows republishing a version, and its # cleanup rule collects only the dev ones. CMake asserts the prefix matches # the OCCT it found. -version = "7.9.3.1" +version = "8.0.1.1" description = "nanobind bindings for the OpenCASCADE geometry kernel (drop-in OCP)" readme = "README.md" requires-python = ">=3.12" diff --git a/src/common/occt_exceptions.cpp b/src/common/occt_exceptions.cpp index eaf9823..f30302d 100644 --- a/src/common/occt_exceptions.cpp +++ b/src/common/occt_exceptions.cpp @@ -90,9 +90,12 @@ void register_occt_exceptions(nb::module_ &root) { } catch (const Standard_Failure &e) { // Dispatch on the dynamic OCCT type so the Python type name // matches what the kernel actually threw; anything we did not - // bind falls back to the RuntimeError-derived base. + // bind falls back to the RuntimeError-derived base. OCCT 8.0 + // dropped the Standard_Transient RTTI here (Standard_Failure + // now derives std::exception); ExceptionType() is the virtual + // that replaced it and returns the same class names. PyObject *type = g_base; - const char *name = e.DynamicType()->Name(); + const char *name = e.ExceptionType(); if (name) { auto it = g_exc_types.find(name); if (it != g_exc_types.end()) diff --git a/src/modules/mod_GeomLProp.cpp b/src/modules/mod_GeomLProp.cpp index ef666b4..7b47ff1 100644 --- a/src/modules/mod_GeomLProp.cpp +++ b/src/modules/mod_GeomLProp.cpp @@ -4,9 +4,9 @@ One use: text emboss reads the surface normal at the anchor's UV to decide which way the glyph solid grows out of a curved face. - OCCT 8.0 supersedes this package with GeomProp/BRepProp (results come back - as structs carrying IsDefined flags instead of throwing). Only Normal() - is bound, so the port is a one-line move — see design.md's 8.0 watchlist. + OCCT 8.0 rebuilt this as a template — GeomLProp_SLProps is now an alias for + GeomLProp_SLPropsBase> — but kept the constructor and + the accessors we bind, so the surface below is unchanged. */ #include "../common/occt_module.h" diff --git a/src/modules/mod_NCollection.cpp b/src/modules/mod_NCollection.cpp index 6ef5e4f..5d91f70 100644 --- a/src/modules/mod_NCollection.cpp +++ b/src/modules/mod_NCollection.cpp @@ -1,9 +1,13 @@ /* OCP.NCollection — the UTF-8 string the font API takes. - NCollection_Utf8String is the only NCollection type the app names directly: - it wraps the font path and the text to emboss. Constructed from a Python - str, which nanobind hands over as UTF-8 already. + This is the only NCollection type the app names directly: it wraps the font + path and the text to emboss. Constructed from a Python str, which nanobind + hands over as UTF-8 already. + + OCCT 8.0 dropped the NCollection_Utf8String alias; NCollection_String is the + same type (NCollection_UtfString) and is what the font API takes. The + Python name stays as it was — it is what the app imports. */ #include "../common/occt_module.h" @@ -13,20 +17,20 @@ void register_NCollection(nb::module_ &root) { nb::module_ m = ocp_submodule(root, "NCollection"); - nb::class_(m, "NCollection_Utf8String") + nb::class_(m, "NCollection_Utf8String") .def(nb::init<>()) // Upstream also offers an explicit-length overload; the app always // passes a whole Python str, which arrives NUL-terminated, so only the // one-argument form is bound. .def( "__init__", - [](NCollection_Utf8String *self, const char *text) { - new (self) NCollection_Utf8String(text); + [](NCollection_String *self, const char *text) { + new (self) NCollection_String(text); }, "theCopy"_a) - .def("ToCString", &NCollection_Utf8String::ToCString) - .def("Size", &NCollection_Utf8String::Size) - .def("Length", &NCollection_Utf8String::Length) - .def("IsEmpty", &NCollection_Utf8String::IsEmpty) - .def("__str__", &NCollection_Utf8String::ToCString); + .def("ToCString", &NCollection_String::ToCString) + .def("Size", &NCollection_String::Size) + .def("Length", &NCollection_String::Length) + .def("IsEmpty", &NCollection_String::IsEmpty) + .def("__str__", &NCollection_String::ToCString); } diff --git a/src/modules/mod_StdPrs.cpp b/src/modules/mod_StdPrs.cpp index d2a47d3..4d1875c 100644 --- a/src/modules/mod_StdPrs.cpp +++ b/src/modules/mod_StdPrs.cpp @@ -5,11 +5,12 @@ compound of face outlines, holes already subtracted, which it then extrudes into or out of the model. - These live in TKService, and both classes are real in 7.9.3 rather than the - deprecated typedefs of Font_BRepFont/Font_BRepTextBuilder that OCCT 8.0 - turns them into — so the 8.0 port is a rename here and nothing else. The - builder image already asserts TKService links without libGL or libX11, so - binding them costs the runtime image nothing. + Both are real classes, and the aliasing runs the opposite way from what the + 8.0 watchlist expected: Font_BRepFont/Font_BRepTextBuilder are the typedefs + of these, not the reverse. What 8.0 did move is the package — StdPrs went + from TKService to TKV3d, which is why CMakeLists links both. The builder + image asserts neither pulls in libGL or libX11, so binding them costs the + runtime image nothing. Rendering a glyph is FreeType work with no Python in it, so it releases the GIL. diff --git a/src/modules/mod_TopTools.cpp b/src/modules/mod_TopTools.cpp index 05a06a9..0b6723f 100644 --- a/src/modules/mod_TopTools.cpp +++ b/src/modules/mod_TopTools.cpp @@ -62,8 +62,13 @@ void register_TopTools(nb::module_ &root) { }, "theKey"_a) .def("Contains", &TopTools_IndexedMapOfShape::Contains, "theKey"_a) - .def("FindKey", &TopTools_IndexedMapOfShape::FindKey, "theIndex"_a, - OCP_RETURN_COPY) + // OCCT 8.0 added size_t index overloads beside the int ones, so the + // plain member pointer is ambiguous; the int form keeps the negative + // index guarded rather than wrapping it into a huge size_t. + .def("FindKey", + nb::overload_cast( + &TopTools_IndexedMapOfShape::FindKey, nb::const_), + "theIndex"_a, OCP_RETURN_COPY) .def("FindIndex", &TopTools_IndexedMapOfShape::FindIndex, "theKey"_a) .def("Extent", &TopTools_IndexedMapOfShape::Extent) .def("Size", &TopTools_IndexedMapOfShape::Size) @@ -78,7 +83,10 @@ void register_TopTools(nb::module_ &root) { .def(nb::init<>()) .def("Contains", &TopTools_IndexedDataMapOfShapeListOfShape::Contains, "theKey"_a) - .def("FindKey", &TopTools_IndexedDataMapOfShapeListOfShape::FindKey, + .def("FindKey", + nb::overload_cast( + &TopTools_IndexedDataMapOfShapeListOfShape::FindKey, + nb::const_), "theIndex"_a, OCP_RETURN_COPY) .def("FindIndex", &TopTools_IndexedDataMapOfShapeListOfShape::FindIndex, "theKey"_a) diff --git a/tools/parity_venv.sh b/tools/parity_venv.sh index 7c2bd04..be16141 100755 --- a/tools/parity_venv.sh +++ b/tools/parity_venv.sh @@ -57,8 +57,9 @@ else fi # Everything below runs the venv's interpreter directly. `uv run` would re-sync -# the environment against the app's manifest first, which still asks for -# cadquery-ocp-novtk — reinstalling it on top of the swap. +# the environment against the app's manifest first, reinstalling the pinned +# version on top of the swap — which across a kernel bump means silently +# testing the old OCCT while believing you are on the new one. echo "--- sanity ---" # Asserted, not printed: __occt_version__ exists only on our wheel, so this is # also the check that the swap landed in the environment we meant.