Skip to content

release 0.0.100: large-source-package platform fixes (#247 #248 #249) + build.mcpp capability completion#250

Merged
Sunrisepeak merged 12 commits into
mainfrom
fix/p0-large-source-pkg-platform
Jul 19, 2026
Merged

release 0.0.100: large-source-package platform fixes (#247 #248 #249) + build.mcpp capability completion#250
Sunrisepeak merged 12 commits into
mainfrom
fix/p0-large-source-pkg-platform

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Summary

大型源码直编包(ffmpeg/opencv 级,数千 TU)全平台化批次。设计文档:.agents/docs/2026-07-19-large-source-pkg-platform-fixes-and-buildmcpp-generation-design.md(三 issue 根因逐行核实 + 第四坑 + P0–P2 方案)。

Closes #247, closes #248, closes #249.

P0 平台修复

P1 build.mcpp 指令面补全

mcpp:source= / mcpp:include-dir[-after]=(私有,typed 通道)/ MCPP_TARGET_OS/ARCH/ENV / root build.mcpp 后移依赖解析后并获 MCPP_DEP_*_DIR;指令落通道收敛 root/dep 共享单 fold;feature 未知子键告警;xpkg parse --all-os

架构 review(三视角,PR 前完成)

正确性(零高置信 bug)、跨平台(抓到 rsp 反斜杠 P0 与 cd /d)、架构/简洁性(fold 单一 owner、类型对齐 path、规则发射统一、文档版本标注)。

测试

  • 单测 35/35;e2e 新增 141–145;linux 全量 e2e 134 过(3 项既知环境性失败:22/54/62)。
  • macOS/windows 断面依赖平台 CI;生态复现件(mcpp-index spike PR#89/90/91)在 release 后按设计文档验收基线收口。

发布

末 commit 为 chore(release): 0.0.99 -> 0.0.100(CHANGELOG + 双版本常量)。合入后走 tag release → 镜像 → xim-pkgindex → 真装验证 → pin bump。

…on Windows

Driver-style toolchains (g++/clang++ as link driver — the gnu dialect,
which is what Windows clang-MSVC resolves to via dialect_for) inlined
$in into cxx_link/cxx_archive/cxx_shared. On Windows ninja spawns via
CreateProcess (32 KiB command-line ceiling), so ffmpeg/opencv-class
packages (thousands of objects) overflow at the final link.

Route $in through @$out.rsp on Windows only (gcc/clang drivers and
GNU/llvm ar all accept @rspfile); POSIX rule text stays byte-identical
(ARG_MAX is ample). The three rules share one driver_rule emitter.

Also lands the batch design doc (P0-P2).
…ction

The build path splices only the running host's per-OS block and skip-
tables the rest, so a typo in the windows section is invisible on linux
CI. --all-os re-parses the descriptor once per OS the xpm table declares
versions for (a linux+macosx-only package legitimately has no windows
section), via a new osOverride seam on synthesize_from_xpkg_lua — same
function, same grammar as the resolver.

mcpp-index CI can now lint all legs of a multi-platform descriptor from
one runner (design doc 2026-07-19 §4).
…path (launcher-unify)

Root cause: on macOS the shell fallback built `KEY='v' cd <cwd> && prog
2>&1` when both extraEnv and cwd were non-empty. POSIX binds the env
assignment prefix to the first simple command only — `cd` — so across
`&&` the real program received NO extra env. The only env+cwd call site
is the build.mcpp child (build_program.cppm), which therefore lost the
entire MCPP_* G3 contract (MCPP_OUT_DIR etc.) on macOS and aborted.

Fix: extend the run_exec / capture_exec guards from __linux__ to
__linux__ || __APPLE__ so macOS shares the direct posix_spawn path —
child-only env via merged_environ, cwd via
posix_spawn_file_actions_addchdir_np (macOS 10.15+; mcpp's floor is
macOS 14, no runtime probe needed).

Portability: on Apple, `environ` is only linkable from executables, not
dylibs, so merged_environ now reads the host block through a
host_environ() helper using _NSGetEnviron() from <crt_externs.h>; Linux
keeps the plain symbol.

Windows _WIN32 branches and the residual non-POSIX shell fallback are
untouched; the L286 rationale + TODO(launcher-unify) now document
Windows as the sole remaining shell-path exception. As a side effect
macOS also sheds the shell quoting/signal/injection surface for these
two launchers.

Regression test: capture_exec with BOTH non-empty extraEnv and cwd
(previously the untested combination — all prior call sites passed at
most one) asserting the child sees the env value AND runs in cwd; runs
on Linux (same shared code path) and macOS CI.
…keeps incremental builds alive)

materialize_generated_files rewrote every entry unconditionally whenever
prepare_build ran -> mtime bump -> ninja (mtime-driven, header reached via
depfiles) recompiled every TU including the materialized header, even when
only an unrelated source changed. Frozen-snapshot packages (compat.ffmpeg:
config.h included by 2283 TUs) degraded to full rebuilds on every edit.

Now the intended content is byte-compared against the on-disk file and the
write is skipped when identical, preserving the mtime. Correctness is
unchanged: content changes are already folded into the fingerprint, which
owns change detection; the skip only avoids the mtime churn (mirroring the
build.mcpp cache design). Path-escape guards, create_directories, and the
fingerprint interplay are untouched.

e2e 142 locks it: touching an unrelated TU (abandoning the P0 fast-path so
prepare re-runs) must keep the generated header's mtime and not recompile
the including TU; changing the generated content in mcpp.toml must still
rewrite the file and rebuild (no false-fresh). A truly no-op rebuild never
reaches materialization (fast-path), so the unrelated-touch case is the
load-bearing repro - verified failing on the unfixed binary.
…stop shadowing system headers)

Root cause: dependency include_dirs are always emitted as -I (highest
priority). On case-insensitive macOS filesystems a dep's extracted-tarball
root on -I that contains a file named like a standard header shadows it for
every C++ TU of every consumer — ffmpeg's top-level VERSION file shadows
libc++'s <version>, breaking any consumer that touches the standard
library. There was no lower-priority include variant in the descriptor
vocabulary.

Why -idirafter and not -isystem: -isystem dirs still precede the default
system dirs, so the poison file still wins. -idirafter dirs are searched
AFTER all system dirs (gcc+clang), so system headers win while non-system
headers (<libavutil/frame.h>) are still found.

Full chain:
- manifest/types.cppm: BuildConfig.includeDirsAfter next to includeDirs
- manifest/xpkg.cppm: include_dirs_after key (+ include_after /
  include_dir_after aliases), parse branch mirroring include_dirs;
  platform overlays get it for free via the shared key loop
- manifest/toml.cppm: [build] include_dirs_after
- modgraph: UsageRequirements.includeDirsAfter,
  SourceUnit.localIncludeDirsAfter, threaded through scan_package /
  scan_packages / scan_packages_p1689 with the same expand_dir_glob
  treatment (`*` extracted-tarball-root convention)
- build/prepare.cppm: expansion into privateBuild/publicUsage, propagation
  along the SAME dependency edges as includeDirs — consumers receive them
  as after-dirs, never upgraded to -I; after-dirs enter the resolved-usage
  identity string; staged-secondary absolutization mirrored
- build/plan.cppm: CompileUnit.localIncludeDirsAfter populated wherever
  localIncludeDirs is (scanned units + synthesized entry main)
- build/ninja_backend.cppm: emitted as -idirafter into $local_includes,
  appended after the -I entries (flag semantics, not position, carry the
  priority). MSVC dialect: cl.exe has no -idirafter — documented
  degradation to trailing /I (clang-MSVC uses the gnu dialect and gets
  real -idirafter)
- build/flags.cppm: main-manifest [build] include_dirs_after on the global
  typed-path channel (-idirafter / trailing /I; nasm degrades to -I)
- build/compile_commands.cppm: -idirafter spelling preserved in the
  compile DB so clangd reproduces the search order

Adjacent consistency fix: plan.cppm local_include_dirs_for_manifest did a
bare root/inc join with no glob expansion, unlike the dep path in
prepare.cppm. Now unified on expand_dir_glob, with a plain-join fallback
for literal wildcard-free entries that don't exist yet (expand_dir_glob
only returns existing dirs; the old unconditional join must keep working
for dirs created later by build steps).

Tests: unit (ninja -idirafter emission + ordering, msvc /I degradation,
toml + xpkg parse), e2e 141 (poisoned stdlib.h + real mylib.h in a path
dep's include root: include_dirs_after builds and runs, plain
include_dirs control fails on the poison).
Names the capability that the grey `mcpp:generated=<absolute path>` usage
provided by accident: a build.mcpp can now SELECT a pre-existing source
(tarball payload / vendored tree) into the compile set, without claiming it
wrote the file. Design: 2026-07-19-large-source-pkg-platform-fixes-and-
buildmcpp-generation-design.md §3.1 item 1.

- parse_line: new `source` directive key; value verbatim into Directives::sources.
- Path rules: absolute taken as-is; relative resolves against the PACKAGE
  ROOT in both root and dependency mode (unlike generated=, whose dep-mode
  relatives resolve to genBase/OUT_DIR) — a payload file lives in the package
  tree, never in OUT_DIR. `generated=` behavior is byte-for-byte untouched,
  including the absolute-path grey usage.
- apply(): same landing as generated= (bc.sources + m.modules.sources, both
  lists so the scanner sees files outside the base globs).
- Existence: post-run hard error with a source=-specific message ("selected
  source ... no such file"); cache_fresh also invalidates when a selected
  source vanishes (mirrors generated=).
- Cache: new `d source <path>` record kind (write_cache/read_cache round-trip).
- Typed lib: mcpp::source(path) mirrors mcpp::generated 1:1.
- execute.cppm sources_newer_than: the root build.mcpp is now a fast-path
  input — editing ONLY build.mcpp previously never fell through to
  prepare_build (fresh build.ninja short-circuited before the program-hash
  cache key could see the change), so the documented "re-runs when the
  build.mcpp source itself changes" was unreachable on the fast path. Found
  by the new e2e's missing-source case; covers try_fast_build AND try_fast_run.
- Docs: directive + typed-API tables (en/zh).

No unit seam exists for parse_line/apply (anonymous namespace in the module
impl unit) — covered by e2e 143_build_mcpp_source_directive.sh: pre-existing
file outside all sources globs is compiled+linked via the typed API, `d source`
cache record replays on rebuild without a re-run, and a missing selection
fails with the actionable error.

Verified: unit 35/35; e2e 143/110/111/125 green.
Two new directives (design §3.1 item 2) replacing the unnormalized
`cxxflag=-I` + `cflag=-I` double-emission hack (opencv descriptor:412-413):

- Paths resolve like link-search: absolute as-is, relative against the
  package root, lexically normalized at parse time (abs_against_root).
- include-dir → buildConfig.includeDirs; include-dir-after →
  buildConfig.includeDirsAfter. The includeDirsAfter FIELD is added to
  manifest/types.cppm here (line coordinated with the parallel #249
  worktree); the -idirafter NINJA EMISSION chain is deliberately NOT
  implemented — that worktree owns ninja_backend/plan. This commit lands
  values into buildConfig only; the root-path end-to-end -idirafter arrives
  when the branches merge.
- Cache: `d include-dir` / `d include-dir-after` record kinds round-trip.
- Typed lib: mcpp::include_dir / mcpp::include_dir_after.

Scope decision (dep path — the invariant is: directive include dirs NEVER
leak to consumers): a dep's own TUs read privateBuild, consumers read
publicUsage (re-flowed by the usage fixpoint right after the dep loop). The
dep loop therefore mirrors the new includeDirs tail into
pkg.privateBuild.includeDirs ONLY (publicUsage untouched → no leak; the
post-snapshot bcDep entries are inert since descriptor include_dirs
propagation snapshotted publicUsage at makePackageRoot). includeDirsAfter
has no privateBuild slot (emission chain is #249's), so for deps it is
spelled as private "-idirafter <dir>" flag pairs into privateBuild
cflags+cxxflags — deps get the semantics today, same two-token spelling as
host_base_flags. For the ROOT the dirs join buildConfig before the
packages[0] snapshot, where expandIncludeDirs handles absolute entries;
root has no consumers, so privacy holds trivially.

Also folded here (nearest theme — unsupported per-feature include_dirs was
the motivating case): xpkg feature UNKNOWN SUB-KEY is now recorded as
`features.<name>.<sub>` in xpkgUnknownKeys instead of being silently
swallowed; the existing adoption-site warn_unknown_xpkg_keys (prepare.cppm)
surfaces it as a warning. Unit: XpkgUnknownKeys.FeatureSubKeyRecorded.

e2e 144_build_mcpp_include_dir.sh: header written under MCPP_OUT_DIR +
absolute include-dir → #include resolves with no manual -I; include-dir-after
accepted (no unknown-directive warning); both replay from the directive cache.

Verified: unit 35/35 green; e2e 144/143/110/111/125 green.
Cargo CARGO_CFG_TARGET_* parity (design §3.1 item 3): contract_env now
parses the resolved target ONCE through the canonical triple parser
(mcpp.toolchain.triple — already imported for MCPP_HOST) and injects

  MCPP_TARGET_OS    linux|macos|windows
  MCPP_TARGET_ARCH  GNU spelling (x86_64, aarch64, ...)
  MCPP_TARGET_ENV   gnu|musl|msvc, "" when the triple has no env segment

so every build.mcpp (ffmpeg/opencv per-OS selection) stops hand-splitting
MCPP_TARGET. All three are always SET; an escape-hatch triple outside the
canonical vocabulary yields "" for all three (parse failure is not an error
here — MCPP_TARGET still carries the verbatim spelling).

Re-run key: verified — contract_hash hashes the WHOLE (name,value) env
vector, and the new vars ride that same vector, so they fold into ctxHash
with no extra plumbing (a target change already re-ran the program via
MCPP_TARGET; the splits add no new invalidation axis).

Typed lib: mcpp::target_os()/target_arch()/target_env() readers.

e2e 110 extended: asserts the three vars are set, OS/ARCH agree with the
full MCPP_TARGET triple's leading segments, and ENV is set (content may be
empty on macOS). Docs env tables updated (en/zh).

Verified: unit 35/35; e2e 110/143/144/111/125 green.
…CPP_DEP_*_DIR

Design §3.1 item 4 (+ #230-#243 ledger follow-up): the ROOT project's
build.mcpp used to run BEFORE dependency resolution (prepare.cppm ~1180), so
unlike a dependency's it never saw MCPP_DEP_<NAME>_DIR — consumer-side
synthesis (ffmpeg-m reading compat.ffmpeg's payload tree) was impossible.
The call now sits right after the dep build.mcpp loop / usage fixpoint /
capability binding, BEFORE the [targets.*] gate and the modgraph scan, and
populates env.depDirs from the SAME authoritative dependencyEdges graph as
the dep path (consumer index 0 = root; canonical + namespace-stripped
spellings; same collision guard in contract_env).

Ordering invariants, each verified at the new call point:
(1) generated=/source= registration precedes the modgraph scan — the scan
    walks packages[0].manifest, so the new-source TAILS are mirrored into
    packages[0].manifest.{buildConfig.sources,modules.sources} (see wrinkle
    below); the scan call sits ~200 lines further down.
(2) directive flags precede canonicalization/fingerprint — fpi.compileFlags
    = canonical_compile_flags(*m) + canonical_package_build_metadata(packages)
    is computed AFTER the scan, i.e. after this call; flag tails are mirrored
    into packages[0].privateBuild (per-TU assembly) AND
    packages[0].manifest.buildConfig (fingerprint metadata parity).
(3) materialize_generated_files (root) still runs earlier — untouched at its
    pre-move position, so [generated_files] may still produce build.mcpp.
(4) L1 cfg-conditional merge ordering unchanged — it still runs before
    makePackageRoot; ONLY the build.mcpp call moved later (e2e 108 green).
(5) root features env — recomputed with the byte-identical expression
    (feature_closure(*m, parse_feature_request(overrides.features))) so the
    contract hash / build.mcpp cache is stable across the move.

The wrinkle the old ordering hid: apply() used to mutate *m before
`packages[0] = makePackageRoot(*root, *m)` snapshotted buildConfig into
privateBuild/manifest — the copies the scan and per-TU flag assembly read.
Post-move the snapshot (and root feature activation on it) already happened,
so the directive tails are mirrored explicitly, dep-loop style: sources →
manifest.buildConfig.sources + manifest.modules.sources; c/cxx flags →
privateBuild + manifest.buildConfig; ldflags → linkUsage +
manifest.buildConfig (final link reads *m, already applied); include-dir →
privateBuild.includeDirs ONLY (private discipline — under the old order they
also leaked into root publicUsage via the snapshot, harmless with no
consumers but now deliberately private); include-dir-after →
manifest.buildConfig.includeDirsAfter. Known benign delta: directive flags
now append AFTER feature/usage-propagated flags in privateBuild instead of
before them (order within one flags vector).

e2e 145_root_build_mcpp_dep_dirs.sh (modeled on 125): root with a path dep;
the ROOT's build.mcpp hard-fails unless dep_dir("datad") is set, generates a
source returning it; asserts the dir exists and is datad's root.

Docs: MCPP_DEP_<NAME>_DIR documented in the env table (en/zh) — previously
undocumented — with the root now receiving it.

Verified: unit 35/35; e2e 145/110/111/112/125/143/144 green + regression
batch 106/107/108/109/114/100/04/09/15/126/128 green.
…249 channel

Review pass over the merged P0+P1 branch. Two spellings of the same
decision survived the parallel tracks:

- dep loop: after-dirs were spelled as raw private '-idirafter <dir>'
  flag pairs (written before #249 landed privateBuild.includeDirsAfter);
  the raw spelling bypasses the typed channel's per-dialect degradations
  (cl.exe /I, NASM -I). Now appendUniquePath into the typed slot.
- root: after-dirs reached only the fingerprint mirror
  (manifest.buildConfig), never privateBuild — scanned units read
  privateBuild.includeDirsAfter, so 'mcpp:include-dir-after=' on the
  root was inert on compile edges. Both channels now mirrored for both
  kinds, matching the flag mirrors above.

e2e 144 upgraded from 'directive accepted' to asserting -idirafter
reaches build.ninja.
… directive-fold owner, type/rule unification

Consolidated fixes from the three-lens pre-PR review (correctness /
cross-platform / architecture):

- #247 P0: escape_ninja_path emits generic (forward-slash) node names.
  rspfile_content copies node names verbatim into a response file that
  gcc/clang/GNU ar tokenize GNU-style, where backslash is an ESCAPE —
  obj\cli.o would arrive as objcli.o and break every Windows
  driver-style link. All Windows consumers accept forward slashes;
  POSIX output is byte-identical.
- link/archive/shared rules: ONE link_rule emitter for both dialect
  branches (useRsp = separateLinker || is_windows); msvc rule text
  byte-identical to the previous hand-written stanzas.
- prepare.cppm: markDirectiveTail/foldDirectiveTailIntoPrivateBuild —
  a single owner of 'which compile-visible channels a build.mcpp
  directive lands in', shared by the dep loop and the root call site
  (kills the #242 two-derivations shape the parallel tracks had
  reintroduced). Link/source/fingerprint residues stay at the call
  sites where they genuinely differ.
- BuildConfig::includeDirsAfter aligned to filesystem::path (was
  string), deleting per-consumer conversions.
- local_include_flags derives the NASM degradation from cu.source
  itself instead of a third parameter threaded by every caller.
- Windows shell fallback: cd /d (cmd.exe cd does not switch drives).
- docs: source= joins the cache-invalidation list; 0.0.100+ tags on
  the new directives/env rows; NASM -I degradation documented (en+zh).
@Sunrisepeak
Sunrisepeak merged commit 86ccdfd into main Jul 19, 2026
7 checks passed
@Sunrisepeak
Sunrisepeak deleted the fix/p0-large-source-pkg-platform branch July 19, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment