Skip to content

Commit e73c078

Browse files
zmerlynnclaude
andauthored
Add MANIFOLD_NO_IOSTREAM build-time option (#1690)
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). Discussion: #1046 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f95a3a commit e73c078

17 files changed

Lines changed: 344 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/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
}

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
154+
# it. Applied via wrapper script so re-configures (which re-trigger
155+
# PATCH_COMMAND) don't fail when the patch is already applied.
156+
PATCH_COMMAND ${CMAKE_COMMAND}
157+
-DPATCH_FILE=${CMAKE_CURRENT_LIST_DIR}/patches/0001-clipper2-no-iostream.patch
158+
-DSOURCE_DIR=<SOURCE_DIR>
159+
-P ${CMAKE_CURRENT_LIST_DIR}/patches/apply-clipper2-patch.cmake
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+
{
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Idempotent application of the Clipper2 carry-patch. Runs as the
2+
# PATCH_COMMAND of FetchContent_Declare(Clipper2 ...). Re-runs of
3+
# `cmake` (e.g., when CMakeLists.txt files in the parent project
4+
# are edited) reinvoke the patch step; without this idempotency
5+
# wrapper, `git apply` fails on already-patched files.
6+
#
7+
# Invoked via: cmake -DPATCH_FILE=<path> -DSOURCE_DIR=<src> -P this.cmake
8+
9+
if(NOT PATCH_FILE OR NOT SOURCE_DIR)
10+
message(FATAL_ERROR "PATCH_FILE and SOURCE_DIR are required")
11+
endif()
12+
13+
# `git apply --reverse --check` succeeds iff the patch is already applied.
14+
execute_process(
15+
COMMAND git apply --reverse --check --ignore-whitespace --whitespace=nowarn "${PATCH_FILE}"
16+
WORKING_DIRECTORY "${SOURCE_DIR}"
17+
RESULT_VARIABLE _already_applied
18+
OUTPUT_QUIET
19+
ERROR_QUIET
20+
)
21+
if(_already_applied EQUAL 0)
22+
return()
23+
endif()
24+
25+
execute_process(
26+
COMMAND git apply --ignore-whitespace --whitespace=nowarn "${PATCH_FILE}"
27+
WORKING_DIRECTORY "${SOURCE_DIR}"
28+
RESULT_VARIABLE _apply_result
29+
)
30+
if(NOT _apply_result EQUAL 0)
31+
message(FATAL_ERROR "Failed to apply Clipper2 carry-patch: ${PATCH_FILE}")
32+
endif()

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)