Skip to content

Commit e6b030f

Browse files
zmerlynnclaude
andcommitted
Add MANIFOLD_NO_IOSTREAM build-time option
Wraps iostream- and filesystem-using bits of the public API and tests under `#ifndef MANIFOLD_NO_IOSTREAM` / `#ifndef MANIFOLD_NO_FILESYSTEM`. Adds an `option(MANIFOLD_NO_IOSTREAM)` (default OFF) that propagates both macros as PUBLIC compile definitions on the manifold target; consumers' own includes of manifold's headers also see the stripped API. The two macros stay semantically distinct in the source (NO_IOSTREAM gates the iostream-using public API; NO_FILESYSTEM gates `<filesystem>`/`<fstream>` use in test fixtures), but the single user-facing option flips both — `<filesystem>` paths are only useful if you can `<fstream>` them, so they go together in practice. Wraps land in: * include/manifold/manifold.h, bindings/c/include/manifold/manifoldc.h (header decls of ReadOBJ/WriteOBJ + manifold_*_obj C-API) * src/impl.cpp, bindings/c/manifoldc.cpp (impls + sstream/iomanip includes) * test/test_main.cpp (filesystem includes + file-I/O fixture helpers, with stub-alternates so call sites keep linking) * test/manifold_test.cpp, test/polygon_test.cpp, test/manifoldc_test.cpp (per-test/per-block gates for the iostream-using TESTs; polygon corpus tests are skipped via a no-op stub for RegisterPolygonTests) The bundled Clipper2 (when MANIFOLD_USE_BUILTIN_CLIPPER2=ON) gets a corresponding patch (cmake/patches/0001-clipper2-no-iostream.patch) that adds `CLIPPER2_NO_IOSTREAM` macro guards to its public headers — tracking AngusJohnson/Clipper2#1094, drops once that PR lands and the SHA pin moves past it. When MANIFOLD_NO_IOSTREAM=ON, manifold sets CLIPPER2_NO_IOSTREAM=ON for the bundled Clipper2 build. CMake-level guard: MANIFOLD_NO_IOSTREAM=ON is incompatible with MANIFOLD_DEBUG / MANIFOLD_TIMING (those use std::cout for diagnostic output), enforced by FATAL_ERROR. Build-time checks (always-on, regardless of the option's value): * src/no_iostream_check.cpp + manifold_no_iostream_check OBJECT target compiles manifold/manifold.h with the macro defined. * bindings/c/no_iostream_check.cpp + manifoldc_no_iostream_check does the same for the C-API header. CI: new build_no_iostream cell on Ubuntu/clang with MANIFOLD_NO_IOSTREAM=ON + MANIFOLD_USE_BUILTIN_CLIPPER2=ON + MANIFOLD_STRICT=ON. Builds + runs manifold_test (271/272 tests pass — iostream-using TESTs are gated out). Plus a step that verifies the DEBUG conflict guard fires. Discussion + the broader consumer cocktail: elalish#1046 (comment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f95a3a commit e6b030f

17 files changed

Lines changed: 332 additions & 0 deletions

.github/workflows/manifold.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,40 @@ jobs:
175175
sudo cmake --install build
176176
./scripts/test-cmake.sh
177177
178+
build_no_iostream:
179+
name: NO_IOSTREAM (clang)
180+
timeout-minutes: 20
181+
runs-on: ubuntu-latest
182+
steps:
183+
- uses: actions/checkout@v6
184+
- name: Install apt packages
185+
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
186+
with:
187+
packages: cmake clang libgtest-dev
188+
- name: Configure + build with MANIFOLD_NO_IOSTREAM=ON, MANIFOLD_TEST=ON
189+
run: |
190+
cmake \
191+
-DCMAKE_BUILD_TYPE=Release \
192+
-DCMAKE_C_COMPILER=clang \
193+
-DCMAKE_CXX_COMPILER=clang++ \
194+
-DMANIFOLD_NO_IOSTREAM=ON \
195+
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
196+
-DMANIFOLD_STRICT=ON \
197+
-DMANIFOLD_TEST=ON \
198+
-DMANIFOLD_PYBIND=OFF \
199+
. -B build
200+
cmake --build build
201+
- name: Run manifold_test (iostream-using TESTs are gated out)
202+
run: ./build/test/manifold_test
203+
- name: Verify conflict guard fires (NO_IOSTREAM + DEBUG)
204+
run: |
205+
if cmake -DMANIFOLD_NO_IOSTREAM=ON -DMANIFOLD_DEBUG=ON \
206+
-DMANIFOLD_USE_BUILTIN_CLIPPER2=ON \
207+
. -B /tmp/conflict-debug 2>&1; then
208+
echo "FAIL: configure should have failed for NO_IOSTREAM + DEBUG"
209+
exit 1
210+
fi
211+
178212
build_gcc_codecov:
179213
name: code coverage
180214
timeout-minutes: 45

CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
4242
option(MANIFOLD_ASSERT "Enable assertions - requires MANIFOLD_DEBUG" OFF)
4343
option(MANIFOLD_TIMING "Enable Boolean3 phase timing without debug overhead" OFF)
4444
option(MANIFOLD_STRICT "Treat compile warnings as fatal build errors" OFF)
45+
option(
46+
MANIFOLD_NO_IOSTREAM
47+
"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."
48+
OFF
49+
)
4550
option(
4651
MANIFOLD_DOWNLOADS
4752
"Allow Manifold build to download missing dependencies"
@@ -100,6 +105,19 @@ mark_as_advanced(TRACY_MEMORY_USAGE)
100105
mark_as_advanced(MANIFOLD_FUZZ)
101106
mark_as_advanced(ASSIMP_ENABLE)
102107

108+
# MANIFOLD_NO_IOSTREAM strips iostream- and filesystem-using bits
109+
# from the public API and from the test files that use them.
110+
# Incompatible with options that emit diagnostic output via iostream:
111+
# MANIFOLD_DEBUG and MANIFOLD_TIMING (both use std::cout).
112+
# MANIFOLD_TEST=ON is supported — iostream-using TEST(...) blocks
113+
# in manifold_test/polygon_test/manifoldc_test are gated out under
114+
# the macro; the test executable still builds and runs the rest.
115+
if(MANIFOLD_NO_IOSTREAM AND (MANIFOLD_DEBUG OR MANIFOLD_TIMING))
116+
message(FATAL_ERROR
117+
"MANIFOLD_NO_IOSTREAM is incompatible with MANIFOLD_DEBUG / MANIFOLD_TIMING "
118+
"(those options use std::cout for diagnostic output).")
119+
endif()
120+
103121
# Always build position independent code for relocatability
104122
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
105123

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ CMake flags (usage e.g. `-DMANIFOLD_DEBUG=ON`):
124124
See profiling section below.
125125
- `ASSIMP_ENABLE=[<OFF>, ON]`: Enable integration with assimp, which is needed for some of the utilities in `extras`.
126126
- `MANIFOLD_STRICT=[<OFF>, ON]`: Treat compile warnings as fatal build errors.
127+
- `MANIFOLD_NO_IOSTREAM=[<OFF>, ON]`: Strip iostream- and filesystem-using
128+
bits from the public API and tests; useful for freestanding/embedded
129+
builds (e.g., `wasm32-unknown-unknown`). Defines both
130+
`MANIFOLD_NO_IOSTREAM` and `MANIFOLD_NO_FILESYSTEM` as PUBLIC compile
131+
definitions. The test suite still builds + runs — iostream-using
132+
TESTs in `manifold_test`/`polygon_test`/`manifoldc_test` are gated
133+
out under the macro. Incompatible with `MANIFOLD_DEBUG` /
134+
`MANIFOLD_TIMING` (which use `std::cout` for diagnostic output).
127135

128136
Dependency version override:
129137
- `MANIFOLD_USE_BUILTIN_TBB=[<OFF>, ON]`: Use builtin version of tbb.

bindings/c/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,14 @@ install(
4343
FILES include/manifold/manifoldc.h include/manifold/types.h
4444
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/manifold
4545
)
46+
47+
# Always-on build-time check: compiles manifold/manifoldc.h with
48+
# MANIFOLD_NO_IOSTREAM defined. Mirrors src/no_iostream_check.cpp's
49+
# role for the C++ public header.
50+
add_library(manifoldc_no_iostream_check OBJECT no_iostream_check.cpp)
51+
target_link_libraries(manifoldc_no_iostream_check PRIVATE manifoldc)
52+
if(NOT MANIFOLD_NO_IOSTREAM)
53+
target_compile_definitions(
54+
manifoldc_no_iostream_check PRIVATE MANIFOLD_NO_IOSTREAM
55+
)
56+
endif()

bindings/c/include/manifold/manifoldc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ void manifold_delete_rect(ManifoldRect* b);
542542
void manifold_delete_triangulation(ManifoldTriangulation* m);
543543
void manifold_delete_execution_context(ManifoldExecutionContext* ctx);
544544

545+
#ifndef MANIFOLD_NO_IOSTREAM
545546
// MeshIO / Export
546547

547548
// Import a manifold from a Wavefront obj file.
@@ -570,6 +571,7 @@ void manifold_write_obj(ManifoldManifold* manifold,
570571
// passing additional data into the callback.
571572
void manifold_meshgl64_write_obj(ManifoldMeshGL64* mesh,
572573
void (*callback)(char*, void*), void* args);
574+
#endif // !MANIFOLD_NO_IOSTREAM
573575
#ifdef __cplusplus
574576
}
575577
#endif

bindings/c/manifoldc.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
#include "manifold/manifoldc.h"
1616

17+
#ifndef MANIFOLD_NO_IOSTREAM
1718
#include <sstream>
19+
#endif
1820
#include <vector>
1921

2022
#include "conv.h"
@@ -1048,6 +1050,7 @@ void manifold_destruct_execution_context(ManifoldExecutionContext* ctx) {
10481050

10491051
// IO
10501052

1053+
#ifndef MANIFOLD_NO_IOSTREAM
10511054
ManifoldManifold* manifold_read_obj(void* mem, char* obj_file) {
10521055
std::istringstream iss(obj_file);
10531056
Manifold m = Manifold::ReadOBJ(iss);
@@ -1075,6 +1078,7 @@ void manifold_meshgl64_write_obj(ManifoldMeshGL64* mesh,
10751078
WriteOBJ(ss, *m);
10761079
callback(ss.str().data(), args);
10771080
}
1081+
#endif // !MANIFOLD_NO_IOSTREAM
10781082

10791083
#ifdef __cplusplus
10801084
}

bindings/c/no_iostream_check.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2026 The Manifold Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// Build-time check: compiles manifold/manifoldc.h with MANIFOLD_NO_IOSTREAM
16+
// defined. Fails the build if iostream usage creeps into the C-API
17+
// header outside the macro guards.
18+
19+
#include "manifold/manifoldc.h"

cmake/manifoldDeps.cmake

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ if(MANIFOLD_CROSS_SECTION)
119119
CACHE BOOL
120120
"Preempt cache default of USINGZ (we only use 2d)"
121121
)
122+
# When manifold is built with MANIFOLD_NO_IOSTREAM, also strip
123+
# iostream from the bundled Clipper2 — manifold doesn't call any
124+
# of Clipper2's stream operators internally, so passing this
125+
# through is safe regardless. The CLIPPER2_NO_IOSTREAM macro is
126+
# added by the carry-patch below; once Clipper2#1094 lands and
127+
# the SHA pin moves past it, the patch drops and the option is
128+
# honored natively.
129+
if(MANIFOLD_NO_IOSTREAM)
130+
set(
131+
CLIPPER2_NO_IOSTREAM
132+
ON
133+
CACHE BOOL
134+
"Strip iostream-using overloads from Clipper2 (set by manifold when MANIFOLD_NO_IOSTREAM=ON)"
135+
FORCE
136+
)
137+
endif()
122138
FetchContent_Declare(
123139
Clipper2
124140
GIT_REPOSITORY https://github.com/AngusJohnson/Clipper2.git
@@ -127,6 +143,13 @@ if(MANIFOLD_CROSS_SECTION)
127143
GIT_PROGRESS TRUE
128144
SOURCE_SUBDIR
129145
CPP
146+
# Carry-patch: tracks AngusJohnson/Clipper2#1094 (CLIPPER2_NO_IOSTREAM
147+
# macro guards). Drops once that PR lands and the SHA pin moves past it.
148+
PATCH_COMMAND
149+
git
150+
apply
151+
--ignore-whitespace
152+
${CMAKE_SOURCE_DIR}/cmake/patches/0001-clipper2-no-iostream.patch
130153
)
131154
FetchContent_MakeAvailable(Clipper2)
132155
set_property(
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
Carry-patch: add CLIPPER2_NO_IOSTREAM macro guards to Clipper2's
2+
public headers so manifold can build Clipper2 without iostream when
3+
MANIFOLD_NO_IOSTREAM is set (e.g., for wasm32-unknown-unknown
4+
consumers via the wasm-cxx-shim integration).
5+
6+
Tracks AngusJohnson/Clipper2#1094 — once that lands and manifold's
7+
Clipper2 SHA pin moves past it, this patch drops.
8+
9+
Generated against Clipper2 SHA 46f639177fe418f9689e8ddb74f08a870c71f5b4
10+
(the SHA manifoldDeps.cmake currently pins). The headers at this SHA
11+
are byte-identical to upstream main, so the patch tracks #1094's diff
12+
exactly.
13+
14+
Apply with: git apply -p1 0001-clipper2-no-iostream.patch
15+
16+
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
17+
index 99a5205..d3af0a0 100644
18+
--- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h
19+
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
20+
@@ -14,7 +14,9 @@
21+
#include <cstdint>
22+
#include <vector>
23+
#include <string>
24+
+#ifndef CLIPPER2_NO_IOSTREAM
25+
#include <iostream>
26+
+#endif
27+
#include <algorithm>
28+
#include <numeric>
29+
#include <cmath>
30+
@@ -166,11 +168,13 @@ namespace Clipper2Lib
31+
32+
void SetZ(const z_type z_value) { z = z_value; }
33+
34+
+#ifndef CLIPPER2_NO_IOSTREAM
35+
friend std::ostream& operator<<(std::ostream& os, const Point& point)
36+
{
37+
os << point.x << "," << point.y << "," << point.z;
38+
return os;
39+
}
40+
+#endif
41+
42+
#else
43+
44+
@@ -203,11 +207,13 @@ namespace Clipper2Lib
45+
return Point(x * scale, y * scale);
46+
}
47+
48+
+#ifndef CLIPPER2_NO_IOSTREAM
49+
friend std::ostream& operator<<(std::ostream& os, const Point& point)
50+
{
51+
os << point.x << "," << point.y;
52+
return os;
53+
}
54+
+#endif
55+
#endif
56+
57+
friend bool operator==(const Point& a, const Point& b)
58+
@@ -396,10 +402,12 @@ namespace Clipper2Lib
59+
return result;
60+
}
61+
62+
+#ifndef CLIPPER2_NO_IOSTREAM
63+
friend std::ostream& operator<<(std::ostream& os, const Rect<T>& rect) {
64+
os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") ";
65+
return os;
66+
}
67+
+#endif
68+
};
69+
70+
template <typename T1, typename T2>
71+
@@ -498,6 +506,7 @@ namespace Clipper2Lib
72+
return Rect<T>(xmin, ymin, xmax, ymax);
73+
}
74+
75+
+#ifndef CLIPPER2_NO_IOSTREAM
76+
template <typename T>
77+
std::ostream& operator << (std::ostream& outstream, const Path<T>& path)
78+
{
79+
@@ -518,6 +527,7 @@ namespace Clipper2Lib
80+
outstream << p;
81+
return outstream;
82+
}
83+
+#endif
84+
85+
86+
template <typename T1, typename T2>
87+
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h
88+
index fe1e299..deb1f7b 100644
89+
--- a/CPP/Clipper2Lib/include/clipper2/clipper.h
90+
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.h
91+
@@ -309,6 +309,7 @@ namespace Clipper2Lib {
92+
return true;
93+
}
94+
95+
+#ifndef CLIPPER2_NO_IOSTREAM
96+
static void OutlinePolyPath(std::ostream& os,
97+
size_t idx, bool isHole, size_t count, const std::string& preamble)
98+
{
99+
@@ -338,6 +339,7 @@ namespace Clipper2Lib {
100+
if (pp.Child(i)->Count())
101+
details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " ");
102+
}
103+
+#endif
104+
105+
template<typename T, typename U>
106+
inline constexpr void MakePathGeneric(const T an_array,
107+
@@ -377,6 +379,7 @@ namespace Clipper2Lib {
108+
109+
} // end details namespace
110+
111+
+#ifndef CLIPPER2_NO_IOSTREAM
112+
inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp)
113+
{
114+
std::string plural = (pp.Count() == 1) ? " polygon." : " polygons.";
115+
@@ -399,6 +402,7 @@ namespace Clipper2Lib {
116+
if (!pp.Level()) os << std::endl;
117+
return os;
118+
}
119+
+#endif
120+
121+
inline Paths64 PolyTreeToPaths64(const PolyTree64& polytree)
122+
{

include/manifold/manifold.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ using MeshGL = MeshGLP<float>;
273273
*/
274274
using MeshGL64 = MeshGLP<double, uint64_t>;
275275

276+
#ifndef MANIFOLD_NO_IOSTREAM
276277
MeshGL64 ReadOBJ(std::istream& stream);
277278
bool WriteOBJ(std::ostream& stream, const MeshGL64& mesh);
279+
#endif
278280

279281
/**
280282
* @brief This library's internal representation of an oriented, 2-manifold,
@@ -517,8 +519,10 @@ class Manifold {
517519
* ofile.close();
518520
* @endcode
519521
*/
522+
#ifndef MANIFOLD_NO_IOSTREAM
520523
static Manifold ReadOBJ(std::istream& stream);
521524
bool WriteOBJ(std::ostream& stream) const;
525+
#endif
522526

523527
/** @name Testing Hooks
524528
* These are just for internal testing.

0 commit comments

Comments
 (0)