Skip to content

Commit 2c6a4c3

Browse files
zmerlynnclaude
andcommitted
Add MANIFOLD_NO_IOSTREAM build-time option
Strips iostream- and filesystem-using bits from the public API and tests, for freestanding/embedded consumers (e.g., wasm32-unknown-unknown via wasm-cxx-shim). Default OFF. When ON, defines MANIFOLD_NO_IOSTREAM and MANIFOLD_NO_FILESYSTEM as PUBLIC compile defs on manifold, gates iostream-using TESTs at compile time, and triggers CLIPPER2_NO_IOSTREAM=ON for the bundled Clipper2 via a carry-patch tracking AngusJohnson/Clipper2#1094 (drops once that lands and the SHA pin moves past it). Incompatible with MANIFOLD_DEBUG / MANIFOLD_TIMING (FATAL_ERROR). Always-on manifold_no_iostream_check and manifoldc_no_iostream_check compile the public headers with the macro defined to catch creep. Discussion: #1046 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f95a3a commit 2c6a4c3

19 files changed

Lines changed: 380 additions & 0 deletions

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Carry-patches must keep LF line endings even when checked out on
2+
# Windows with default core.autocrlf=true; otherwise `git apply`
3+
# refuses them on EOL mismatch with the (non-CRLF) target tree.
4+
*.patch text eol=lf

.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
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
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: 30 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,20 @@ if(MANIFOLD_CROSS_SECTION)
127143
GIT_PROGRESS TRUE
128144
SOURCE_SUBDIR
129145
CPP
146+
# Disable Windows autocrlf on the clone so the carry-patch (which
147+
# is LF-only) applies cleanly. Default core.autocrlf=true on
148+
# Windows would convert all LFs to CRLFs in the working tree, and
149+
# `git apply` then fails on the line-ending mismatch.
150+
GIT_CONFIG
151+
core.autocrlf=false
152+
# Carry-patch: tracks AngusJohnson/Clipper2#1094 (CLIPPER2_NO_IOSTREAM
153+
# macro guards). Drops once that PR lands and the SHA pin moves past it.
154+
PATCH_COMMAND
155+
git
156+
apply
157+
--ignore-whitespace
158+
--whitespace=nowarn
159+
${CMAKE_CURRENT_LIST_DIR}/patches/0001-clipper2-no-iostream.patch
130160
)
131161
FetchContent_MakeAvailable(Clipper2)
132162
set_property(
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
Carry-patch: add a CLIPPER2_NO_IOSTREAM build option (off by default)
2+
that strips iostream-using overloads from Clipper2's public headers,
3+
and propagate the macro through Clipper2's PUBLIC compile definitions
4+
so consumers see it. Lets manifold pass MANIFOLD_NO_IOSTREAM=ON
5+
through to the bundled Clipper2 (e.g., for wasm32-unknown-unknown
6+
consumers via the wasm-cxx-shim integration).
7+
8+
Tracks AngusJohnson/Clipper2#1094 — once that lands and manifold's
9+
Clipper2 SHA pin moves past it, this patch drops.
10+
11+
Generated against Clipper2 SHA 46f639177fe418f9689e8ddb74f08a870c71f5b4
12+
(the SHA manifoldDeps.cmake currently pins). The headers at this SHA
13+
are byte-identical to upstream main, so the patch tracks #1094's diff
14+
exactly.
15+
16+
Apply with: git apply -p1 0001-clipper2-no-iostream.patch
17+
18+
19+
diff --git a/CPP/CMakeLists.txt b/CPP/CMakeLists.txt
20+
index 7642e86..b327296 100644
21+
--- a/CPP/CMakeLists.txt
22+
+++ b/CPP/CMakeLists.txt
23+
@@ -17,6 +17,7 @@ option(CLIPPER2_HI_PRECISION "Caution: enabling this will compromise performance
24+
option(CLIPPER2_UTILS "Build utilities" ON)
25+
option(CLIPPER2_EXAMPLES "Build examples" ON)
26+
option(CLIPPER2_TESTS "Build tests" ON)
27+
+option(CLIPPER2_NO_IOSTREAM "Disable iostream-using header overloads" OFF)
28+
option(USE_EXTERNAL_GTEST "Use system-wide installed GoogleTest" OFF)
29+
option(USE_EXTERNAL_GBENCHMARK "Use the googlebenchmark" OFF)
30+
option(BUILD_SHARED_LIBS "Build shared libs" OFF)
31+
@@ -69,6 +70,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "ONLY"))
32+
Clipper2 PUBLIC
33+
CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION}
34+
$<$<BOOL:${CLIPPER2_HI_PRECISION}>:CLIPPER2_HI_PRECISION>
35+
+ $<$<BOOL:${CLIPPER2_NO_IOSTREAM}>:CLIPPER2_NO_IOSTREAM>
36+
)
37+
38+
target_include_directories(
39+
@@ -95,6 +97,7 @@ if (NOT (CLIPPER2_USINGZ STREQUAL "OFF"))
40+
USINGZ
41+
CLIPPER2_MAX_DECIMAL_PRECISION=${CLIPPER2_MAX_DECIMAL_PRECISION}
42+
$<$<BOOL:${CLIPPER2_HI_PRECISION}>:CLIPPER2_HI_PRECISION>
43+
+ $<$<BOOL:${CLIPPER2_NO_IOSTREAM}>:CLIPPER2_NO_IOSTREAM>
44+
)
45+
target_include_directories(
46+
Clipper2Z PUBLIC
47+
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.core.h b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
48+
index 99a5205..d3af0a0 100644
49+
--- a/CPP/Clipper2Lib/include/clipper2/clipper.core.h
50+
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.core.h
51+
@@ -14,7 +14,9 @@
52+
#include <cstdint>
53+
#include <vector>
54+
#include <string>
55+
+#ifndef CLIPPER2_NO_IOSTREAM
56+
#include <iostream>
57+
+#endif
58+
#include <algorithm>
59+
#include <numeric>
60+
#include <cmath>
61+
@@ -166,11 +168,13 @@ namespace Clipper2Lib
62+
63+
void SetZ(const z_type z_value) { z = z_value; }
64+
65+
+#ifndef CLIPPER2_NO_IOSTREAM
66+
friend std::ostream& operator<<(std::ostream& os, const Point& point)
67+
{
68+
os << point.x << "," << point.y << "," << point.z;
69+
return os;
70+
}
71+
+#endif
72+
73+
#else
74+
75+
@@ -203,11 +207,13 @@ namespace Clipper2Lib
76+
return Point(x * scale, y * scale);
77+
}
78+
79+
+#ifndef CLIPPER2_NO_IOSTREAM
80+
friend std::ostream& operator<<(std::ostream& os, const Point& point)
81+
{
82+
os << point.x << "," << point.y;
83+
return os;
84+
}
85+
+#endif
86+
#endif
87+
88+
friend bool operator==(const Point& a, const Point& b)
89+
@@ -396,10 +402,12 @@ namespace Clipper2Lib
90+
return result;
91+
}
92+
93+
+#ifndef CLIPPER2_NO_IOSTREAM
94+
friend std::ostream& operator<<(std::ostream& os, const Rect<T>& rect) {
95+
os << "(" << rect.left << "," << rect.top << "," << rect.right << "," << rect.bottom << ") ";
96+
return os;
97+
}
98+
+#endif
99+
};
100+
101+
template <typename T1, typename T2>
102+
@@ -498,6 +506,7 @@ namespace Clipper2Lib
103+
return Rect<T>(xmin, ymin, xmax, ymax);
104+
}
105+
106+
+#ifndef CLIPPER2_NO_IOSTREAM
107+
template <typename T>
108+
std::ostream& operator << (std::ostream& outstream, const Path<T>& path)
109+
{
110+
@@ -518,6 +527,7 @@ namespace Clipper2Lib
111+
outstream << p;
112+
return outstream;
113+
}
114+
+#endif
115+
116+
117+
template <typename T1, typename T2>
118+
diff --git a/CPP/Clipper2Lib/include/clipper2/clipper.h b/CPP/Clipper2Lib/include/clipper2/clipper.h
119+
index fe1e299..deb1f7b 100644
120+
--- a/CPP/Clipper2Lib/include/clipper2/clipper.h
121+
+++ b/CPP/Clipper2Lib/include/clipper2/clipper.h
122+
@@ -309,6 +309,7 @@ namespace Clipper2Lib {
123+
return true;
124+
}
125+
126+
+#ifndef CLIPPER2_NO_IOSTREAM
127+
static void OutlinePolyPath(std::ostream& os,
128+
size_t idx, bool isHole, size_t count, const std::string& preamble)
129+
{
130+
@@ -338,6 +339,7 @@ namespace Clipper2Lib {
131+
if (pp.Child(i)->Count())
132+
details::OutlinePolyPathD(os, *pp.Child(i), i, preamble + " ");
133+
}
134+
+#endif
135+
136+
template<typename T, typename U>
137+
inline constexpr void MakePathGeneric(const T an_array,
138+
@@ -377,6 +379,7 @@ namespace Clipper2Lib {
139+
140+
} // end details namespace
141+
142+
+#ifndef CLIPPER2_NO_IOSTREAM
143+
inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp)
144+
{
145+
std::string plural = (pp.Count() == 1) ? " polygon." : " polygons.";
146+
@@ -399,6 +402,7 @@ namespace Clipper2Lib {
147+
if (!pp.Level()) os << std::endl;
148+
return os;
149+
}
150+
+#endif
151+
152+
inline Paths64 PolyTreeToPaths64(const PolyTree64& polytree)
153+
{

0 commit comments

Comments
 (0)