diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..4928eee --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,166 @@ +--- +name: release +description: Cut a wasm-cxx-shim release — version bump, CHANGELOG, doc-staleness sweep, PR (and post-merge, tag + GitHub Release) +user-invocable: true +--- + +# Release — wasm-cxx-shim + +Drives the mechanical parts of a release end-to-end. Pauses at each +human-decision point (CHANGELOG content review, PR body sign-off, +post-merge tag) rather than barreling through. + +Read [`CLAUDE.md`](../../../CLAUDE.md) first — the "Commit history +policy" and "All changes to main go through PRs" sections set the +ground rules this skill operates within. + +## Arguments + +- `` (required): the version being released, no leading `v`. + Examples: `0.4.0`, `0.4.0-alpha.1`, `0.4.0-alpha.1+5f95a3ac`. The + CMake `project(VERSION ...)` line accepts only `MAJOR.MINOR.PATCH`, + so for pre-releases the alpha/build-metadata segments live only on + the git tag and the CHANGELOG title — CMakeLists.txt gets the bare + numeric version. +- `prep`: run only phases 1-5 (version bump, CHANGELOG, sweep, build + verify, push branch + open PR). Stop before the post-merge phases. +- `tag`: run only phases 6-7 (assumes the PR has merged; tags the + merge commit, pushes, creates the GitHub Release). +- No phase flag: run `prep` (default). + +## Pre-flight checks + +Before doing anything, verify: + +1. **On a feature branch**, not `main`. If on `main`, stop and tell + the user to `git checkout -b release/v` first. +2. **Working tree is clean** (`git status --porcelain` empty). If not, + stop and surface the dirty paths. +3. **The version isn't already tagged.** `git rev-parse v` + should fail with "unknown revision." If it succeeds, stop — + tagging the same version twice would be a bug. +4. **`gh` is authenticated** (`gh auth status`). The skill needs it + for PR + release creation. + +## Phases + +### 1. Version bump + +Update `CMakeLists.txt`'s `project(... VERSION X.Y.Z ...)` line. For +pre-releases, drop the alpha/build-metadata segments — CMake doesn't +parse them. Verify with `grep VERSION CMakeLists.txt | head -3`. + +### 2. CHANGELOG + +Update `CHANGELOG.md`: + +- **Move the `## Unreleased` section** to a new versioned section + (`## v ()`). The + one-line summary is yours to draft based on what's in the + Unreleased section; surface it to the user for sign-off before + finalizing. +- **Re-add an empty `## Unreleased`** above the new versioned + section. +- **Update the "Tested upstream combinations" table** at the top + with a new row for this version. Pull the manifold pin from + `cmake/WasmCxxShimManifold.cmake`'s + `_wasm_cxx_shim_manifold_default_manifold_tag` default, the + Clipper2 pin (where applicable — for v0.4.0+ it's "inherits + manifold's pin"), and the patch count (e.g., "1 (verbatim diff + of #1690)"). + +### 3. Doc-staleness sweep + +Run the sweep from the review skill's Cat 8: + +```sh +grep -rn -E "v0\.[0-9]|pre-CI|TBD|TODO|coming|not started|in flight|NEXT" \ + --include='*.md' --exclude=CHANGELOG.md --exclude-dir=build . +``` + +Plus the API/patch-rename audit from Cat 8: if this release renames +or drops any `wasm_cxx_shim_add_manifold()` arguments, or shipped +patches under `cmake/manifold-patches/`, grep for the old names +across all `*.md` and surface hits. + +Stop and walk the user through each hit: intentional historical +narrative or stale text? Apply the user's calls. + +### 4. Build verify + +Don't tag a release that doesn't build. Run: + +```sh +rm -rf build/wasm32 && cmake --preset wasm32 -DWASM_CXX_SHIM_BUILD_MANIFOLD_LINK=ON \ + && cmake --build --preset wasm32 -j \ + && ctest --preset wasm32 +``` + +All ctest entries must pass, including manifold-link and +manifold-tests. If anything fails, stop and surface — releasing on +red is never the right call. + +### 5. PR + +Per CLAUDE.md's "one session, one commit" + "All changes to main go +through PRs": + +- If the branch has multiple commits, propose squashing them + (`git reset --soft && git commit`). Stop and + ask before force-resetting. +- Push the branch (`git push -u origin ` for new branches, + `git push --force-with-lease` for amended ones). +- Open the PR via `gh pr create`. The PR title is "Release + v" or similar; the body is **extracted from the + CHANGELOG section you just wrote** (the versioned `## v` + block you moved Unreleased into). Surface the body for user + sign-off before submitting. + +**Stop here.** Per CLAUDE.md's "merge guardrail" — don't run `gh pr +merge`. Wait for the user to merge via the GitHub UI or their own +`gh pr merge`. CI must be green; a green CI is the END of the +prep flow, not a green light to merge. + +### 6. Tag (post-merge) + +After the user confirms the merge happened: + +- `git checkout main && git pull && git remote prune origin`. +- `git tag v `. Use the actual merge + commit, not whatever `HEAD` happens to be. +- `git push origin v`. + +### 7. GitHub Release + +```sh +gh release create v \ + --title "v" \ + --notes-file <(awk '/^## v/,/^## v[^.]+\.[^.]+\.[^.]+/' CHANGELOG.md | head -n -1) +``` + +The awk extracts the section between `## v` and the next +`## v...` header. Verify the extracted notes look right before +submitting; offer the user the rendered preview. + +## Notes / gotchas + +- **Pre-release versions**: CMake's `project(VERSION ...)` doesn't + accept `0.4.0-alpha.1+5f95a3ac` — only `0.4.0`. The alpha/build + metadata lives on the git tag (`v0.4.0-alpha.1+5f95a3ac`) and in + the CHANGELOG title. Inside the project, version comparisons use + the bare numeric version. +- **Tested-combinations table**: the column meanings shifted at + v0.4.0-alpha.1. Pre-v0.4: the shim pre-declared Clipper2 with + its own pin. v0.4+: manifold owns Clipper2's declaration, so the + Clipper2 column reads "inherits manifold's pin" instead of a SHA. + When updating, match the convention of the row above to stay + consistent. +- **Carry-patch SHA in tested-combinations**: when a release + vendors an upstream PR's diff (e.g., elalish/manifold#1690 in + v0.4.0-alpha.1), the manifold pin includes the upstream commit + the patch was generated against. Format: `` (master + + vendored elalish/manifold#). +- **Don't tag before merge.** If the branch hasn't merged yet, + there's no merge commit to tag against. Tagging a feature branch + ahead of merge produces a tag that points at a commit that + later disappears from `main`'s history. diff --git a/.claude/skills/review/SKILL.md b/.claude/skills/review/SKILL.md index 796f7fe..fa4be8e 100644 --- a/.claude/skills/review/SKILL.md +++ b/.claude/skills/review/SKILL.md @@ -270,6 +270,26 @@ For any change touching `libc/src/dlmalloc/malloc.c`, installed prefix. - **Install rules cover headers.** `libm/include/` must be installed, or downstream consumers can't `#include `. +- **`FetchContent_Declare(... PATCH_COMMAND ...)` is idempotent.** + Re-configures (e.g., from any CMakeLists.txt edit) re-trigger + PATCH_COMMAND; vanilla `git apply` then fails on the already-patched + source tree. Acceptable mitigations: `UPDATE_DISCONNECTED TRUE` on + the FetchContent_Declare (short-circuits the patch step on + subsequent configures), or a wrapper `cmake -P` script that does + `git apply --reverse --check` first. A bare `PATCH_COMMAND git + apply ${PATCH}` with neither mitigation is an **error** — works + on a clean tree, breaks the moment the user re-configures. +- **Order-of-operations on derived options across nested + `FetchContent_MakeAvailable` calls.** When a consumer pre-declares + package A before package B's MakeAvailable runs, B's CMakeLists + runs after A is already populated. Any cache var B sets to + influence A's option resolution arrives too late. If a helper has + a pre-declare, audit whether a transitive dep of the + pre-declared package needs to consume an option set by an + intermediate CMakeLists — if so, drop the pre-declare or set the + cache var explicitly in the helper. **Warning** if a + consumer-pre-declared dep also has cache-var dependencies coming + from a nested package. ### 6. Toolchain & build infrastructure @@ -420,6 +440,18 @@ For any change touching `libc/src/dlmalloc/malloc.c`, READMEs that drift when test coverage extends. Either keep abstract ("a slice of its tests passes") or commit to updating on every test addition. Drift here is a **note**. +- **API renames / removals propagated to docstrings + caller sites.** + When a CMake helper drops or renames an argument (e.g., + `wasm_cxx_shim_add_manifold(CLIPPER2_GIT_TAG ...)` → no longer + accepted), audit: (a) the helper's own docstring at the top of the + file, (b) every caller site (`grep -rn 'wasm_cxx_shim_add_manifold'`), + (c) any README that documents the helper's parameters by name. Same + for renamed/removed shipped patches: when `0001-foo.patch` becomes + `0001-bar.patch` (or three patches collapse to one), grep all `*.md` + for the old patch filenames. A docstring that lists a removed + argument is a **warning**; a README that names a removed patch is + also a **warning** since a future agent will look for it and be + confused. ### 9. Style for hand-written code diff --git a/CHANGELOG.md b/CHANGELOG.md index d996783..cfeff97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,11 @@ pin of manifold + Clipper2 with the carry-patches that make them link on `wasm32-unknown-unknown`. Each tagged shim release verifies the combination in CI: -| Shim version | manifold | Clipper2 | Patches shipped | -|--------------|----------------|----------------------------------------------|-----------------------| -| v0.3.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 | -| v0.2.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 (carried in `test/manifold-link/patches/`; helper not yet present) | +| Shim version | manifold | Clipper2 | Patches shipped | +|-----------------------|-----------------------------------------------|----------------------------------------------|-----------------------| +| v0.4.0-alpha.1 | `5f95a3ac` (master + vendored elalish/manifold#1690) | inherits manifold's pin (manifold owns the FetchContent_Declare) | 1 (verbatim diff of #1690) | +| v0.3.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 | +| v0.2.0 | `v3.4.1` | `46f639177fe418f9689e8ddb74f08a870c71f5b4` | 0001 + 0002 + 0003 (carried in `test/manifold-link/patches/`; helper not yet present) | Consumers calling `wasm_cxx_shim_add_manifold()` (introduced in v0.3.0) with no arguments get the row matching their installed shim version. @@ -32,7 +33,63 @@ version where the macro guard appears natively. ## Unreleased -(no changes since v0.3.0) +(no changes since v0.4.0-alpha.1) + +## v0.4.0-alpha.1 — manifold pin bump + #1690 carry-patch (2026-05-03) + +Pre-release. Pins manifold to a current upstream master commit and +folds the three shim-side iostream patches into a single vendored +diff of [elalish/manifold#1690][pr1690] (the upstream PR that adds +`MANIFOLD_NO_IOSTREAM` natively). Once #1690 lands and the shim's +manifold pin moves past it, the carry-patch drops entirely and a +v0.4.0 (non-alpha) release follows. + +[pr1690]: https://github.com/elalish/manifold/pull/1690 + +### Added + +- `cmake/manifold-patches/0001-manifold-no-iostream.patch` — verbatim + diff of #1690 against the pinned upstream manifold commit. Replaces + the three previously-shipped patches. +- Six additional manifold test files now run on the shim: + `boolean_complex_test`, `manifoldc_test`, `smooth_test` (plus the + existing `boolean_test`, `cross_section_test`, `sdf_test`). Total + test count: **121** (up from 71). + +### Changed + +- `wasm_cxx_shim_add_manifold()`: refactored for the post-#1690 + world. Drops the Clipper2 pre-declaration, sets + `MANIFOLD_NO_IOSTREAM=ON` as a CMake cache var, and lets manifold's + carry-patched option chain propagate `MANIFOLD_NO_FILESYSTEM` and + `CLIPPER2_NO_IOSTREAM` as PUBLIC compile defs. +- API: removed `CLIPPER2_GIT_TAG` and `EXTRA_CLIPPER2_PATCHES` + parameters (manifold owns the Clipper2 declaration; shim no longer + has a say). `MANIFOLD_GIT_TAG`, `EXTRA_MANIFOLD_PATCHES`, and + `SKIP_BUILTIN_PATCHES` continue to work. +- Default `MANIFOLD_GIT_TAG` bumped from `v3.4.1` to a master commit + (`5f95a3ac`) so the carry-patch applies cleanly. +- `manifold_tests_size_budget` raised from 1.10 MB to 1.40 MB to + accommodate the expanded test set. + +### Removed + +- The three shim-side iostream patches + (`0001-clipper2-strip-iostream`, `0002-manifold-ifdef-iostream`, + `0003-manifold-test-main-ifdef-filesystem`). Replaced by the + single vendored #1690 diff. + +### Notes for downstream consumers + +- This is an **alpha pre-release** pinned to a specific manifold + commit. The carry-patch may be re-rolled if #1690 evolves in + review (would land as alpha.2, alpha.3, etc.). The non-alpha + v0.4.0 ships once #1690 merges. +- Consumers calling `wasm_cxx_shim_add_manifold()` without + overrides will be on the bumped pin automatically. Override + with `MANIFOLD_GIT_TAG ` if you need to stay on the v0.3.0 + combination — pair with `EXTRA_MANIFOLD_PATCHES` to supply your + own iostream patches in that case. ## v0.3.0 — CMake integration helper (2026-05-02) diff --git a/CLAUDE.md b/CLAUDE.md index 270d353..5ce28cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -369,6 +369,38 @@ them. "patches not found at expected path" with no obvious connection to the bug, so future helpers should follow the captured-at-top pattern. +- **`FetchContent` `PATCH_COMMAND` is not idempotent by default.** The + patch step re-runs on every CMake re-configure (any CMakeLists.txt + edit triggers it). Vanilla `git apply` then fails on the + already-patched source tree with "patch does not apply" — and the + failure mode is mysterious because nothing in the diff explains why + `cmake --build` started failing after a trivial edit. Two known fixes: + 1. `UPDATE_DISCONNECTED TRUE` on the `FetchContent_Declare(...)` + call short-circuits the patch step on subsequent configures. + Simplest when you control the declare site (e.g., the shim's + own `wasm_cxx_shim_add_manifold()` helper uses this). + 2. A wrapper `cmake -P` script that does `git apply --reverse + --check` first and only applies if not already applied. Use this + when `UPDATE_DISCONNECTED` isn't appropriate (e.g., a library + wants its own FetchContent_Declare to be re-applyable on every + configure for development workflows). Pattern lives at + `manifold-upstream/cmake/patches/apply-clipper2-patch.cmake` in + the in-flight #1690. +- **Order-of-operations with `FetchContent_MakeAvailable` and a + consumer pre-declared transitive dep.** When a consumer (the shim) + pre-declares package A (Clipper2) before package B (manifold) does + `FetchContent_MakeAvailable(B)`, B's CMakeLists runs *after* A is + already populated and added as a subdirectory. Any cache var that B + sets to influence A's option resolution arrives *too late* — A's + `option()`s have already evaluated to their defaults. We hit this + trying to make `MANIFOLD_NO_IOSTREAM=ON` propagate to + `CLIPPER2_NO_IOSTREAM=ON` (manifold #1690 sets the latter from the + former in `manifoldDeps.cmake`); shim's pre-declare meant manifold's + derivation never reached Clipper2's option. Fix: drop the + pre-declare and let the inner FetchContent declaration win, so the + derivation happens in the right order. Pre-declaration is a + workaround for injecting patches that the upstream doesn't carry — + once those patches move upstream, the workaround stops being needed. ### wasm-ld and test wasms @@ -441,6 +473,18 @@ them. commit to updating on every test addition. Detailed counts live in `test/manifold-tests/README.md` where they're maintained alongside the test list itself. +- **Don't claim a test failure is "pre-existing" without verifying on + a default build.** When a test fails under a non-default config + (e.g., `MANIFOLD_NO_IOSTREAM=ON`), it's tempting to assume the + failure is upstream-pre-existing and unrelated. Verify by running + the same test on a default build *before* claiming the failure is + unrelated. Cost of getting this wrong: a real bug that I introduced + propagated from an open upstream PR through the shim's integration + test and into the alpha-tagging plan, until the user explicitly + questioned the claim. Specific case: `BooleanComplex.CraycloudBool` + passes on default builds; only failed because the asymmetric stub + for `ReadTestOBJ` returned an empty `Manifold`, breaking the test's + assertion that the boolean output was non-empty. ## Things to NEVER do diff --git a/CMakeLists.txt b/CMakeLists.txt index d308db4..c13dd92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ cmake_minimum_required(VERSION 3.25) project(wasm-cxx-shim - VERSION 0.3.0 + VERSION 0.4.0 DESCRIPTION "Minimal C/C++ runtime shim for wasm32-unknown-unknown" LANGUAGES C CXX ) diff --git a/cmake/WasmCxxShimManifold.cmake b/cmake/WasmCxxShimManifold.cmake index c0859e4..07d8b23 100644 --- a/cmake/WasmCxxShimManifold.cmake +++ b/cmake/WasmCxxShimManifold.cmake @@ -3,13 +3,13 @@ # Helper for consumers building manifold (elalish/manifold) on top of # wasm-cxx-shim for the wasm32-unknown-unknown target. Captures the # high-change-rate parts of the integration cocktail (FetchContent -# pins, patch application, manifold's CMake options) that drift fast -# across manifold versions. Lower-change-rate parts (the libc++ -# `__config_site` override, the `` stub header, and -# `libcxx-extras.cpp`) stay scoped under the shim's +# pin, the iostream-stripping carry-patch, manifold's CMake options) +# that drift fast across manifold versions. Lower-change-rate parts +# (the libc++ `__config_site` override, the `` stub header, +# and `libcxx-extras.cpp`) stay scoped under the shim's # `test/manifold-link/` for libc++-source-drift insulation reasons -# documented in CLAUDE.md; the consumer copies those files into their -# own tree (or rolls equivalents). +# documented in CLAUDE.md; the consumer copies those files into +# their own tree (or rolls equivalents). # # Loaded automatically by `find_package(wasm-cxx-shim)`. Source-tree # consumers (`add_subdirectory(wasm-cxx-shim)`) can include explicitly: @@ -24,18 +24,17 @@ # # wasm_cxx_shim_add_manifold( # MANIFOLD_GIT_TAG # tag or SHA; default = shim's tested pin -# CLIPPER2_GIT_TAG # default = matching SHA # EXTRA_MANIFOLD_PATCHES

... # additional patches to apply -# EXTRA_CLIPPER2_PATCHES

... -# SKIP_BUILTIN_PATCHES # opt out of shim-shipped patches +# SKIP_BUILTIN_PATCHES # opt out of the shipped #1690 carry-patch # ) # # After the call, the `manifold`, `manifoldc`, and `Clipper2` CMake -# targets exist with the right manifold/Clipper2 CMake options applied -# (MANIFOLD_TEST=OFF, MANIFOLD_PAR=OFF, etc.) and ambient -# `add_compile_options` set (-fno-exceptions / -fno-rtti / -nostdlib / -# -nostdinc++ + the MANIFOLD_NO_IOSTREAM/FILESYSTEM/PAR=-1 macros + -# CLIPPER2_MAX_DECIMAL_PRECISION=8). +# targets exist with the right manifold/Clipper2 CMake options +# applied (MANIFOLD_TEST=OFF, MANIFOLD_PAR=OFF, MANIFOLD_NO_IOSTREAM=ON, +# etc.) and ambient `add_compile_options` set (-fno-exceptions / +# -fno-rtti / -nostdlib / -nostdinc++). `Clipper2` is created via +# manifold's nested FetchContent_MakeAvailable (this helper does +# not pre-declare it). # # The consumer is responsible for: # * the `-isystem` chain (libc++ headers, the `__config_site` @@ -47,30 +46,40 @@ # # The shim's `test/manifold-link/CMakeLists.txt` is a worked example # that consumes this helper. +# +# --- +# +# The shipped carry-patch is the verbatim diff of elalish/manifold#1690 +# (adds `MANIFOLD_NO_IOSTREAM` build option + Clipper2 tracking patch) +# against the upstream master commit pinned below. Once #1690 lands and +# the pin moves past it, the patch drops entirely. Until then, this +# helper bakes #1690's design into the shim — manifold's option chain +# does the work, the shim just sets the option ON and lets manifold +# propagate `MANIFOLD_NO_IOSTREAM` / `MANIFOLD_NO_FILESYSTEM` / +# `CLIPPER2_NO_IOSTREAM` as PUBLIC compile defs. include_guard(GLOBAL) -# Resolve our own location so the helper finds its shipped patches +# Resolve our own location so the helper finds its shipped patch # regardless of whether it's loaded from the source tree or from an # installed package config dir. set(_wasm_cxx_shim_manifold_helper_dir "${CMAKE_CURRENT_LIST_DIR}") -# Tested-pin defaults. Bumped when the shim cuts a new release that -# verifies a new manifold/Clipper2 combination. Source of truth: this -# file. Changes here must be paired with a CHANGELOG.md entry + a -# version bump. -set(_wasm_cxx_shim_manifold_default_manifold_tag "v3.4.1" +# Tested-pin default. Bumped when the shim cuts a new release that +# verifies a new manifold combination. Source of truth: this file. +# Changes here must be paired with a CHANGELOG.md entry + a version +# bump (and re-rolling the carry-patch if upstream master moved +# beneath #1690's diff context). +set(_wasm_cxx_shim_manifold_default_manifold_tag + "5f95a3ac0e906f596bb2d27a52d005ef60de58f3" CACHE STRING "Default manifold pin used by wasm_cxx_shim_add_manifold()") -set(_wasm_cxx_shim_manifold_default_clipper2_tag "46f639177fe418f9689e8ddb74f08a870c71f5b4" - CACHE STRING - "Default Clipper2 pin (must match manifold's pin for the manifold tag)") function(wasm_cxx_shim_add_manifold) cmake_parse_arguments(_WCSAM "SKIP_BUILTIN_PATCHES" - "MANIFOLD_GIT_TAG;CLIPPER2_GIT_TAG" - "EXTRA_MANIFOLD_PATCHES;EXTRA_CLIPPER2_PATCHES" + "MANIFOLD_GIT_TAG" + "EXTRA_MANIFOLD_PATCHES" ${ARGN}) if(_WCSAM_UNPARSED_ARGUMENTS) @@ -81,83 +90,68 @@ function(wasm_cxx_shim_add_manifold) if(NOT _WCSAM_MANIFOLD_GIT_TAG) set(_WCSAM_MANIFOLD_GIT_TAG "${_wasm_cxx_shim_manifold_default_manifold_tag}") endif() - if(NOT _WCSAM_CLIPPER2_GIT_TAG) - set(_WCSAM_CLIPPER2_GIT_TAG "${_wasm_cxx_shim_manifold_default_clipper2_tag}") - endif() - # Assemble patch lists. Builtin patches first, then user extras — - # caller can append context-aware patches that depend on builtin - # state, or replace builtins via SKIP_BUILTIN_PATCHES + EXTRA_*. - set(_clipper2_patches "") + # Builtin patch (the vendored #1690 diff) plus user extras. set(_manifold_patches "") if(NOT _WCSAM_SKIP_BUILTIN_PATCHES) - list(APPEND _clipper2_patches - "${_wasm_cxx_shim_manifold_helper_dir}/manifold-patches/0001-clipper2-strip-iostream.patch") list(APPEND _manifold_patches - "${_wasm_cxx_shim_manifold_helper_dir}/manifold-patches/0002-manifold-ifdef-iostream.patch" - "${_wasm_cxx_shim_manifold_helper_dir}/manifold-patches/0003-manifold-test-main-ifdef-filesystem.patch") + "${_wasm_cxx_shim_manifold_helper_dir}/manifold-patches/0001-manifold-no-iostream.patch") endif() - list(APPEND _clipper2_patches ${_WCSAM_EXTRA_CLIPPER2_PATCHES}) list(APPEND _manifold_patches ${_WCSAM_EXTRA_MANIFOLD_PATCHES}) include(FetchContent) - # Pre-declare Clipper2 BEFORE manifold's own FetchContent_Declare - # runs. CMake's first-declaration-wins makes manifold's silently - # ignored, so our pin + patches are used. Match manifold's - # SOURCE_SUBDIR (CPP); see manifold's cmake/manifoldDeps.cmake. - set(_clipper2_decl_args - GIT_REPOSITORY https://github.com/AngusJohnson/Clipper2.git - GIT_TAG ${_WCSAM_CLIPPER2_GIT_TAG} - SOURCE_SUBDIR CPP - UPDATE_DISCONNECTED TRUE) - if(_clipper2_patches) - list(APPEND _clipper2_decl_args - PATCH_COMMAND git apply --ignore-whitespace -p0 ${_clipper2_patches}) - endif() - FetchContent_Declare(Clipper2 ${_clipper2_decl_args}) - + # Manifold owns its Clipper2 declaration (manifoldDeps.cmake). + # With the #1690 carry-patch applied, that declaration also + # carries a Clipper2 carry-patch tracking AngusJohnson/Clipper2#1094, + # and sets CLIPPER2_NO_IOSTREAM=ON when MANIFOLD_NO_IOSTREAM is on. + # We don't pre-declare Clipper2 here. set(_manifold_decl_args GIT_REPOSITORY https://github.com/elalish/manifold.git GIT_TAG ${_WCSAM_MANIFOLD_GIT_TAG} - GIT_SHALLOW TRUE UPDATE_DISCONNECTED TRUE) if(_manifold_patches) + # `git apply` flag set matches manifold-csg's proven recipe: + # --ignore-whitespace + --whitespace=nowarn for cross-platform + # robustness (CRLF, mixed indentation). list(APPEND _manifold_decl_args - PATCH_COMMAND git apply --ignore-whitespace -p0 ${_manifold_patches}) + PATCH_COMMAND git apply --ignore-whitespace --whitespace=nowarn ${_manifold_patches}) endif() FetchContent_Declare(manifold ${_manifold_decl_args}) # Manifold + Clipper2 CMake options. Set BEFORE - # FetchContent_MakeAvailable. Most are about turning off things - # that would otherwise pull in dependencies the shim doesn't have - # (Python bindings, JS bindings, googletest, threading backends). - set(MANIFOLD_TEST OFF CACHE BOOL "" FORCE) - set(MANIFOLD_CBIND ON CACHE BOOL "" FORCE) - set(MANIFOLD_PYBIND OFF CACHE BOOL "" FORCE) - set(MANIFOLD_JSBIND OFF CACHE BOOL "" FORCE) - set(MANIFOLD_PAR OFF CACHE BOOL "" FORCE) - set(MANIFOLD_EXCEPTIONS OFF CACHE BOOL "" FORCE) - set(MANIFOLD_DEBUG OFF CACHE BOOL "" FORCE) - set(MANIFOLD_EXPORT OFF CACHE BOOL "" FORCE) - set(CLIPPER2_TESTS OFF CACHE BOOL "" FORCE) - set(CLIPPER2_UTILS OFF CACHE BOOL "" FORCE) - set(CLIPPER2_EXAMPLES OFF CACHE BOOL "" FORCE) + # FetchContent_MakeAvailable. MANIFOLD_NO_IOSTREAM=ON is the + # load-bearing one — manifold's option chain (added by the + # carry-patch) propagates it to MANIFOLD_NO_FILESYSTEM and + # CLIPPER2_NO_IOSTREAM as PUBLIC compile defs. + set(MANIFOLD_NO_IOSTREAM ON CACHE BOOL "" FORCE) + set(MANIFOLD_TEST OFF CACHE BOOL "" FORCE) + set(MANIFOLD_CBIND ON CACHE BOOL "" FORCE) + set(MANIFOLD_PYBIND OFF CACHE BOOL "" FORCE) + set(MANIFOLD_JSBIND OFF CACHE BOOL "" FORCE) + set(MANIFOLD_PAR OFF CACHE BOOL "" FORCE) + set(MANIFOLD_EXCEPTIONS OFF CACHE BOOL "" FORCE) + set(MANIFOLD_DEBUG OFF CACHE BOOL "" FORCE) + set(MANIFOLD_EXPORT OFF CACHE BOOL "" FORCE) + set(CLIPPER2_TESTS OFF CACHE BOOL "" FORCE) + set(CLIPPER2_UTILS OFF CACHE BOOL "" FORCE) + set(CLIPPER2_EXAMPLES OFF CACHE BOOL "" FORCE) # Compile flags applied to manifold + Clipper2 sources via the # ambient add_compile_options. These propagate to all # subdirectories added afterward, which is what we want for a - # wasm32 build (consumer code wants the same flags). + # wasm32 build (consumer code wants the same flags). The + # NO_IOSTREAM/NO_FILESYSTEM macros are now set as PUBLIC compile + # defs by manifold itself (via the carry-patch); we no longer + # need to add them here. add_compile_options( -fno-exceptions -fno-rtti -nostdlib -nostdinc++ - -DMANIFOLD_NO_IOSTREAM=1 - -DMANIFOLD_NO_FILESYSTEM=1 -DMANIFOLD_PAR=-1 -DCLIPPER2_MAX_DECIMAL_PRECISION=8 ) - FetchContent_MakeAvailable(Clipper2 manifold) + FetchContent_MakeAvailable(manifold) endfunction() diff --git a/cmake/check-manifold-helper.cmake b/cmake/check-manifold-helper.cmake index 51033b9..88ad59d 100644 --- a/cmake/check-manifold-helper.cmake +++ b/cmake/check-manifold-helper.cmake @@ -32,26 +32,18 @@ if(NOT COMMAND wasm_cxx_shim_add_manifold) "wasm_cxx_shim_add_manifold() command not registered after include") endif() -# Default tags should be non-empty (catches accidental nuking + typos). +# Default tag should be non-empty (catches accidental nuking + typos). if(NOT _wasm_cxx_shim_manifold_default_manifold_tag) message(FATAL_ERROR "default manifold tag is empty") endif() -if(NOT _wasm_cxx_shim_manifold_default_clipper2_tag) - message(FATAL_ERROR "default Clipper2 tag is empty") -endif() -# Shipped patches must resolve. Catches "file moved" / "captured the -# wrong CMAKE_CURRENT_LIST_DIR" / "install rule missed a file". -foreach(_patch IN ITEMS - "0001-clipper2-strip-iostream.patch" - "0002-manifold-ifdef-iostream.patch" - "0003-manifold-test-main-ifdef-filesystem.patch") - set(_p "${_wasm_cxx_shim_manifold_helper_dir}/manifold-patches/${_patch}") - if(NOT EXISTS "${_p}") - message(FATAL_ERROR "shipped patch missing: ${_p}") - endif() -endforeach() +# Shipped carry-patch must resolve. Catches "file moved" / "captured +# the wrong CMAKE_CURRENT_LIST_DIR" / "install rule missed a file". +set(_p "${_wasm_cxx_shim_manifold_helper_dir}/manifold-patches/0001-manifold-no-iostream.patch") +if(NOT EXISTS "${_p}") + message(FATAL_ERROR "shipped patch missing: ${_p}") +endif() message(STATUS - "helper API smoke OK: defaults [manifold=${_wasm_cxx_shim_manifold_default_manifold_tag}, " - "clipper2=${_wasm_cxx_shim_manifold_default_clipper2_tag}], 3 patches resolved") + "helper API smoke OK: default manifold pin=${_wasm_cxx_shim_manifold_default_manifold_tag}, " + "carry-patch resolved") diff --git a/cmake/manifold-patches/0001-clipper2-strip-iostream.patch b/cmake/manifold-patches/0001-clipper2-strip-iostream.patch deleted file mode 100644 index 44ade89..0000000 --- a/cmake/manifold-patches/0001-clipper2-strip-iostream.patch +++ /dev/null @@ -1,243 +0,0 @@ -From wasm-cxx-shim Phase-7-B1 work-in-progress. -Strip references from Clipper2 headers so the library compiles -without libc++'s localization machinery (which is disabled via -_LIBCPP_HAS_LOCALIZATION=0 in the smoke test's __config_site override). - -Originally documented in pca006132's manifold-on-wasm experiment: - https://github.com/elalish/manifold/discussions/1046#discussioncomment-11302257 - -Generated against Clipper2 SHA 46f639177fe418f9689e8ddb74f08a870c71f5b4 -(the SHA manifold v3.4.1 pins via FetchContent). - -Upstream PR target: AngusJohnson/Clipper2. Cleaner upstream form would -guard the stream-using bits with `#ifndef CLIPPER2_NO_IOSTREAM`. - -diff --git CPP/Clipper2Lib/include/clipper2/clipper.core.h CPP/Clipper2Lib/include/clipper2/clipper.core.h -index 99a5205..c692f36 100644 ---- CPP/Clipper2Lib/include/clipper2/clipper.core.h -+++ CPP/Clipper2Lib/include/clipper2/clipper.core.h -@@ -14,7 +14,7 @@ - #include - #include - #include --#include -+// #include - #include - #include - #include -@@ -166,11 +166,11 @@ namespace Clipper2Lib - - void SetZ(const z_type z_value) { z = z_value; } - -- friend std::ostream& operator<<(std::ostream& os, const Point& point) -- { -- os << point.x << "," << point.y << "," << point.z; -- return os; -- } -+ // friend std::ostream& operator<<(std::ostream& os, const Point& point) -+ // { -+ // os << point.x << "," << point.y << "," << point.z; -+ // return os; -+ // } - - #else - -@@ -203,11 +203,11 @@ namespace Clipper2Lib - return Point(x * scale, y * scale); - } - -- friend std::ostream& operator<<(std::ostream& os, const Point& point) -- { -- os << point.x << "," << point.y; -- return os; -- } -+ // friend std::ostream& operator<<(std::ostream& os, const Point& point) -+ // { -+ // os << point.x << "," << point.y; -+ // return os; -+ // } - #endif - - friend bool operator==(const Point& a, const Point& b) -@@ -396,10 +396,10 @@ namespace Clipper2Lib - return result; - } - -- friend std::ostream& operator<<(std::ostream& os, const Rect& rect) { -- os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") "; -- return os; -- } -+ // friend std::ostream& operator<<(std::ostream& os, const Rect& rect) { -+ // os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") "; -+ // return os; -+ // } - }; - - template -@@ -498,26 +498,26 @@ namespace Clipper2Lib - return Rect(xmin, ymin, xmax, ymax); - } - -- template -- std::ostream& operator << (std::ostream& outstream, const Path& path) -- { -- if (!path.empty()) -- { -- auto pt = path.cbegin(), last = path.cend() - 1; -- while (pt != last) -- outstream << *pt++ << ", "; -- outstream << *last << std::endl; -- } -- return outstream; -- } -- -- template -- std::ostream& operator << (std::ostream& outstream, const Paths& paths) -- { -- for (auto p : paths) -- outstream << p; -- return outstream; -- } -+ // template -+ // std::ostream& operator << (std::ostream& outstream, const Path& path) -+ // { -+ // if (!path.empty()) -+ // { -+ // auto pt = path.cbegin(), last = path.cend() - 1; -+ // while (pt != last) -+ // outstream << *pt++ << ", "; -+ // outstream << *last << std::endl; -+ // } -+ // return outstream; -+ // } -+ -+ // template -+ // std::ostream& operator << (std::ostream& outstream, const Paths& paths) -+ // { -+ // for (auto p : paths) -+ // outstream << p; -+ // return outstream; -+ // } - - - template -diff --git CPP/Clipper2Lib/include/clipper2/clipper.h CPP/Clipper2Lib/include/clipper2/clipper.h -index fe1e299..aff9f60 100644 ---- CPP/Clipper2Lib/include/clipper2/clipper.h -+++ CPP/Clipper2Lib/include/clipper2/clipper.h -@@ -309,35 +309,35 @@ namespace Clipper2Lib { - return true; - } - -- static void OutlinePolyPath(std::ostream& os, -- size_t idx, bool isHole, size_t count, const std::string& preamble) -- { -- std::string plural = (count == 1) ? "." : "s."; -- if (isHole) -- os << preamble << "+- Hole (" << idx << ") contains " << count << -- " nested polygon" << plural << std::endl; -- else -- os << preamble << "+- Polygon (" << idx << ") contains " << count << -- " hole" << plural << std::endl; -- } -- -- static void OutlinePolyPath64(std::ostream& os, const PolyPath64& pp, -- size_t idx, std::string preamble) -- { -- OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); -- for (size_t i = 0; i < pp.Count(); ++i) -- if (pp.Child(i)->Count()) -- details::OutlinePolyPath64(os, *pp.Child(i), i, preamble + " "); -- } -- -- static void OutlinePolyPathD(std::ostream& os, const PolyPathD& pp, -- size_t idx, std::string preamble) -- { -- OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); -- for (size_t i = 0; i < pp.Count(); ++i) -- if (pp.Child(i)->Count()) -- details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " "); -- } -+ // static void OutlinePolyPath(std::ostream& os, -+ // size_t idx, bool isHole, size_t count, const std::string& preamble) -+ // { -+ // std::string plural = (count == 1) ? "." : "s."; -+ // if (isHole) -+ // os << preamble << "+- Hole (" << idx << ") contains " << count << -+ // " nested polygon" << plural << std::endl; -+ // else -+ // os << preamble << "+- Polygon (" << idx << ") contains " << count << -+ // " hole" << plural << std::endl; -+ // } -+ -+ // static void OutlinePolyPath64(std::ostream& os, const PolyPath64& pp, -+ // size_t idx, std::string preamble) -+ // { -+ // OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); -+ // for (size_t i = 0; i < pp.Count(); ++i) -+ // if (pp.Child(i)->Count()) -+ // details::OutlinePolyPath64(os, *pp.Child(i), i, preamble + " "); -+ // } -+ -+ // static void OutlinePolyPathD(std::ostream& os, const PolyPathD& pp, -+ // size_t idx, std::string preamble) -+ // { -+ // OutlinePolyPath(os, idx, pp.IsHole(), pp.Count(), preamble); -+ // for (size_t i = 0; i < pp.Count(); ++i) -+ // if (pp.Child(i)->Count()) -+ // details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " "); -+ // } - - template - inline constexpr void MakePathGeneric(const T an_array, -@@ -377,28 +377,28 @@ namespace Clipper2Lib { - - } // end details namespace - -- inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) -- { -- std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; -- os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; -- for (size_t i = 0; i < pp.Count(); ++i) -- if (pp.Child(i)->Count()) -- details::OutlinePolyPath64(os, *pp.Child(i), i, " "); -- os << std::endl << std::endl; -- return os; -- } -- -- inline std::ostream& operator<< (std::ostream& os, const PolyTreeD& pp) -- { -- std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; -- os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; -- for (size_t i = 0; i < pp.Count(); ++i) -- if (pp.Child(i)->Count()) -- details::OutlinePolyPathD(os, *pp.Child(i), i, " "); -- os << std::endl << std::endl; -- if (!pp.Level()) os << std::endl; -- return os; -- } -+ // inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) -+ // { -+ // std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; -+ // os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; -+ // for (size_t i = 0; i < pp.Count(); ++i) -+ // if (pp.Child(i)->Count()) -+ // details::OutlinePolyPath64(os, *pp.Child(i), i, " "); -+ // os << std::endl << std::endl; -+ // return os; -+ // } -+ -+ // inline std::ostream& operator<< (std::ostream& os, const PolyTreeD& pp) -+ // { -+ // std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; -+ // os << std::endl << "Polytree with " << pp.Count() << plural << std::endl; -+ // for (size_t i = 0; i < pp.Count(); ++i) -+ // if (pp.Child(i)->Count()) -+ // details::OutlinePolyPathD(os, *pp.Child(i), i, " "); -+ // os << std::endl << std::endl; -+ // if (!pp.Level()) os << std::endl; -+ // return os; -+ // } - - inline Paths64 PolyTreeToPaths64(const PolyTree64& polytree) - { diff --git a/cmake/manifold-patches/0001-manifold-no-iostream.patch b/cmake/manifold-patches/0001-manifold-no-iostream.patch new file mode 100644 index 0000000..be928f9 --- /dev/null +++ b/cmake/manifold-patches/0001-manifold-no-iostream.patch @@ -0,0 +1,784 @@ +Carry-patch: vendored verbatim diff of elalish/manifold#1690 against +manifold master, providing a `MANIFOLD_NO_IOSTREAM` build option that +strips iostream- and filesystem-using bits from the public API and +tests, plus a tracking patch for AngusJohnson/Clipper2#1094 that +adds the matching `CLIPPER2_NO_IOSTREAM` macro guards in Clipper2. + +When MANIFOLD_NO_IOSTREAM=ON, manifold defines MANIFOLD_NO_IOSTREAM +and MANIFOLD_NO_FILESYSTEM as PUBLIC compile defs on the manifold +target, and sets CLIPPER2_NO_IOSTREAM=ON so the bundled Clipper2 +inherits the same. + +Generated against manifold SHA 5f95a3ac0e906f596bb2d27a52d005ef60de58f3 +(upstream/master HEAD at the time of vendoring). Drops once #1690 +lands and the shim's manifold pin moves past it. + +Apply with: git apply --ignore-whitespace --whitespace=nowarn + 0001-manifold-no-iostream.patch + +diff --git a/.gitattributes b/.gitattributes +new file mode 100644 +index 00000000..888e025b +--- /dev/null ++++ b/.gitattributes +@@ -0,0 +1,4 @@ ++# Carry-patches must keep LF line endings even when checked out on ++# Windows with default core.autocrlf=true; otherwise `git apply` ++# refuses them on EOL mismatch with the (non-CRLF) target tree. ++*.patch text eol=lf +diff --git a/.github/workflows/manifold.yml b/.github/workflows/manifold.yml +index 2df52438..f2603709 100644 +--- a/.github/workflows/manifold.yml ++++ b/.github/workflows/manifold.yml +@@ -175,6 +175,40 @@ jobs: + sudo cmake --install build + ./scripts/test-cmake.sh + ++ build_no_iostream: ++ name: NO_IOSTREAM (clang) ++ timeout-minutes: 20 ++ runs-on: ubuntu-latest ++ steps: ++ - uses: actions/checkout@v6 ++ - name: Install apt packages ++ uses: awalsh128/cache-apt-pkgs-action@v1.6.0 ++ with: ++ packages: cmake clang libgtest-dev ++ - name: Configure + build with MANIFOLD_NO_IOSTREAM=ON, MANIFOLD_TEST=ON ++ run: | ++ cmake \ ++ -DCMAKE_BUILD_TYPE=Release \ ++ -DCMAKE_C_COMPILER=clang \ ++ -DCMAKE_CXX_COMPILER=clang++ \ ++ -DMANIFOLD_NO_IOSTREAM=ON \ ++ -DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \ ++ -DMANIFOLD_STRICT=ON \ ++ -DMANIFOLD_TEST=ON \ ++ -DMANIFOLD_PYBIND=OFF \ ++ . -B build ++ cmake --build build ++ - name: Run manifold_test (iostream-using TESTs are gated out) ++ run: ./build/test/manifold_test ++ - name: Verify conflict guard fires (NO_IOSTREAM + DEBUG) ++ run: | ++ if cmake -DMANIFOLD_NO_IOSTREAM=ON -DMANIFOLD_DEBUG=ON \ ++ -DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \ ++ . -B /tmp/conflict-debug 2>&1; then ++ echo "FAIL: configure should have failed for NO_IOSTREAM + DEBUG" ++ exit 1 ++ fi ++ + build_gcc_codecov: + name: code coverage + timeout-minutes: 45 +diff --git a/CMakeLists.txt b/CMakeLists.txt +index e83223eb..1b543ffb 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -42,6 +42,11 @@ option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF) + option(MANIFOLD_ASSERT "Enable assertions - requires MANIFOLD_DEBUG" OFF) + option(MANIFOLD_TIMING "Enable Boolean3 phase timing without debug overhead" OFF) + option(MANIFOLD_STRICT "Treat compile warnings as fatal build errors" OFF) ++option( ++ MANIFOLD_NO_IOSTREAM ++ "Strip iostream- and filesystem-using bits from the public API and tests; useful for freestanding/embedded builds. When ON, defines both MANIFOLD_NO_IOSTREAM and MANIFOLD_NO_FILESYSTEM." ++ OFF ++) + option( + MANIFOLD_DOWNLOADS + "Allow Manifold build to download missing dependencies" +@@ -100,6 +105,19 @@ mark_as_advanced(TRACY_MEMORY_USAGE) + mark_as_advanced(MANIFOLD_FUZZ) + mark_as_advanced(ASSIMP_ENABLE) + ++# MANIFOLD_NO_IOSTREAM strips iostream- and filesystem-using bits ++# from the public API and from the test files that use them. ++# Incompatible with options that emit diagnostic output via iostream: ++# MANIFOLD_DEBUG and MANIFOLD_TIMING (both use std::cout). ++# MANIFOLD_TEST=ON is supported — iostream-using TEST(...) blocks ++# in manifold_test/polygon_test/manifoldc_test are gated out under ++# the macro; the test executable still builds and runs the rest. ++if(MANIFOLD_NO_IOSTREAM AND (MANIFOLD_DEBUG OR MANIFOLD_TIMING)) ++ message(FATAL_ERROR ++ "MANIFOLD_NO_IOSTREAM is incompatible with MANIFOLD_DEBUG / MANIFOLD_TIMING " ++ "(those options use std::cout for diagnostic output).") ++endif() ++ + # Always build position independent code for relocatability + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +diff --git a/README.md b/README.md +index e6d33464..5f6ddd8b 100644 +--- a/README.md ++++ b/README.md +@@ -124,6 +124,14 @@ CMake flags (usage e.g. `-DMANIFOLD_DEBUG=ON`): + See profiling section below. + - `ASSIMP_ENABLE=[, ON]`: Enable integration with assimp, which is needed for some of the utilities in `extras`. + - `MANIFOLD_STRICT=[, ON]`: Treat compile warnings as fatal build errors. ++- `MANIFOLD_NO_IOSTREAM=[, ON]`: Strip iostream- and filesystem-using ++ bits from the public API and tests; useful for freestanding/embedded ++ builds (e.g., `wasm32-unknown-unknown`). Defines both ++ `MANIFOLD_NO_IOSTREAM` and `MANIFOLD_NO_FILESYSTEM` as PUBLIC compile ++ definitions. The test suite still builds + runs — iostream-using ++ TESTs in `manifold_test`/`polygon_test`/`manifoldc_test` are gated ++ out under the macro. Incompatible with `MANIFOLD_DEBUG` / ++ `MANIFOLD_TIMING` (which use `std::cout` for diagnostic output). + + Dependency version override: + - `MANIFOLD_USE_BUILTIN_TBB=[, ON]`: Use builtin version of tbb. +diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt +index 66158cc6..1641f636 100644 +--- a/bindings/c/CMakeLists.txt ++++ b/bindings/c/CMakeLists.txt +@@ -43,3 +43,14 @@ install( + FILES include/manifold/manifoldc.h include/manifold/types.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/manifold + ) ++ ++# Always-on build-time check: compiles manifold/manifoldc.h with ++# MANIFOLD_NO_IOSTREAM defined. Mirrors src/no_iostream_check.cpp's ++# role for the C++ public header. ++add_library(manifoldc_no_iostream_check OBJECT no_iostream_check.cpp) ++target_link_libraries(manifoldc_no_iostream_check PRIVATE manifoldc) ++if(NOT MANIFOLD_NO_IOSTREAM) ++ target_compile_definitions( ++ manifoldc_no_iostream_check PRIVATE MANIFOLD_NO_IOSTREAM ++ ) ++endif() +diff --git a/bindings/c/include/manifold/manifoldc.h b/bindings/c/include/manifold/manifoldc.h +index 608f6eb1..08ee60d2 100644 +--- a/bindings/c/include/manifold/manifoldc.h ++++ b/bindings/c/include/manifold/manifoldc.h +@@ -542,6 +542,7 @@ void manifold_delete_rect(ManifoldRect* b); + void manifold_delete_triangulation(ManifoldTriangulation* m); + void manifold_delete_execution_context(ManifoldExecutionContext* ctx); + ++#ifndef MANIFOLD_NO_IOSTREAM + // MeshIO / Export + + // Import a manifold from a Wavefront obj file. +@@ -570,6 +571,7 @@ void manifold_write_obj(ManifoldManifold* manifold, + // passing additional data into the callback. + void manifold_meshgl64_write_obj(ManifoldMeshGL64* mesh, + void (*callback)(char*, void*), void* args); ++#endif + #ifdef __cplusplus + } + #endif +diff --git a/bindings/c/manifoldc.cpp b/bindings/c/manifoldc.cpp +index f411de7b..3e6d3fd7 100644 +--- a/bindings/c/manifoldc.cpp ++++ b/bindings/c/manifoldc.cpp +@@ -14,7 +14,9 @@ + + #include "manifold/manifoldc.h" + ++#ifndef MANIFOLD_NO_IOSTREAM + #include ++#endif + #include + + #include "conv.h" +@@ -1048,6 +1050,7 @@ void manifold_destruct_execution_context(ManifoldExecutionContext* ctx) { + + // IO + ++#ifndef MANIFOLD_NO_IOSTREAM + ManifoldManifold* manifold_read_obj(void* mem, char* obj_file) { + std::istringstream iss(obj_file); + Manifold m = Manifold::ReadOBJ(iss); +@@ -1075,6 +1078,7 @@ void manifold_meshgl64_write_obj(ManifoldMeshGL64* mesh, + WriteOBJ(ss, *m); + callback(ss.str().data(), args); + } ++#endif + + #ifdef __cplusplus + } +diff --git a/bindings/c/no_iostream_check.cpp b/bindings/c/no_iostream_check.cpp +new file mode 100644 +index 00000000..455c39e0 +--- /dev/null ++++ b/bindings/c/no_iostream_check.cpp +@@ -0,0 +1,19 @@ ++// Copyright 2026 The Manifold Authors. ++// ++// Licensed under the Apache License, Version 2.0 (the "License"); ++// you may not use this file except in compliance with the License. ++// You may obtain a copy of the License at ++// ++// https://www.apache.org/licenses/LICENSE-2.0 ++// ++// Unless required by applicable law or agreed to in writing, software ++// distributed under the License is distributed on an "AS IS" BASIS, ++// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++// See the License for the specific language governing permissions and ++// limitations under the License. ++// ++// Build-time check: compiles manifold/manifoldc.h with MANIFOLD_NO_IOSTREAM ++// defined. Fails the build if iostream usage creeps into the C-API ++// header outside the macro guards. ++ ++#include "manifold/manifoldc.h" +diff --git a/cmake/manifoldDeps.cmake b/cmake/manifoldDeps.cmake +index bf51371a..78ba8f07 100644 +--- a/cmake/manifoldDeps.cmake ++++ b/cmake/manifoldDeps.cmake +@@ -119,6 +119,22 @@ if(MANIFOLD_CROSS_SECTION) + CACHE BOOL + "Preempt cache default of USINGZ (we only use 2d)" + ) ++ # When manifold is built with MANIFOLD_NO_IOSTREAM, also strip ++ # iostream from the bundled Clipper2 — manifold doesn't call any ++ # of Clipper2's stream operators internally, so passing this ++ # through is safe regardless. The CLIPPER2_NO_IOSTREAM macro is ++ # added by the carry-patch below; once Clipper2#1094 lands and ++ # the SHA pin moves past it, the patch drops and the option is ++ # honored natively. ++ if(MANIFOLD_NO_IOSTREAM) ++ set( ++ CLIPPER2_NO_IOSTREAM ++ ON ++ CACHE BOOL ++ "Strip iostream-using overloads from Clipper2 (set by manifold when MANIFOLD_NO_IOSTREAM=ON)" ++ FORCE ++ ) ++ endif() + FetchContent_Declare( + Clipper2 + GIT_REPOSITORY https://github.com/AngusJohnson/Clipper2.git +@@ -127,6 +143,20 @@ if(MANIFOLD_CROSS_SECTION) + GIT_PROGRESS TRUE + SOURCE_SUBDIR + CPP ++ # Disable Windows autocrlf on the clone so the carry-patch (which ++ # is LF-only) applies cleanly. Default core.autocrlf=true on ++ # Windows would convert all LFs to CRLFs in the working tree, and ++ # `git apply` then fails on the line-ending mismatch. ++ GIT_CONFIG ++ core.autocrlf=false ++ # Carry-patch: tracks AngusJohnson/Clipper2#1094 (CLIPPER2_NO_IOSTREAM ++ # macro guards). Drops once that PR lands and the SHA pin moves past ++ # it. Applied via wrapper script so re-configures (which re-trigger ++ # PATCH_COMMAND) don't fail when the patch is already applied. ++ PATCH_COMMAND ${CMAKE_COMMAND} ++ -DPATCH_FILE=${CMAKE_CURRENT_LIST_DIR}/patches/0001-clipper2-no-iostream.patch ++ -DSOURCE_DIR= ++ -P ${CMAKE_CURRENT_LIST_DIR}/patches/apply-clipper2-patch.cmake + ) + FetchContent_MakeAvailable(Clipper2) + set_property( +diff --git a/cmake/patches/0001-clipper2-no-iostream.patch b/cmake/patches/0001-clipper2-no-iostream.patch +new file mode 100644 +index 00000000..4f4c7819 +--- /dev/null ++++ b/cmake/patches/0001-clipper2-no-iostream.patch +@@ -0,0 +1,153 @@ ++Carry-patch: add a CLIPPER2_NO_IOSTREAM build option (off by default) ++that strips iostream-using overloads from Clipper2's public headers, ++and propagate the macro through Clipper2's PUBLIC compile definitions ++so consumers see it. Lets manifold pass MANIFOLD_NO_IOSTREAM=ON ++through to the bundled Clipper2 (e.g., for wasm32-unknown-unknown ++consumers via the wasm-cxx-shim integration). ++ ++Tracks AngusJohnson/Clipper2#1094 — once that lands and manifold's ++Clipper2 SHA pin moves past it, this patch drops. ++ ++Generated against Clipper2 SHA 46f639177fe418f9689e8ddb74f08a870c71f5b4 ++(the SHA manifoldDeps.cmake currently pins). The headers at this SHA ++are byte-identical to upstream main, so the patch tracks #1094's diff ++exactly. ++ ++Apply with: git apply -p1 0001-clipper2-no-iostream.patch ++ ++ ++diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt ++index 7642e86..b327296 100644 ++--- a/CPP/CMakeLists.txt +++++ b/CPP/CMakeLists.txt ++@@ -17,6 +17,7 @@ option(CLIPPER2_HI_PRECISION "Caution: enabling this will compromise performance ++ option(CLIPPER2_UTILS "Build utilities" ON) ++ option(CLIPPER2_EXAMPLES "Build examples" ON) ++ option(CLIPPER2_TESTS "Build tests" ON) +++option(CLIPPER2_NO_IOSTREAM "Disable iostream-using header overloads" OFF) ++ option(USE_EXTERNAL_GTEST "Use system-wide installed GoogleTest" OFF) ++ option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" OFF) ++ option(BUILD_SHARED_LIBS "Build shared libs" OFF) ++@@ -69,6 +70,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY")) ++ Clipper2 PUBLIC ++ CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} ++ $<$:CLIPPER2_HI_PRECISION> +++ $<$:CLIPPER2_NO_IOSTREAM> ++ ) ++ ++ target_include_directories( ++@@ -95,6 +97,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "OFF")) ++ USINGZ ++ CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION} ++ $<$:CLIPPER2_HI_PRECISION> +++ $<$:CLIPPER2_NO_IOSTREAM> ++ ) ++ target_include_directories( ++ Clipper2Z PUBLIC ++diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h ++index 99a5205..d3af0a0 100644 ++--- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h +++++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h ++@@ -14,7 +14,9 @@ ++ #include ++ #include ++ #include +++#ifndef CLIPPER2_NO_IOSTREAM ++ #include +++#endif ++ #include ++ #include ++ #include ++@@ -166,11 +168,13 @@ namespace Clipper2Lib ++ ++ void SetZ(const z_type z_value) { z = z_value; } ++ +++#ifndef CLIPPER2_NO_IOSTREAM ++ friend std::ostream& operator<<(std::ostream& os, const Point& point) ++ { ++ os << point.x << "," << point.y << "," << point.z; ++ return os; ++ } +++#endif ++ ++ #else ++ ++@@ -203,11 +207,13 @@ namespace Clipper2Lib ++ return Point(x * scale, y * scale); ++ } ++ +++#ifndef CLIPPER2_NO_IOSTREAM ++ friend std::ostream& operator<<(std::ostream& os, const Point& point) ++ { ++ os << point.x << "," << point.y; ++ return os; ++ } +++#endif ++ #endif ++ ++ friend bool operator==(const Point& a, const Point& b) ++@@ -396,10 +402,12 @@ namespace Clipper2Lib ++ return result; ++ } ++ +++#ifndef CLIPPER2_NO_IOSTREAM ++ friend std::ostream& operator<<(std::ostream& os, const Rect& rect) { ++ os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") "; ++ return os; ++ } +++#endif ++ }; ++ ++ template ++@@ -498,6 +506,7 @@ namespace Clipper2Lib ++ return Rect(xmin, ymin, xmax, ymax); ++ } ++ +++#ifndef CLIPPER2_NO_IOSTREAM ++ template ++ std::ostream& operator << (std::ostream& outstream, const Path& path) ++ { ++@@ -518,6 +527,7 @@ namespace Clipper2Lib ++ outstream << p; ++ return outstream; ++ } +++#endif ++ ++ ++ template ++diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h ++index fe1e299..deb1f7b 100644 ++--- a/CPP/Clipper2Lib/include/clipper2/clipper.h +++++ b/CPP/Clipper2Lib/include/clipper2/clipper.h ++@@ -309,6 +309,7 @@ namespace Clipper2Lib { ++ return true; ++ } ++ +++#ifndef CLIPPER2_NO_IOSTREAM ++ static void OutlinePolyPath(std::ostream& os, ++ size_t idx, bool isHole, size_t count, const std::string& preamble) ++ { ++@@ -338,6 +339,7 @@ namespace Clipper2Lib { ++ if (pp.Child(i)->Count()) ++ details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " "); ++ } +++#endif ++ ++ template ++ inline constexpr void MakePathGeneric(const T an_array, ++@@ -377,6 +379,7 @@ namespace Clipper2Lib { ++ ++ } // end details namespace ++ +++#ifndef CLIPPER2_NO_IOSTREAM ++ inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp) ++ { ++ std::string plural = (pp.Count() == 1) ? " polygon." : " polygons."; ++@@ -399,6 +402,7 @@ namespace Clipper2Lib { ++ if (!pp.Level()) os << std::endl; ++ return os; ++ } +++#endif ++ ++ inline Paths64 PolyTreeToPaths64(const PolyTree64& polytree) ++ { +diff --git a/cmake/patches/apply-clipper2-patch.cmake b/cmake/patches/apply-clipper2-patch.cmake +new file mode 100644 +index 00000000..31518fcd +--- /dev/null ++++ b/cmake/patches/apply-clipper2-patch.cmake +@@ -0,0 +1,32 @@ ++# Idempotent application of the Clipper2 carry-patch. Runs as the ++# PATCH_COMMAND of FetchContent_Declare(Clipper2 ...). Re-runs of ++# `cmake` (e.g., when CMakeLists.txt files in the parent project ++# are edited) reinvoke the patch step; without this idempotency ++# wrapper, `git apply` fails on already-patched files. ++# ++# Invoked via: cmake -DPATCH_FILE= -DSOURCE_DIR= -P this.cmake ++ ++if(NOT PATCH_FILE OR NOT SOURCE_DIR) ++ message(FATAL_ERROR "PATCH_FILE and SOURCE_DIR are required") ++endif() ++ ++# `git apply --reverse --check` succeeds iff the patch is already applied. ++execute_process( ++ COMMAND git apply --reverse --check --ignore-whitespace --whitespace=nowarn "${PATCH_FILE}" ++ WORKING_DIRECTORY "${SOURCE_DIR}" ++ RESULT_VARIABLE _already_applied ++ OUTPUT_QUIET ++ ERROR_QUIET ++) ++if(_already_applied EQUAL 0) ++ return() ++endif() ++ ++execute_process( ++ COMMAND git apply --ignore-whitespace --whitespace=nowarn "${PATCH_FILE}" ++ WORKING_DIRECTORY "${SOURCE_DIR}" ++ RESULT_VARIABLE _apply_result ++) ++if(NOT _apply_result EQUAL 0) ++ message(FATAL_ERROR "Failed to apply Clipper2 carry-patch: ${PATCH_FILE}") ++endif() +diff --git a/include/manifold/manifold.h b/include/manifold/manifold.h +index 20f7c0c0..80c49765 100644 +--- a/include/manifold/manifold.h ++++ b/include/manifold/manifold.h +@@ -273,8 +273,10 @@ using MeshGL = MeshGLP; + */ + using MeshGL64 = MeshGLP; + ++#ifndef MANIFOLD_NO_IOSTREAM + MeshGL64 ReadOBJ(std::istream& stream); + bool WriteOBJ(std::ostream& stream, const MeshGL64& mesh); ++#endif + + /** + * @brief This library's internal representation of an oriented, 2-manifold, +@@ -517,8 +519,10 @@ class Manifold { + * ofile.close(); + * @endcode + */ ++#ifndef MANIFOLD_NO_IOSTREAM + static Manifold ReadOBJ(std::istream& stream); + bool WriteOBJ(std::ostream& stream) const; ++#endif + + /** @name Testing Hooks + * These are just for internal testing. +diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt +index d9bf8963..67cdecb4 100644 +--- a/src/CMakeLists.txt ++++ b/src/CMakeLists.txt +@@ -80,6 +80,7 @@ set( + MANIFOLD_ASSERT + MANIFOLD_TIMING + MANIFOLD_CROSS_SECTION ++ MANIFOLD_NO_IOSTREAM + TRACY_ENABLE + TRACY_MEMORY_USAGE + ) +@@ -94,6 +95,30 @@ else() + target_compile_definitions(manifold PUBLIC MANIFOLD_PAR=-1) + endif() + ++# MANIFOLD_NO_IOSTREAM also defines MANIFOLD_NO_FILESYSTEM (the two ++# track together in practice — `` paths are only useful ++# if you can `` them, which needs iostream). Source code ++# uses the macros separately for semantic clarity (NO_IOSTREAM gates ++# the iostream-using public API; NO_FILESYSTEM gates filesystem use ++# in test_main.cpp's fixture helpers + main()). ++if(MANIFOLD_NO_IOSTREAM) ++ target_compile_definitions(manifold PUBLIC MANIFOLD_NO_FILESYSTEM) ++endif() ++ ++# Always-on build-time check: compiles manifold/manifold.h with ++# MANIFOLD_NO_IOSTREAM defined. Fails if iostream usage creeps into ++# the public header outside the macro guards. Independent of the ++# MANIFOLD_NO_IOSTREAM option's value (just inherits manifold's other ++# PUBLIC defines via target_link_libraries; sets the macro PRIVATE ++# only when the option isn't already setting it PUBLIC). ++add_library(manifold_no_iostream_check OBJECT no_iostream_check.cpp) ++target_link_libraries(manifold_no_iostream_check PRIVATE manifold) ++if(NOT MANIFOLD_NO_IOSTREAM) ++ target_compile_definitions( ++ manifold_no_iostream_check PRIVATE MANIFOLD_NO_IOSTREAM ++ ) ++endif() ++ + target_include_directories( + manifold + PUBLIC +diff --git a/src/impl.cpp b/src/impl.cpp +index ce4a2090..0dfb96a0 100644 +--- a/src/impl.cpp ++++ b/src/impl.cpp +@@ -19,11 +19,15 @@ + #include + #include + #include ++#ifndef MANIFOLD_NO_IOSTREAM + #include ++#endif + #include + #include + #include ++#ifndef MANIFOLD_NO_IOSTREAM + #include ++#endif + + #include "csg_tree.h" + #include "disjoint_sets.h" +@@ -61,6 +65,7 @@ int GetLabels(std::vector& components, + return uf.connectedComponents(components); + } + ++#ifndef MANIFOLD_NO_IOSTREAM + template + double FromChars(T buffer) { + double tmp; +@@ -69,6 +74,7 @@ double FromChars(T buffer) { + iss >> tmp; + return tmp; + } ++#endif + } // namespace + + namespace manifold { +@@ -699,6 +705,7 @@ void Manifold::Impl::IncrementMeshIDs() { + UpdateMeshID({meshIDold2new.D()})); + } + ++#ifndef MANIFOLD_NO_IOSTREAM + static std::ostream& WriteOBJWithEpsilon(std::ostream& stream, + const MeshGL64& mesh, + std::optional epsilon) { +@@ -860,4 +867,5 @@ bool Manifold::WriteOBJ(std::ostream& stream) const { + stream << *this->GetCsgLeafNode().GetImpl(); + return true; + } ++#endif + } // namespace manifold +diff --git a/src/no_iostream_check.cpp b/src/no_iostream_check.cpp +new file mode 100644 +index 00000000..0f0e4f69 +--- /dev/null ++++ b/src/no_iostream_check.cpp +@@ -0,0 +1,24 @@ ++// Copyright 2026 The Manifold Authors. ++// ++// Licensed under the Apache License, Version 2.0 (the "License"); ++// you may not use this file except in compliance with the License. ++// You may obtain a copy of the License at ++// ++// https://www.apache.org/licenses/LICENSE-2.0 ++// ++// Unless required by applicable law or agreed to in writing, software ++// distributed under the License is distributed on an "AS IS" BASIS, ++// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++// See the License for the specific language governing permissions and ++// limitations under the License. ++// ++// Build-time check: compiles manifold's public header with ++// MANIFOLD_NO_IOSTREAM defined. Fails the build if iostream usage ++// creeps into the public header outside the macro guards. ++ ++#include "manifold/manifold.h" ++ ++namespace { ++manifold::MeshGL64 _force_meshgl64; ++manifold::Manifold _force_manifold; ++} // namespace +diff --git a/test/boolean_complex_test.cpp b/test/boolean_complex_test.cpp +index 2b83f0a5..b8787027 100644 +--- a/test/boolean_complex_test.cpp ++++ b/test/boolean_complex_test.cpp +@@ -1468,6 +1468,7 @@ TEST(BooleanComplex, Ring) { + EXPECT_EQ(result.Status(), Manifold::Error::NoError); + } + ++#ifndef MANIFOLD_NO_FILESYSTEM + TEST(BooleanComplex, SelfIntersect) { + ManifoldParamGuard guard; + manifold::ManifoldParams().processOverlaps = true; +@@ -1518,6 +1519,7 @@ TEST(BooleanComplex, HullMask) { + Manifold ret = body - mask; + MeshGL mesh = ret.GetMeshGL(); + } ++#endif + + TEST(BooleanComplex, LazyCollider) { + Manifold ele1 = Manifold::Cylinder(50, 50); +diff --git a/test/manifold_test.cpp b/test/manifold_test.cpp +index e8c21be3..8ae2909f 100644 +--- a/test/manifold_test.cpp ++++ b/test/manifold_test.cpp +@@ -334,6 +334,7 @@ TEST(Manifold, ErrorPropagationSimplify) { + EXPECT_EQ(errored.Simplify().Status(), Manifold::Error::NonFiniteVertex); + } + ++#ifndef MANIFOLD_NO_IOSTREAM + TEST(Manifold, ObjRoundTrip) { + Manifold m = Manifold::Cube(); + std::stringstream ss; +@@ -343,6 +344,7 @@ TEST(Manifold, ObjRoundTrip) { + EXPECT_EQ(m2.Status(), Manifold::Error::NoError); + EXPECT_EQ(m2.Volume(), 1); + } ++#endif + + TEST(Manifold, OppositeFace) { + MeshGL gl; +diff --git a/test/manifoldc_test.cpp b/test/manifoldc_test.cpp +index e84eeda6..3a27be2f 100644 +--- a/test/manifoldc_test.cpp ++++ b/test/manifoldc_test.cpp +@@ -1,7 +1,9 @@ + #include "manifold/manifoldc.h" + + #include ++#ifndef MANIFOLD_NO_IOSTREAM + #include ++#endif + + #include "gtest/gtest.h" + #include "manifold/types.h" +@@ -140,6 +142,7 @@ TEST(CBIND, include_pt_mutates_bounds) { + free(box); + } + ++#ifndef MANIFOLD_NO_IOSTREAM + TEST(CBIND, obj_round_trip) { + ManifoldManifold* cube = + manifold_cube(alloc_manifold_buffer(), 1.0, 1.0, 1.0, 1); +@@ -162,6 +165,7 @@ TEST(CBIND, obj_round_trip) { + free(result); + free(buffer); + } ++#endif + + TEST(CBIND, level_set) { + // can't convert lambda with captures to funptr +@@ -205,6 +209,7 @@ TEST(CBIND, level_set) { + ManifoldMeshGL* sdf_mesh = + manifold_get_meshgl(alloc_meshgl_buffer(), sdf_man); + ++#ifndef MANIFOLD_NO_IOSTREAM + if (options.exportModels) { + manifold_write_obj( + sdf_man, +@@ -215,6 +220,7 @@ TEST(CBIND, level_set) { + }, + NULL); + } ++#endif + + EXPECT_EQ(manifold_status(sdf_man), MANIFOLD_NO_ERROR); + EXPECT_EQ(manifold_status(sdf_man_context), MANIFOLD_NO_ERROR); +diff --git a/test/polygon_test.cpp b/test/polygon_test.cpp +index eaf62a27..2e1824fb 100644 +--- a/test/polygon_test.cpp ++++ b/test/polygon_test.cpp +@@ -15,7 +15,9 @@ + #include "manifold/polygon.h" + + #include ++#ifndef MANIFOLD_NO_IOSTREAM + #include ++#endif + #include + + #include "test.h" +@@ -79,6 +81,7 @@ class PolygonTestFixture : public testing::Test { + void TestBody() { TestPoly(polys, expectedNumTri, epsilon); } + }; + ++#ifndef MANIFOLD_NO_IOSTREAM + void RegisterPolygonTestsFile(const std::string& filename) { + auto f = std::ifstream(filename); + EXPECT_TRUE(f.is_open()); +@@ -117,8 +120,10 @@ void RegisterPolygonTestsFile(const std::string& filename) { + } + f.close(); + } ++#endif + } // namespace + ++#ifndef MANIFOLD_NO_IOSTREAM + void RegisterPolygonTests() { + std::string files[] = {"polygon_corpus.txt", "sponge.txt", "zebra.txt", + "zebra3.txt"}; +@@ -132,3 +137,9 @@ void RegisterPolygonTests() { + for (auto f : files) RegisterPolygonTestsFile(dir + "/polygons/" + f); + #endif + } ++#else ++// Stub when MANIFOLD_NO_IOSTREAM is set: polygon corpus tests need ++// std::ifstream to load fixtures, so they're skipped. test_main.cpp ++// still calls RegisterPolygonTests(); this stub keeps the link clean. ++void RegisterPolygonTests() {} ++#endif +diff --git a/test/test_main.cpp b/test/test_main.cpp +index 39321a88..5da56b65 100644 +--- a/test/test_main.cpp ++++ b/test/test_main.cpp +@@ -12,8 +12,10 @@ + // See the License for the specific language governing permissions and + // limitations under the License. + #include ++#ifndef MANIFOLD_NO_FILESYSTEM + #include + #include ++#endif + + #include "manifold/manifold.h" + #include "test.h" +@@ -485,6 +487,7 @@ void CheckGLEquiv(const MeshGL& mgl1, const MeshGL& mgl2) { + } + } + ++#ifndef MANIFOLD_NO_FILESYSTEM + Manifold ReadTestOBJ(const std::string& filename) { + return Manifold(ReadTestMeshGL64OBJ(filename)); + } +@@ -511,3 +514,15 @@ void WriteTestOBJ(const std::string& filename, Manifold m) { + WriteOBJ(f, m.GetMeshGL64()); + f.close(); + } ++#else ++// Stub WriteTestOBJ for MANIFOLD_NO_FILESYSTEM builds: lets the ++// `if (options.exportModels) WriteTestOBJ(...)` call sites keep ++// linking. exportModels defaults false, so the runtime path is dead. ++// ++// ReadTestOBJ / ReadTestMeshGL64OBJ are deliberately NOT stubbed — ++// tests that read fixtures depend on real geometry, and a stub ++// returning an empty Manifold would silently make assertions fail ++// by construction. Callers must be compile-time gated; an unguarded ++// caller fails to link, which is the bright signal we want. ++void WriteTestOBJ(const std::string& /*filename*/, Manifold /*m*/) {} ++#endif diff --git a/cmake/manifold-patches/0002-manifold-ifdef-iostream.patch b/cmake/manifold-patches/0002-manifold-ifdef-iostream.patch deleted file mode 100644 index 4ec8539..0000000 --- a/cmake/manifold-patches/0002-manifold-ifdef-iostream.patch +++ /dev/null @@ -1,78 +0,0 @@ -From wasm-cxx-shim Phase-7-B1 work-in-progress. -Wrap manifold's iostream-using OBJ I/O paths in `#ifndef MANIFOLD_NO_IOSTREAM` -so the library compiles when libc++'s localization machinery is disabled -(`_LIBCPP_HAS_LOCALIZATION=0` in the smoke test's `__config_site` override). - -Three blocks: -- src/impl.cpp ~lines 152-159: `FromChars` template helper using - std::istringstream -- src/impl.cpp ~lines 790-918: WriteOBJWithEpsilon, ReadOBJWithEpsilon, - ReadOBJ (×2), WriteOBJ (×2), operator<<(ostream, Manifold::Impl) -- bindings/c/manifoldc.cpp ~lines 916-944: the C-API OBJ I/O bindings - (manifold_read_obj, manifold_meshgl64_read_obj, manifold_write_obj, - manifold_meshgl64_write_obj) — all use std::istringstream / stringstream - -Originally documented in pca006132's manifold-on-wasm experiment: - https://github.com/elalish/manifold/discussions/1046#discussioncomment-11302257 - -Generated against manifold v3.4.1. - -Upstream PR target: elalish/manifold. Cleanest upstream form would -gate the OBJ I/O bits on a `MANIFOLD_NO_IOSTREAM` (or equivalently -named) build option. The patch here uses that macro name for forward -compatibility. - -diff --git bindings/c/manifoldc.cpp bindings/c/manifoldc.cpp -index 1d32ecb..5a80637 100644 ---- bindings/c/manifoldc.cpp -+++ bindings/c/manifoldc.cpp -@@ -913,6 +913,7 @@ void manifold_destruct_triangulation(ManifoldTriangulation* m) { - from_c(m)->~vector(); - } - -+#ifndef MANIFOLD_NO_IOSTREAM - // IO - - ManifoldManifold* manifold_read_obj(void* mem, char* obj_file) { -@@ -942,6 +943,7 @@ void manifold_meshgl64_write_obj(ManifoldMeshGL64* mesh, - WriteOBJ(ss, *m); - callback(ss.str().data(), args); - } -+#endif // !MANIFOLD_NO_IOSTREAM - - #ifdef __cplusplus - } -diff --git src/impl.cpp src/impl.cpp -index 57603c4..3500dac 100644 ---- src/impl.cpp -+++ src/impl.cpp -@@ -149,6 +149,7 @@ int GetLabels(std::vector& components, - return uf.connectedComponents(components); - } - -+#ifndef MANIFOLD_NO_IOSTREAM - template - double FromChars(T buffer) { - double tmp; -@@ -157,6 +158,7 @@ double FromChars(T buffer) { - iss >> tmp; - return tmp; - } -+#endif // !MANIFOLD_NO_IOSTREAM - } // namespace - - namespace manifold { -@@ -787,6 +789,7 @@ void Manifold::Impl::IncrementMeshIDs() { - UpdateMeshID({meshIDold2new.D()})); - } - -+#ifndef MANIFOLD_NO_IOSTREAM - static std::ostream& WriteOBJWithEpsilon(std::ostream& stream, - const MeshGL64& mesh, - std::optional epsilon) { -@@ -916,4 +919,5 @@ bool Manifold::WriteOBJ(std::ostream& stream) const { - stream << *this->GetCsgLeafNode().GetImpl(); - return true; - } -+#endif // !MANIFOLD_NO_IOSTREAM - } // namespace manifold diff --git a/cmake/manifold-patches/0003-manifold-test-main-ifdef-filesystem.patch b/cmake/manifold-patches/0003-manifold-test-main-ifdef-filesystem.patch deleted file mode 100644 index 1e8f8af..0000000 --- a/cmake/manifold-patches/0003-manifold-test-main-ifdef-filesystem.patch +++ /dev/null @@ -1,99 +0,0 @@ -From wasm-cxx-shim Phase-7-B2 work. -Wrap manifold's filesystem-using bits in test/test_main.cpp in -`#ifndef MANIFOLD_NO_FILESYSTEM` so it compiles on wasm32 freestanding -(no , no , no real main()). - -Six blocks: -- includes: + -- print_usage(): uses printf -- main(): uses InitGoogleTest, RegisterPolygonTests, RUN_ALL_TESTS, fprintf, - argv parsing, std::thread::hardware_concurrency. Consumers in - filesystem-less builds provide their own entry point. -- ReadTestOBJ / ReadTestMeshGL64OBJ / WriteTestOBJ: provide trivial - alternate definitions (stub no-ops) when MANIFOLD_NO_FILESYSTEM is set, - so call sites in other test files (e.g. `if (options.exportModels) - WriteTestOBJ(...)` in boolean_test.cpp) link cleanly without needing - per-test-file ifdefs. - -Generated against manifold v3.4.1. - -Upstream PR target: elalish/manifold. Cleanest upstream form would -gate filesystem-using bits behind a `MANIFOLD_NO_FILESYSTEM` (or -equivalently named) build option. The patch here uses that macro name -for forward compatibility. - -diff --git test/test_main.cpp test/test_main.cpp -index f16a9e4..66812a1 100644 ---- test/test_main.cpp -+++ test/test_main.cpp -@@ -12,8 +12,10 @@ - // See the License for the specific language governing permissions and - // limitations under the License. - #include -+#ifndef MANIFOLD_NO_FILESYSTEM - #include - #include -+#endif // !MANIFOLD_NO_FILESYSTEM - - #include "manifold/manifold.h" - #include "test.h" -@@ -34,6 +36,7 @@ using namespace manifold; - - Options options; - -+#ifndef MANIFOLD_NO_FILESYSTEM - void print_usage() { - printf("-------------------------------\n"); - printf("manifold_test specific options:\n"); -@@ -48,7 +51,9 @@ void print_usage() { - "if compiled with MANIFOLD_DEBUG " - "flag)\n"); - } -+#endif // !MANIFOLD_NO_FILESYSTEM - -+#ifndef MANIFOLD_NO_FILESYSTEM - int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - -@@ -109,6 +114,7 @@ int main(int argc, char** argv) { - RegisterPolygonTests(); - return RUN_ALL_TESTS(); - } -+#endif // !MANIFOLD_NO_FILESYSTEM - - Polygons SquareHole(double xOffset) { - Polygons polys; -@@ -478,10 +484,15 @@ void CheckGLEquiv(const MeshGL& mgl1, const MeshGL& mgl2) { - } - } - -+#ifndef MANIFOLD_NO_FILESYSTEM - Manifold ReadTestOBJ(const std::string& filename) { - return Manifold(ReadTestMeshGL64OBJ(filename)); - } -+#else -+Manifold ReadTestOBJ(const std::string& /*filename*/) { return Manifold(); } -+#endif // !MANIFOLD_NO_FILESYSTEM - -+#ifndef MANIFOLD_NO_FILESYSTEM - MeshGL64 ReadTestMeshGL64OBJ(const std::string& filename) { - #ifdef __EMSCRIPTEN__ - std::string obj = "/models/" + filename; -@@ -497,10 +508,17 @@ MeshGL64 ReadTestMeshGL64OBJ(const std::string& filename) { - f.close(); - return a; - } -+#else -+MeshGL64 ReadTestMeshGL64OBJ(const std::string& /*filename*/) { return MeshGL64{}; } -+#endif // !MANIFOLD_NO_FILESYSTEM - -+#ifndef MANIFOLD_NO_FILESYSTEM - void WriteTestOBJ(const std::string& filename, Manifold m) { - std::ofstream f; - f.open(filename); - WriteOBJ(f, m.GetMeshGL64()); - f.close(); - } -+#else -+void WriteTestOBJ(const std::string& /*filename*/, Manifold /*m*/) { /* stub: filesystem disabled */ } -+#endif // !MANIFOLD_NO_FILESYSTEM diff --git a/docs/plan.md b/docs/plan.md index ef14dc4..d06dfa0 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -332,6 +332,16 @@ Status: - **Phase 7-A — DONE**. `manifold-csg` (Rust bindings to manifold) builds + runs on `wasm32-unknown-unknown` against the shim, unblocking `wasm-bindgen` consumers. +- **Phase 7 follow-up — IN-FLIGHT (PR #13, v0.4.0-alpha.1)**. Bumped + the manifold pin to upstream master and consolidated the three + iostream patches into a single vendored diff of + [elalish/manifold#1690](https://github.com/elalish/manifold/pull/1690) + (the upstream PR adding `MANIFOLD_NO_IOSTREAM` natively). Helper + refactor: drops the Clipper2 pre-declare (manifold owns it now) + and lets manifold's option chain propagate the macros as PUBLIC + compile defs. Test coverage 71 → 121. Pre-release while #1690 is + in upstream review; once it merges and the pin moves past it, the + carry-patch drops entirely and v0.4.0 (non-alpha) ships. CI integration (running 7-B1/7-B2 jobs in CI) is a follow-up PR after the merges; the local ctest is green but the heavyweight build diff --git a/test/manifold-link/README.md b/test/manifold-link/README.md index b4f10bf..36326cf 100644 --- a/test/manifold-link/README.md +++ b/test/manifold-link/README.md @@ -32,44 +32,32 @@ repo would defeat the "minimal shim" framing. ## Pinned upstream versions -| Project | Tag | Why | -|---|---|---| -| manifold | `v3.4.1` | Latest stable as of March 2026. | -| Clipper2 | `Clipper2_2.0.1` | What manifold v3.4.1 pulls. | +The shim's `wasm_cxx_shim_add_manifold()` helper bundles a known-good +manifold pin (see `cmake/WasmCxxShimManifold.cmake`'s default +`MANIFOLD_GIT_TAG`). Clipper2's pin is owned by manifold's own +`cmake/manifoldDeps.cmake`; the shim no longer pre-declares it. -Bumping these is a deliberate act tracked in git. When upstream -manifold takes the patches we carry below, drop the patch and bump -the pin. +Each shim release verifies a specific manifold pin in CI; see +`CHANGELOG.md`'s "Tested upstream combinations" table for the +mapping. -## Carry-patches +## Carry-patch -Live under [`cmake/manifold-patches/`](../../cmake/manifold-patches/) +Lives under [`cmake/manifold-patches/`](../../cmake/manifold-patches/) (canonical location — alongside the `wasm_cxx_shim_add_manifold()` -helper that applies them via FetchContent's `PATCH_COMMAND`). Each is -a `git diff --no-prefix`-style file with an upstream-PR-target note in -its header. - -Current patches: - -- `0001-clipper2-strip-iostream.patch` — strip `` references - from Clipper2's headers. Originally from - [pca006132's recipe](https://github.com/elalish/manifold/discussions/1046#discussioncomment-11302257). - Generated against Clipper2 SHA `46f639177...` (the SHA manifold v3.4.1 - pins). Upstream PR: TODO (cleaner form would be a `#ifdef - CLIPPER2_NO_IOSTREAM` guard rather than commented-out blocks). -- `0002-manifold-ifdef-iostream.patch` — wraps manifold's iostream-using - OBJ I/O paths (`FromChars` template, `Read/WriteOBJ*` in `impl.cpp`, - the C-API `manifold_*_obj` bindings in `manifoldc.cpp`) in - `#ifndef MANIFOLD_NO_IOSTREAM`. Generated against manifold v3.4.1. -- `0003-manifold-test-main-ifdef-filesystem.patch` — wraps the - ``/`` includes plus `print_usage()`/`main()` - plus the file-I/O fixture helpers (`Read/WriteTestOBJ*`) in - `test/test_main.cpp` under `#ifndef MANIFOLD_NO_FILESYSTEM`. The - helpers gain trivial stub-alternative bodies so call sites in - individual test files (e.g. `if (options.exportModels) WriteTestOBJ(...)` - in `boolean_test.cpp`) link cleanly. Pulled in by Phase 7-B2 so we - can compile manifold's own `test_main.cpp` directly under the - harness instead of vendoring its helpers. +helper that applies it via FetchContent's `PATCH_COMMAND`): + +- **`0001-manifold-no-iostream.patch`** — verbatim vendored diff of + [elalish/manifold#1690](https://github.com/elalish/manifold/pull/1690) + against the pinned manifold commit. Adds a `MANIFOLD_NO_IOSTREAM` + build option that strips iostream- and filesystem-using bits from + manifold's public API and tests, and propagates the matching + `CLIPPER2_NO_IOSTREAM` macro to the bundled Clipper2 (via a tracking + patch for [AngusJohnson/Clipper2#1094](https://github.com/AngusJohnson/Clipper2/pull/1094) + shipped inside #1690 itself). + +Once #1690 lands and the shim's manifold pin moves past the merge, +this carry-patch drops entirely. ## Stub headers @@ -83,10 +71,10 @@ all mutex operations are trivially correct as no-ops. ## Status — green -manifold v3.4.1's library + C API bindings compile against the shim, -link with **zero unexpected wasm imports**, and the probe runs under -Node returning a sane triangle count for a boolean-union operation. -Two ctest entries cover this: +manifold's library + C API bindings compile against the shim, link +with **zero unexpected wasm imports**, and the probe runs under Node +returning a sane triangle count for a boolean-union operation. Two +ctest entries cover this: - `manifold_link_imports_check` — wasm-objdump assertion that the Import section is absent. @@ -96,25 +84,22 @@ Two ctest entries cover this: What it took to get green: -1. **`MANIFOLD_PAR=OFF`** (not `=-1`; v3.4.1 changed STRING to BOOL). +1. **`MANIFOLD_PAR=OFF`** — manifold's parallel backend is a hard + dependency on TBB/OpenMP we don't ship. 2. **CMake auto-injection fix** in top-level CMakeLists (`CMAKE__IMPLICIT_INCLUDE_DIRECTORIES ""` post-`project()`). Stops CMake from prepending clang's resource dir as `-isystem` before user `-isystem` paths, which was breaking libc++'s `` → `` resolution chain. Broadly useful — not manifold-specific. -3. **`cmake/manifold-patches/0001-clipper2-strip-iostream.patch`** — - strips iostream `operator<<` / `OutlinePolyPath*` from Clipper2 - v2.0.1 headers. -4. **`cmake/manifold-patches/0002-manifold-ifdef-iostream.patch`** — - wraps manifold's OBJ I/O in `#ifndef MANIFOLD_NO_IOSTREAM`. Three - blocks: `FromChars` template, all of `WriteOBJ*`/`ReadOBJ*` in - `impl.cpp`, and the C-API `manifold_*_obj` bindings in - `manifoldc.cpp`. -5. **Stub `include/mutex`** — no-op `std::mutex`/`recursive_mutex`/ +3. **The vendored #1690 carry-patch** described above — gives manifold + a `MANIFOLD_NO_IOSTREAM` build option that propagates through to + `MANIFOLD_NO_FILESYSTEM` and `CLIPPER2_NO_IOSTREAM`, stripping + stream- and filesystem-using bits from the public API + tests. +4. **Stub `include/mutex`** — no-op `std::mutex`/`recursive_mutex`/ `lock_guard`/`scoped_lock`/`unique_lock` because libc++ gates these behind `_LIBCPP_HAS_THREADS`. -6. **`libcxx-extras.cpp`** — provides the libc++ source-file symbols +5. **`libcxx-extras.cpp`** — provides the libc++ source-file symbols that the main libcxx component intentionally doesn't ship: `std::nothrow`, `std::__1::__shared_count::~__shared_count`, `std::__1::__shared_weak_count::~__shared_weak_count` / @@ -122,28 +107,19 @@ What it took to get green: functions, and `std::align`. Scoped to this test, NOT part of the main libcxx component (which stays insulated from `` / `` includes for libc++ version-drift safety). -7. **Clipper2 utilities/tests/examples disabled** via +6. **Clipper2 utilities/tests/examples disabled** via `CLIPPER2_TESTS=OFF`, `CLIPPER2_UTILS=OFF`, `CLIPPER2_EXAMPLES=OFF` — defaults are ON and pull in ``, googletest, etc. we don't ship. ## Upstream PR roadmap -The patches in this directory are carry-patches. Each has a clear -upstream-friendly form documented in its preamble: - -- **Clipper2** patch: file as `#ifndef CLIPPER2_NO_IOSTREAM` guards - upstream against AngusJohnson/Clipper2. -- **Manifold OBJ-I/O** patch (0002): file as `#ifndef MANIFOLD_NO_IOSTREAM` - guards upstream against elalish/manifold. -- **Manifold test-main filesystem** patch (0003): file as - `#ifndef MANIFOLD_NO_FILESYSTEM` guards upstream against - elalish/manifold. The two manifold patches could be combined upstream - under a single `MANIFOLD_WASM_FREESTANDING=ON` CMake option that - defines both macros (and the parallel/exception/RTTI flags) together. - -Once both upstreams take the patches, the carry-patches drop and we -just bump the pinned versions. +The single carry-patch is the verbatim diff of +[elalish/manifold#1690](https://github.com/elalish/manifold/pull/1690), +which itself contains a tracking patch for +[AngusJohnson/Clipper2#1094](https://github.com/AngusJohnson/Clipper2/pull/1094). +Once #1690 lands the patch drops and the shim just bumps the manifold +pin past the merge. ## Opt-in build diff --git a/test/manifold-tests/CMakeLists.txt b/test/manifold-tests/CMakeLists.txt index 19f40b9..13bb7a9 100644 --- a/test/manifold-tests/CMakeLists.txt +++ b/test/manifold-tests/CMakeLists.txt @@ -38,9 +38,23 @@ set(_manifold_src "${manifold_SOURCE_DIR}") set(_manifold_test_files ${_manifold_src}/test/test_main.cpp ${_manifold_src}/test/boolean_test.cpp - ${_manifold_src}/test/sdf_test.cpp + ${_manifold_src}/test/boolean_complex_test.cpp ${_manifold_src}/test/cross_section_test.cpp + ${_manifold_src}/test/manifoldc_test.cpp + ${_manifold_src}/test/sdf_test.cpp + ${_manifold_src}/test/smooth_test.cpp ) +# Skipped: +# * hull_test.cpp, properties_test.cpp, samples_test.cpp — include +# `samples.h` (the manifold/samples helper library), which we'd +# have to pull in along with the sample-geometry sources. +# * manifold_test.cpp — uses std::set + threading constructs without +# direct ``/`` includes; relies on transitive pulls +# from headers our libcxx subset doesn't ship. +# * polygon_test.cpp — TESTs registered dynamically via +# RegisterPolygonTests, which loads from the polygons/ corpus +# directory and is no-op'd under MANIFOLD_NO_IOSTREAM — file would +# compile but contribute zero tests. # Each test source compiles with: # * gtest-shim BEFORE real GoogleTest's include path so @@ -71,10 +85,10 @@ set(_common_compile_flags -isystem ${CMAKE_SOURCE_DIR}/libm/include -isystem ${CMAKE_SOURCE_DIR}/libc/include -I ${_manifold_src}/include + -I ${_manifold_src}/bindings/c/include # for manifold/manifoldc.h -I ${_manifold_src}/test # for test.h -I ${_manifold_src}/src # for src/utils.h -I ${manifold_BINARY_DIR}/include # for generated headers - -I ${clipper2_SOURCE_DIR}/CPP/Clipper2Lib/include ) # Compile each manifold test source. @@ -145,14 +159,16 @@ if(WASM_OBJDUMP_EXECUTABLE) ) endif() -# Size budget. Current: ~915 KB (boolean+SDF+cross_section); budget -# 1.1 MB allows for adding 1-2 more test files of similar shape -# without immediate breakage. A bigger jump (e.g., adding manifold_test.cpp) -# would legitimately blow this budget — bump it then with a why-line. +# Size budget. Current: ~1.07 MB (boolean + boolean_complex + +# cross_section + manifoldc + sdf + smooth, after the bump-manifold to +# the in-flight #1690 carry-patch). Budget 1.4 MB gives ~30% headroom +# for adding 1-2 more test files of similar shape without immediate +# breakage. Bigger jumps (e.g., adding manifold_test.cpp once +# / deps are sorted) bump this with a why-line. add_test(NAME manifold_tests_size_budget COMMAND bash ${CMAKE_SOURCE_DIR}/test/smoke/check-size.sh ${_tests_wasm} - 1153434 + 1468006 ) # Test runner: `node tools/wasm-test-harness/run.mjs `. diff --git a/test/manifold-tests/README.md b/test/manifold-tests/README.md index 4012e78..88aff91 100644 --- a/test/manifold-tests/README.md +++ b/test/manifold-tests/README.md @@ -5,14 +5,17 @@ the shim. Validates end-to-end correctness of the full stack (libc + libm + libcxx + libcxx-extras + manifold + Clipper2) via a real consumer's tests, not just a probe. -Currently runs three test files from manifold v3.4.1 — **71/71 +Currently runs six test files from upstream manifold — **121/121 tests pass**: -| File | Tests | Surface | -|---|---|---| -| `test/boolean_test.cpp` | 47 | 3D Boolean ops (union, difference, intersect, Minkowski, ...) | -| `test/sdf_test.cpp` | 9 | Signed distance fields → marching cubes; libm-heavy | -| `test/cross_section_test.cpp` | 15 | 2D ops via Clipper2 (offset, hull, fill rules, decompose) | +| File | Surface | +|---|---| +| `test/boolean_test.cpp` | 3D Boolean ops (union, difference, intersect, Minkowski, ...) | +| `test/boolean_complex_test.cpp` | Complex Boolean fixtures (gear permutations, sphere/cube unions, ...) | +| `test/cross_section_test.cpp` | 2D ops via Clipper2 (offset, hull, fill rules, decompose) | +| `test/manifoldc_test.cpp` | C-ABI bindings (CSG ops, transforms, level set) | +| `test/sdf_test.cpp` | Signed distance fields → marching cubes; libm-heavy | +| `test/smooth_test.cpp` | Surface smoothing (Csaszar, mirrored, gyroid, ...) | ## What this exercises @@ -69,9 +72,10 @@ Append the manifold test source path to `_manifold_test_files` in set(_manifold_test_files ${_manifold_src}/test/test_main.cpp ${_manifold_src}/test/boolean_test.cpp - ${_manifold_src}/test/sdf_test.cpp + ${_manifold_src}/test/boolean_complex_test.cpp ${_manifold_src}/test/cross_section_test.cpp - ${_manifold_src}/test/manifold_test.cpp # <- add new ones here + # ... existing entries ... + ${_manifold_src}/test/your_new_test.cpp # <- add new ones here ) ``` @@ -85,28 +89,27 @@ Things that may need extending when you do: - **The libcxx-extras object** at `test/manifold-link/libcxx-extras.cpp` — new tests may exercise `` / `` paths that need additional libc++ source-file symbols. -- **The carry-patches** under `test/manifold-link/patches/` — if a new - test file uses `` / `` / threading directly, - either extend `0003-manifold-test-main-ifdef-filesystem.patch` to - cover those bits or write a new patch. Note that test files - depending on the `samples` library (TorusKnot, MengerSponge, etc.) - will need that library wired in too — manifold gates `samples` on - `MANIFOLD_TEST=ON` which we have OFF, so it's not currently built. +- **`samples` library**: tests that include `samples.h` (e.g., + `hull_test.cpp`, `properties_test.cpp`, `samples_test.cpp`) need the + manifold/samples helper library + sample-geometry sources wired in. + Manifold gates `samples` on `MANIFOLD_TEST=ON` which we have OFF, so + it's not currently built. +- **Direct ``/`` use**: tests like `manifold_test.cpp` + reference `std::set` / threading constructs without a direct + `#include ` / ``, relying on transitive pulls from + headers our libcxx subset doesn't ship. Adding such a test means + either patching the test source or extending the libcxx subset. ## Carry-patch dependency -This directory inherits all patches from `test/manifold-link/` via -the shared `FetchContent_Declare(manifold ...)` call. Specifically: - -- `0002-manifold-ifdef-iostream.patch` — strips iostream from - manifold's library code (transitively used by tests). -- `0003-manifold-test-main-ifdef-filesystem.patch` — strips - filesystem-using bits from `test/test_main.cpp`. Without this, - compiling manifold's `test_main.cpp` would need either a real - `` (we don't have one) or hand-vendoring its helpers - (which is what an earlier draft of this directory did). - -Both are documented in `test/manifold-link/README.md`. +This directory inherits the carry-patch applied at the +`test/manifold-link/` level via the shared +`FetchContent_Declare(manifold ...)` call. The single carry-patch +(`cmake/manifold-patches/0001-manifold-no-iostream.patch`) provides +manifold's `MANIFOLD_NO_IOSTREAM` build option, which transitively +strips iostream/filesystem-using bits from manifold's library code, +its `test/test_main.cpp` fixture helpers, and the bundled Clipper2 +headers. Documented in `test/manifold-link/README.md`. ## Build & run @@ -125,7 +128,7 @@ node tools/wasm-test-harness/run.mjs \ build/wasm32/test/manifold-tests/manifold-tests.wasm ``` -Expected output ends with: `wasm-test-harness: 71 passed, 0 failed, 71 total`. +Expected output ends with: `wasm-test-harness: 121 passed, 0 failed, 121 total`. ## Why this exists