Skip to content

2026.9.28.1: #717-#726 in one round (in progress) - #727

Open
speak-agent wants to merge 42 commits into
mainfrom
fix/726-windows-invocation-env
Open

speak-agent wants to merge 42 commits into
mainfrom
fix/726-windows-invocation-env

Conversation

@speak-agent

@speak-agent speak-agent commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

This pull request is in progress. It carries the whole round planned in .agents/docs/2026-09-27-eight-reports-by-home-and-one-optimisation-plan.md: issues #717, #718, #720, #722, #723, #724 and #725, together with #726, the fix this pull request was opened for. The final description, with the full test table, replaces this one before merge.

Landed so far

Step Issue Change
W1 #725 A rooted workspace carries its workspace context to its own path dependencies. -p resolves a package's identity first, then the directory spellings.
W2 #720 A host-module lib root is a node of its package's import order.
W3 #724 A rule-claimed device source is not a compile unit.
W4 #724 A failed build program's own diagnostic survives.
W5 #724 emit writes no .mcpp/.xlings.json into the project.
W6 #723 One deploy destination has one content, checked when staging, and one writer.
W8 #717 [target.<selector>.build] dialect_cxxflags, a graph-wide key read from the root only.
W10 #724 The S1 document names the files a rule generates (S1 0.3.0, Sunrisepeak/mcpp-language-server#28).
W11 — One progress renderer for every acquisition, with plain output when not on a terminal. It depends on openxlings/xlings#625.
W12 — An index that requires a newer mcpp is a closing tip, not an error, and mcpp self doctor reports it.
W13 #726 On Windows an xlings invocation leaves the process environment as it found it and starts in the registry's home.

Still to come

The version is 2026.9.28.1, in mcpp.toml and MCPP_VERSION both. The first CI run of #727 failed because only mcpp.toml had moved.

speak-agent and others added 10 commits September 27, 2026 21:24
…720)

The lib root was placed at the head of the host-module compile list before
the list was sorted by imports, so a lib root that imports a sibling unit
was compiled first and failed. The lib root is now the first node of the
same sort; it is emitted first whenever it imports nothing of its package,
so existing packages keep their order. e2e 807 covers an explicit and a
conventional lib root.
The read site printed the E0006 text as error: at the start of every run
that read such an index, including runs that then resolved every package
elsewhere and exited 0. The fact is now recorded without printing:
- a run that fails carries the E0006 text in the message that stops it;
- a run that refreshed an index and met a floor ends with one tip: line,
  printed after the command's output (closing notices, mcpp.ui), and an
  envelope reports it as the note MCPP_INDEX_REQUIRES_NEWER_MCPP;
- mcpp self doctor lists every index whose floor this mcpp does not meet;
- a tree recorded as unusable answers no later lookup either;
- the E0006 upgrade note starts on its own line.
Unit tests for the read site, the hint, the notices; e2e 812.
…d -p resolves the package first

A rooted workspace (root [package] + [workspace]) built as itself never set
state.wsManifest / state.runtimeWorkspaceRoot, so a member reached through the
root's own [dependencies] path entry was loaded as an ordinary path dependency:
its x.workspace = true entries were refused or silently unresolved, and it
received none of [workspace.package] / [workspace.build]. The workspace context
is now set in that branch too, before any dependency is loaded, matching the
member-switch branch and satisfying "a member is a member however it is
reached" (graph.cpp's depIsMember).

-p, --package <NAME> promised a package but both resolvers
(manifest.cpp's inline loop and project.cppm's resolve_member_dir) matched only
a member's directory basename or path. They are now one function in
project.cppm, resolving in order: a member's qualified name
(<namespace>.<name>), then its bare package name (refused, naming every match,
when shared by two or more members), then its directory path or basename (kept
as a fallback; a duplicate basename keeps today's first-match selection but now
warns naming the others). A value that is one member's package name and a
different member's directory selects the package, with a warning naming the
other member.

SPEC-004 §9 item 1 names the missing position and states that -p resolves the
package identity first; docs/07 §5.3 (en, zh) and the four `-p` help strings
state the resolution order.

Tests: six unit tests for the resolver in test_workspace_inheritance.cpp
(package/path/basename all selecting one member, same name under two
namespaces, package-outranks-directory with a warning, duplicate-basename
warning, the "not found" listing, and the unaffected no-filter cases); e2e 805
(a rooted workspace's own path dependency, with a non-latest pinned version,
an omitted package.version, a [workspace.build] flag, and a shared root
[toolchain], served from a project-local index so the build touches no
network) and e2e 806 (-p's three resolution steps end to end). Both e2e
scripts fail on the released 2026.9.27.1 binary and on 2026.9.26.1, and pass
with this fix.

Co-authored-by: speak-agent <agent@mcpp-community>
Two or more source paths for one deploy destination no longer collide at
planning. `add_deploy` (src/build/plan.cppm) merges them into one
`DeployFile` entry instead of refusing a second source path; a `BuildPlan`
whose deploy has exactly one source still produces the byte-identical
`build.ninja` line it always has. The merged destination becomes one
`stage_file` edge with every source as an input (src/build/ninja_backend.cppm),
and `mcpp stage` places it once all sources agree byte-for-byte
(`stage_files`, src/build/stage.cppm), otherwise failing and naming every
source and the destination. `cmd_stage` (src/cli/cmd_build.cppm) accepts one
or more source positionals.

One destination, one writer: `place-dlls`'s post-link DLL placement
(src/pack/pack.cppm `place_runtime_dlls`, invoked from
src/cli/cmd_publish.cppm `cmd_place_dlls`) now receives the set of names the
merged deploy list already places beside a program, computed in
ninja_backend.cppm and passed through a per-edge `$placed` ninja variable so
the generated graph stays stable. `place_runtime_dlls` skips those names
instead of overwriting them, comparing content and reporting a difference as
a warning.

SPEC-007 R4.2 and R4.3 are amended to state the content check and the
single-writer rule; docs/04 (en, zh) is corrected to match. e2e 810 covers
the merge and the build-time refusal; e2e 811 (`# requires: windows`, not run
here) covers the single-writer rule; e2e 646's collision case is updated for
the new build-time message. Unit tests cover `stage_files`'s multi-source
behaviour and the deploy-list skip/warn logic in `place_runtime_dlls`.

Co-Authored-By: Claude <noreply@anthropic.com>
…onment as it found it and starts in the registry's home (#726)

On Windows, build_command_prefix prepended the registry's subos/default/bin
to the process PATH and set XLINGS_HOME process-wide, and ran xlings in
mcpp's working directory. After a build installed a payload, every action
found xim:llvm's cl/link/lib/rc shims in front of MSVC's tools, and vcpkg's
compiler detection failed; a project with a .xlings.json at its root also
received the shims of mcpp's toolchain and payloads in its own SubOS.

ScopedInvocationEnv now applies XLINGS_HOME, the scope variables and the
PATH prefix for the invocation and restores them afterwards, and the Windows
prefix starts with `cd /d "<home>" &&`, as the POSIX prefix does.

Refs #726.
…uild

program's own diagnostic survives; emit writes no project file

W3 (src/build/plan.cppm): the compile-unit loop now skips
SourceKind::Device graph units, so build.ninja carries no dead cxx_object
edge for a rule-claimed device source, and compile_commands.json / the S1
document agree without their own filter (unit_invocations already excluded
only NASM; nothing else needed to change). The source stays in `watch` and
still reaches the package's build program through MCPP_DEVICE_SOURCES,
since both read the manifest's sources glob directly, not plan.compileUnits.
Checked every other consumer of plan.compileUnits (prepare/plan.cpp's
dependency-cache collection keys by path, not by index, so it is unaffected
beyond a smaller artifact set for a package with device sources).

W4: a package whose build program failed under `emit`'s plan_only records
MCPP_BUILD_DATABASE_PROGRAM_FAILED and applies none of its directives
(state.cppm: new PrepareState::programFailedPackages, set at both call
sites in target_side.cpp and features.cpp). The device-source orphan check
in target_side.cpp now skips such a package outright, instead of reading
every device source as unclaimed and failing the whole member. Also: notes
a phase recorded before prepare_build's own failing return are no longer
silently dropped (driver.cpp: a thread_local sink in the same per-run-sink
style as mcpp::build::refusal, exported as
mcpp::build::take_notes_on_failure); cmd_build.cppm's emit failure path
folds any such note into the one diagnostic SPEC-005 R5.2 allows a wholly-
failed member (path stays the member's mcpp.toml, exactly one entry), so
the true cause is not lost behind a downstream symptom without violating
that invariant. hasProgram's existing exists(build.mcpp) check is now
correct by construction, since a failed program's package never reaches it.

W5 (src/build/prepare/xlings.cpp): ensure_project_index_dir's two calls
under a private work_dir are collapsed into the one call the
ownerRoot==workRoot branch always made, targeting workRoot in every case.
Previously the runtime-environment half (deps/subos/workspace) went to
runtimeSelection.ownerRoot, which is always the real project root
regardless of emit's private work_dir -- so `emit build-database` wrote
<root>/.mcpp/.xlings.json into a project that declares [xlings] deps.

SPEC-005: R3.7 names device sources beside NASM units (both absent from S1
and compile_commands.json, for different reasons -- NASM is a compile unit
excluded from export, a device source is never a compile unit at all). R5.2
gains the sentence that a check whose premise is a build program's
directives does not run for a package whose program failed in this pass.
R2.1 needed no change.

Tests: e2e 808 (device source: no dead ninja edge, absent from both
databases, and the rule still compiles it and the build still runs), e2e
809 (a device source plus a build.mcpp that does not compile: PROGRAM_FAILED
with path build.mcpp, no device-source mention, package still described),
e2e 817 (688's project-tree digest repeated on a stub-xlings fixture with
[xlings] deps: byte-identical tree, no .mcpp/.xlings.json, no write-project
effect, and the private work directory does gain one naming the
dependency). Each fails against the released 2026.9.27.1 binary and passes
on the fresh build. Full regression: all 15 emit/build-database e2e
scripts, 798, and four more that exercise the compile-unit loop (asm/GAS,
NASM, object-path-collision, multi-module) all still pass; `mcpp test`
(130 unit tests) passes.
A set's ide.generated lists each output of its package's source-role
actions (header or source, with the step's id, inputs, arguments and
work directory) and each generated include directory its units name,
with the path the document names and the path a mcpp build of the same
selection writes. The profile version is 0.3.0 (Sunrisepeak/
mcpp-language-server#28); compile_commands.json is unchanged. SPEC-005
R3.12, docs 50; e2e 815, e2e 688 reads the new version.
- ProgressBar prints one start line and one finish line when stdout is not
  a terminal: no carriage return, no erase sequence, no repaint per frame;
  an item that does not complete says so instead of reporting it done.
- The index refresh runs through xlings interface update_packages and draws
  its progress and download events with the same renderer; an xlings that
  emits none shows its status line as before, and its terminal text no
  longer reaches the output.
- The clone of a git dependency passes --progress and draws the download
  phase, read as it is redrawn (run_streaming_bounded gains an opt-in rule
  that a lone carriage return ends a line); the output is kept whole for
  the failure message, and a clone silent for fifteen minutes is stopped.
- The sandbox bootstrap's hand-drawn spinner is the shared bar.
e2e stubs accept the interface refresh; unit tests for the git progress
parser, both render modes and the line splitting; e2e 816; docs 09.
[target.<selector>.build] dialect_cxxflags is now accepted, parsed into a
new ConditionalConfig member kept apart from BuildInputs (the key is
graph-wide, not a per-package additive input), and merged into the same
BuildConfig::dialectCxxflags every consumer already reads: the std BMI
prebuild, the scan, every TU and the fingerprint. Only the root of the
build renders it; a dependency's own value (conditional or not) reaches
no command and is now excluded from its own fingerprint contribution,
matching the rule that a key enters a fingerprint only where it reaches
a command. The build-program directive is deliberately not added.

SPEC-004 SS3.1 and SS9 item 10 state the new rule; docs/04 and docs/zh/04
document the conditional form (since 2026.9.28.1). Unit tests cover
parsing, the emptiness gate, and root-only resolution order. e2e 813
covers reach into the std BMI/scan/TUs, a non-matching selector, the
A-B-A std BMI rebuild, and a dependency's key being fingerprint-inert;
it fails on the released 2026.9.27.1 binary and passes on the fresh one.
The one-writer rule passed the deploy list's names to place-dlls on its
command line. The plan's deploy set reads runtime search directories that
a prepare action fills, so it differs between the first and the second
plan; the command changed and every build after the first re-ran the
placement (e2e 797, found by a differential run against 2026.9.27.1).
place-dlls now treats a DLL beside the program that it did not place,
and that a runtime search directory also offers, as another writer's;
the command is the one 2026.9.27.1 wrote.
@speak-agent speak-agent changed the title 2026.9.27.2: on Windows an xlings invocation leaves the process environment as it found it and starts in the registry's home (#726) 2026.9.28.1: #717-#726 in one round (in progress) Sep 27, 2026
Every MSVC-ABI row now receives the same CRT model: cl spells it /MT or
/MD, clang++ targeting *-windows-msvc spells it -fms-runtime-lib=static
or =dll. One helper (msvc_abi_crt_word, dialect.cppm) decides the word
for the translation units, the std/std.compat BMIs and the link command
alike, closing #649 E10 (a compile-only flag never reached the clang
driver's own link-time choice of -defaultlib:).

toolchain-coupled (the dynamic CRT, with the toolset's own
vcruntime140.dll/msvcp140.dll staged beside the artifact) is now the
default for every role on this ABI (dist::msvc_abi_default_contract,
ContractStatement::msvcAbiDefault). A toolset with no VC\Redist\MSVC
directory defaults to host-coupled silently and refuses an explicit
toolchain-coupled, naming the missing directory (prepare/plan.cpp). A
free-form CRT word in cxxflags/dialect_cxxflags is checked against the
resolved model: agreeing is a warning, contradicting is a refusal
(dialect::check_crt_word, wired in prepare/scan.cpp).

The toolset's redistributable directory is carried as its own
Toolchain field (msvcRedistDir), populated for cl from vc_redist_dir
and for the LLVM row from its sysroot's tools directory
(vc_redist_dir_for_tools_dir) rather than from linkRuntimeDirs, which
holds LLVM's own runtime directories on that row. The staging gate and
the mcpp run/test search path both read this field, gated on the MSVC
ABI rather than on which compiler is in use. mcpp pack carries the
staged DLLs by default; an explicit --mode system now resolves a
defaulted (never-declared) toolchain-coupled contract to host-coupled
instead of refusing.

e2e 703 is inverted to the new default; e2e 814 covers the LLVM row's
import table, staged DLL, clean-PATH run, self-contained round trip,
BMI switching (A, B, A) and pack modes (Windows-only, unverified here).
Unit tests cover the CRT-word derivation, the free-form-word check, the
MSVC-ABI default/redistributable resolution and a compute_flags-level
property test across both rows; the link-line half of that test is
gated on mcpp.platform.is_windows, since link_shape resolves
LinkShape::PeLld only when current_link_host() is Windows and a
Linux-built mcpp cannot reach that branch regardless of the plan's
target triple.

docs/20, docs/zh/20 and SPEC-006 record the new default and the
upgrade; docs/04 needed no change (it only points at docs/20).

131/131 unit tests pass on Linux (gcc 16.1.0 and llvm 22.1.8 rows);
docs structure/style checks pass.
…; place-dlls comment matches its command; docs/20 names the --mode system refusal
Verbatim extraction along the sections its own banners already name:
prebuilt dependencies, link forms, make_plan, the C++ runtime checks,
graph/schedule, declared build-graph actions, assembly units, Windows
resources, the global dependency cache, mcpp.lock, runtime provider
overrides, ABI enforcement, resolution.json, and the empty-link check.
Longest resulting function: 356 lines (step13_build_graph_actions).
…steps

The worklist step for one item is split into identity resolution, the
already-resolved / identity-adoption handling (with its own version-merge
sub-step), acquiring a fresh dependency's source and manifest, and
finalizing it (recording the package, recursing into children); the
per-item locals that cross those boundaries move into a phase-local
struct, WorklistItemCtx, the same PrepareState pattern one level deeper.
The post-loop cycle check is its own function. Preamble closures that
captured only `state` (or nothing) become static file-scope functions,
called with an explicit PrepareState& where they used to close over it;
this also fixes the one comment that had gone factually stale (activateFeatures's
group banner said 'defined as local lambdas, not file-scope functions' --
now they are file-scope, and are still safe because a static function
carries no external linkage into the module's exported interface).
Longest resulting function: 300 lines (step4b_identity_version_merge).
…) into sub-steps

Along the sections its own banners already name: feature activation,
device extensions and rule application, the graph's [xlings.workspace]
provisioned before build.mcpp, host-module registration, host-tool
provisioning, the dependencies' build programs, and capability binding.
aggregatedRequest (needed by two of these sections) is promoted from a
local lambda to a file-scope function of PrepareState&. Two scoping
braces that had no matching close within their own section (opened to
wrap several sections at once) are dropped as redundant once the
content is distributed across separate functions, each of which
supplies its own scope.
Longest resulting function: 425 lines (step6_provision_host_tools).
…b-steps

Gather the graph's target-side candidates into a phase-local struct
(TargetSideGather, the PrepareState pattern one level deeper), then
resolve and realise [c-abi], broadcast the include set, check kernel-abi
interfaces and layer requirements, apply the layer-conditional config
(L1b), decide each dependency's link form (#519), define the
graph_package_entry closure, run the root build.mcpp (L3), require every
device source to reach an action, and handle re-run inputs (R1.3). Two
scoping braces that wrapped several sections at once are dropped as
redundant, as in the two previous files.
Longest resulting function: 404 lines (step9_kernel_abi_interfaces_and_requirements).
…-steps

The closures phase4a_graph_load assigns onto state (each captures only
state) split into two groups: split/identity closures, and candidate
selection closures. LoadedDep is hoisted to file scope so it stays
visible to state.loadVersionDep, which remains where it was.

state.loadVersionDep itself (509 lines) is not split further: its six
local closures (readLuaContent, findRawInstalled,
installedLayoutMatchesIndex, revisionIsCurrent, findCompleteInstalled,
markInstalled) mutually capture nine-odd shared locals by reference:
hoisting them to free functions would mean threading all of that
through explicit parameter lists for one closure, judged higher risk
than benefit within this round; noted as a residual in the T6 report.
CI (PR #727):
- e2e 190/191 select the program by name; bin/ also holds the staged
  redistributable DLLs on an MSVC-ABI row (#718).
- A DLL found in a runtime search directory yields to a declared deploy of
  the same name, and a difference is warned at planning, where a successful
  build shows it (SPEC-007 R4.3, e2e 811; e2e 818 is its Linux-hosted form).
- build.mcpp that imports only a build rule compiles in the build directory
  under GCC (e2e 807).
- The runtime environment half of .xlings.json goes to the runtime's owner
  again; only plan_only redirects it to the planning directory (e2e 205, 817).

Review:
- Every set of a package names what its build program generates (R3.12).
- A dependency's contradicting CRT word is refused; debug CRT words are
  refused; the agreeing-word message names the key of the dynamic model; the
  dependency cache key carries the CRT word.
- PE deploy destinations compare without case; MSVC version directories
  compare numerically; a missing deploy source is named as missing.
- A failed index refresh draws "did not complete"; an automatic refresh that
  exhausts its retries warns; the floor tip names the version and the
  install-aware upgrade.
- -p compares paths as paths; the help names the qualified form.
…p) into sub-steps

Split at phase1's own banner boundaries: the closures phase1 assigns onto
state, the target/--static override resolution, and the device axis plus
the L1 conditional-section merge.

phase2_define_toolchain_resolver is not split: it is a single stored
closure, state.resolve_target_toolchain, whose ~1000-line body is one
sequential toolchain-resolution flow with dozens of interdependent
locals -- the same category of residual as state.loadVersionDep in
graph_load.cpp, noted in the T6 report. step1_target_and_static_overrides
(524 lines) is a smaller residual of the same kind.
…phase3_xlings_before_graph

Both were found over ~400 lines by the same sweep that produced the
seven functions #722 named (the issue's own "any other function under
src/build/prepare/ over ~400 lines"), and split cleanly along their own
banners: phase0's own "Workspace handling" section becomes
step0_workspace_handling (108 lines; phase0 itself drops to 372); the
host-toolchain closures phase3 assigns onto state, plus the index-refresh
section, become step3_define_host_tc_closures_and_refresh_index (298
lines; phase3 itself drops to 235).
…o CI yet

.github/tools/check_function_sizes.sh runs clang-tidy's
readability-function-size (LineThreshold=400) over the compile database
mcpp produces for its own LLVM build (mcpp build --toolchain
llvm@22.1.8), restricted to files under src/build/prepare/. clang-tidy
is not part of the plain xim:llvm payload; it ships in the sibling
xim:llvm-tools package at the same version, which the script locates
under the xlings store.

Measured: at 7ccbc8d (before this round's split) it reports 10
functions over 400 lines -- the seven #722 named, plus
phase0_manifest_and_workspace, phase11_scan and
phase3_xlings_before_graph, found by the same sweep. After this round's
split it reports 6: step6_provision_host_tools (421),
phase4a_graph_load (518, its loadVersionDep closure), phase11_scan (773,
untouched), step9_kernel_abi_interfaces_and_requirements (401),
step1_target_and_static_overrides (522) and
phase2_define_toolchain_resolver (1006, untouched) -- see the T6 report
for why each remains.

Not wired into CI next to check_file_lengths.sh: it does not yet pass,
so adding the workflow step now would land a gate red on day one. Wire
it once the remaining residuals are split in a follow-up; the file
gate stays the only enforced one for this round, per the design's own
fallback for a working tool over an incomplete split.
…under the file-length limit after the merge; byte-identical on the seven fixtures)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant