Skip to content

Commit 40edfd5

Browse files
committed
fix(e2e): test 83 binary lookup is platform-aware (app/app.exe); doc: interface-define rationale
The 83_feature_defines_propagate.sh runtime check assumed a POSIX binary name (`app`); on Windows the artifact is `app.exe`, so `find -name app` returned empty and `"$BIN"` ran as an empty command (exit 127). Match both names and assert the binary was found. The build-time assertion (the #error guard) already passed on Windows, confirming interface-define propagation is cross-platform; only the test's runtime lookup was non-portable. Doc: record that a feature's `defines` are interface requirements (propagated to consumers along Public/Interface edges) — the realization of the "package-owned namespaced define" rule for header-only providers, with the ODR-safety, visibility-bounding, and MCPP_FEATURE_* non-propagation rationale.
1 parent b17d072 commit 40edfd5

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

.agents/docs/2026-06-29-feature-capability-model-design.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,51 @@ Spack, Nixpkgs, pkg-config. Distilled lessons that shaped this design:
129129
mutual exclusion structurally — no constraint DSL, no `backend-*` boolean
130130
pile.
131131

132+
### Corollary — a feature define is an *interface* requirement
133+
134+
Rule 1 names *what* a feature may contribute to compilation (a package-owned,
135+
namespaced define); it does not, by itself, fix *where* that define applies. For
136+
a **header-only** provider the answer is forced: the library has no sources of
137+
its own, so its feature switch only takes effect in the translation unit that
138+
*includes its headers* — i.e. in the **consumer**. `EIGEN_USE_BLAS` must be
139+
defined when the consumer compiles `a * b`, not (only) when Eigen's anchor TU
140+
compiles. Therefore a feature's `defines` are treated as **interface
141+
requirements**: they propagate to consumers along Public/Interface dependency
142+
edges, on exactly the same machinery and visibility discipline as a dependency's
143+
public `include_dirs` (`PackageRoot::publicUsage`, the `computeUsageRequirements`
144+
fixpoint). This is the realization of Rule 1 for header-only providers, not an
145+
exception to it — the define stays package-owned and namespaced; only its scope
146+
is corrected.
147+
148+
Why this does **not** reintroduce the vcpkg failure mode ("flags leak into the
149+
ABI, break composition"):
150+
151+
- **Only the namespaced, library-owned define crosses the boundary** — never
152+
free-form flags. Link flags / include paths still come from the bound
153+
provider's own build config (Rule 1 intact).
154+
- **Visibility-bounded.** Propagation follows the same Public/Interface edges as
155+
include dirs; a `private` dependency edge keeps the define off the consumer's
156+
public interface.
157+
- **ODR-safe by single-instance propagation.** Activation is unioned onto a
158+
single shared provider instance (Cargo model); propagation flows outward from
159+
that one `publicUsage`, so every consumer of the provider sees the *same*
160+
define set. A header-only library compiled with the switch in one TU and
161+
without it in another would be an ODR violation — single-instance propagation
162+
structurally prevents that split.
163+
- **The automatic `MCPP_FEATURE_<NAME>` macro is deliberately NOT propagated.**
164+
It is not namespaced by the library (two packages may each declare a
165+
`use_blas` feature → colliding `MCPP_FEATURE_USE_BLAS`), so it stays private to
166+
the owning package as a local build signal. Only the namespaced user define is
167+
an interface contract — which reinforces, rather than relaxes, Rule 1.
168+
169+
Simplicity note (少即是多): *all* feature defines are interface defines; mcpp does
170+
**not** add a CMake-style PUBLIC/PRIVATE/INTERFACE tri-state for defines — that is
171+
precisely the complexity this design avoids. A define that happens to matter only
172+
to the provider's own `.cpp` still propagates, but lands in consumers as an
173+
unused, namespaced `-D` (harmless). Should a genuinely provider-private feature
174+
define ever be needed, a `private-defines` key is the future-proofing escape
175+
hatch; it is YAGNI today.
176+
132177
## 4. The model — two primitives
133178

134179
### Primitive ① Feature — additive, composable, does only this

tests/e2e/83_feature_defines_propagate.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ cd app
7878
grep -q 'WIDGET_TURBO' compile_commands.json || {
7979
echo "FAIL: WIDGET_TURBO missing from consumer compile_commands.json"; cat compile_commands.json; exit 1; }
8080

81-
# And the produced binary observes turbo mode at runtime.
82-
BIN=$(find target -name app -type f | head -1)
81+
# And the produced binary observes turbo mode at runtime. (Binary name is
82+
# platform-dependent — `app` on POSIX, `app.exe` on Windows.)
83+
BIN=$(find target -type f \( -name app -o -name app.exe \) | head -1)
84+
[ -n "$BIN" ] || { echo "FAIL: built binary not found under target/"; exit 1; }
8385
"$BIN"; rc=$?
8486
[ "$rc" -eq 0 ] || { echo "FAIL: binary did not observe WIDGET_TURBO (exit $rc)"; exit 1; }
8587

0 commit comments

Comments
 (0)