feat(external): forward a project's args to its sibling projects by default; lint with the lite rules #40

Closed
jorijnvdgraaf wants to merge 0 commits from claude/forward-project-args into master

Two changes: sibling projects get the parent's args without the depArgs copy, and this tree is linted with the lite house rules. The last seven commits fix what review of the first five found.

Forward a project's args to its sibling projects by default

Every project.cpp with a sibling dependency opened with

std::vector<std::string> depArgs(args.begin(), args.end());

and passed .args = depArgs to each GitProject/LocalProject. There were about 45 copies across the Catcrafts repos. CrafterBuildProject gets a span of string_views and the specs wanted owned strings, so the copy was needed. A spec without it built the sibling with none of the project's flags: for the host while the project cross-compiled, and in release under --debug.

The fix: both specs now default args = ProjectArgs(), which returns the args the running CrafterBuildProject was loaded with. That is the same shape as Configuration::target = HostTarget(). The value comes from the per-thread stack LoadProject already keeps for IsTopLevelProject(), whose frames now record the args.

Configuration* math = GitProject({ .source = { "https://…/Crafter.Math.git" } });   // forwards
… .args = filtered   // replaces, e.g. Network without --no-quic
… .args = {}         // forwards nothing
  • Existing project files: the member type is unchanged, so an explicit .args = depArgs means exactly what it did. The one call in the Catcrafts repos without .args is Crafter.LintRules' GitProject of this repo, since 7d93289. It now forwards LintRules' args, as it did before 42a2701, except that --local is no longer removed first.
  • When the args are there: ProjectArgs() is filled in just before CrafterBuildProject is called, and is empty everywhere else. That covers the #pragma crafter dependency pre-pass, so build tools get none of the consumer's args without any opt-out. It also covers the project file's static initialisers, which run only on a file's first load.
  • Where ProjectArgs() is declared: in :External, not :Platform. Importing :Platform there would make the exported struct Configuration; an exported redeclaration of :Platform's unexported one, and clang 23 rejects that.
  • Older releases: a project.cpp without .args still compiles on crafter-build ≤ 2.2.0, but there it forwards nothing. So the sibling repos keep their copies until a release with this change is on stable (list below).

Lint with LintRules::AddLiteRules

Crafter.LintRules now has a lite set: the 8 rules that ask for different code, without the layout, whitespace and naming rules. project.cpp switches to it.

With the full set, lint failed with 49 naming findings. The rule now wants camelCase statics, and this tree spells them PascalCase (Failures, LoadingFrames, …). With the lite set, lint and format --check are clean. This also stops checking eight layout and whitespace rules the tree already passes: tabs, trailing whitespace, final newline, brace style, one declaration per statement, single-line if, wrapping and paren spacing.

AGENTS.md "Code style" now says which rules lint enforces, that format only rewrites string concatenation and builtin integer types, and that layout and naming are kept by hand.

Related: Crafter.LintRules master did not compile after 42a2701: it still used .args = depArgs after the declaration was removed. Every load of this repo's project.cpp, and so CI, would have failed. The fix (7d93289) is already pushed to its master.

Fixes from review

  • Args only for the CrafterBuildProject call. The frame used to get the args when it was pushed. So the pragma pre-pass needed a hand-written .args = {} on both of its specs. And a spec made at namespace scope kept the args of the file's first load for every later load, because dlopen runs static initialisers once. Now the args are set just before the call, and the dependencies the pre-pass resolved just before dlopen. ProjectDependency() from a static initialiser used to find no dependencies and terminate the process.
  • Host tooling keeps its own project-cache entries. This bug predates the branch. A project and its #pragma crafter dependency that both load one sibling with the same args shared a single Configuration. ForceHostTooling rewrote it to host settings in place, so the project built against -march=native BMIs. A per-thread count of pragma loads now goes into the cache key. Shared clones stay shared.
  • -- ends crafter-build's options. This also predates the branch. The help has always advertised [-- project-args...], but -- itself landed in the project args. It changed the variant hash of the project and, with this branch, of every sibling. The words after it were still read as crafter-build's own options. After --, every word is now an operand: a project arg, a glob after test, lint or format, the script and its args after run.
  • Tests: a new HostToolingSibling test, -- cases in Script, and a namespace-scope spec case in ProjectArgs. That test now checks through one helper that prints what each sibling got. ProjectDependency gains a static-initialiser case in a child process. Its args check is folded into the first load, because the separate load's check could never fail and its early return skipped every later check.
  • Docs: the README no longer suggests filtering one sibling's list; it says to give a changed list to every sibling (see #41). It also says when the default is read, and that cfg.projectArgs is no substitute for ProjectArgs(). The spec comment no longer names --local as a flag that carries down a chain, and the GitProject comment no longer says the clone is keyed by the args. AGENTS.md notes that a stale harness gives no symptom for a changed default member initialiser.

#41, two builds of one sibling ending up in one program, gets its own branch and PR.

Testing

  • format and lint are clean with the 8 lite rules.
  • Full suite: 36 passed, 2 skipped. WasmHelloRuns skips because wasmtime isn't on PATH, and ConcurrentCacheRace skips because no installed share dir is found. It passes when run with CRAFTER_BUILD_HOME=$PWD/share/crafter-build.
  • tests/ProjectArgs covers a LocalProject chain app → lib → leaf:
    • Loads without .args inherit the args, transitively.
    • .args = {} gets nothing, and an explicit list replaces the args.
    • The GitProjectSpec default is checked on the spec itself.
    • A spec made at namespace scope forwards nothing.
  • Each new check fails with its fix taken out:
    • Without the host-tooling key, HostToolingSibling fails 3 checks.
    • With -- falling through to the project args, both Script checks fail.
    • With the args set before dlopen, the namespace-scope check fails with projectArgs ["--flavor=x"].
    • With the dependencies set after dlopen, the static-initialiser load exits 134 ("declared: none").
    • With the args set when the frame is pushed, as in the first commits, the pragma dependency gets ["--flavor=x"].
  • ./bin/crafter-build and ./bin/crafter-build --debug build with the depArgs-free project.cpp.

Needs ./build.sh after pulling: interfaces changed, and project.cpp reaches the new exported ProjectArgs symbol.

Follow-up after the next release

  • Remove the copy and its .args:
    • Asset, AI, AI.Coding, Graphics.
    • The inline copies in the Graphics and Graphics-imsd-messages examples (HelloWindow, HelloUI, Decompression, InputSystem, CustomShader).
    • The graphicsArgs copies (VulkanTriangle, RTStress, Sponza, SponzaBench, RayQueryPick, HDRBloom, RTMultiShadow, RTVolume).
  • Keep the copies that forward a different list:
    • Network drops --no-quic.
    • Graphics-imsd-messages drops --local.
    • HelloDom and SimpleClient add --target=.

🤖 Generated with Claude Code

Two changes: sibling projects get the parent's args without the `depArgs` copy, and this tree is linted with the lite house rules. The last seven commits fix what review of the first five found. ## Forward a project's args to its sibling projects by default Every `project.cpp` with a sibling dependency opened with ```cpp std::vector<std::string> depArgs(args.begin(), args.end()); ``` and passed `.args = depArgs` to each `GitProject`/`LocalProject`. There were about 45 copies across the Catcrafts repos. `CrafterBuildProject` gets a span of string_views and the specs wanted owned strings, so the copy was needed. A spec without it built the sibling with none of the project's flags: for the host while the project cross-compiled, and in release under `--debug`. The fix: both specs now default `args = ProjectArgs()`, which returns the args the running `CrafterBuildProject` was loaded with. That is the same shape as `Configuration::target = HostTarget()`. The value comes from the per-thread stack `LoadProject` already keeps for `IsTopLevelProject()`, whose frames now record the args. ```cpp Configuration* math = GitProject({ .source = { "https://…/Crafter.Math.git" } }); // forwards … .args = filtered // replaces, e.g. Network without --no-quic … .args = {} // forwards nothing ``` - **Existing project files:** the member type is unchanged, so an explicit `.args = depArgs` means exactly what it did. The one call in the Catcrafts repos without `.args` is Crafter.LintRules' `GitProject` of this repo, since `7d93289`. It now forwards LintRules' args, as it did before `42a2701`, except that `--local` is no longer removed first. - **When the args are there:** `ProjectArgs()` is filled in just before `CrafterBuildProject` is called, and is empty everywhere else. That covers the `#pragma crafter dependency` pre-pass, so build tools get none of the consumer's args without any opt-out. It also covers the project file's static initialisers, which run only on a file's first load. - **Where `ProjectArgs()` is declared:** in `:External`, not `:Platform`. Importing `:Platform` there would make the exported `struct Configuration;` an exported redeclaration of `:Platform`'s unexported one, and clang 23 rejects that. - **Older releases:** a `project.cpp` without `.args` still compiles on crafter-build ≤ 2.2.0, but there it forwards nothing. So the sibling repos keep their copies until a release with this change is on stable (list below). ## Lint with `LintRules::AddLiteRules` Crafter.LintRules now has a lite set: the 8 rules that ask for different code, without the layout, whitespace and naming rules. `project.cpp` switches to it. With the full set, `lint` failed with 49 `naming` findings. The rule now wants camelCase statics, and this tree spells them PascalCase (`Failures`, `LoadingFrames`, …). With the lite set, `lint` and `format --check` are clean. This also stops checking eight layout and whitespace rules the tree already passes: tabs, trailing whitespace, final newline, brace style, one declaration per statement, single-line `if`, wrapping and paren spacing. AGENTS.md "Code style" now says which rules `lint` enforces, that `format` only rewrites string concatenation and builtin integer types, and that layout and naming are kept by hand. Related: Crafter.LintRules `master` did not compile after `42a2701`: it still used `.args = depArgs` after the declaration was removed. Every load of this repo's `project.cpp`, and so CI, would have failed. The fix (`7d93289`) is already pushed to its master. ## Fixes from review - **Args only for the `CrafterBuildProject` call.** The frame used to get the args when it was pushed. So the pragma pre-pass needed a hand-written `.args = {}` on both of its specs. And a spec made at namespace scope kept the args of the file's first load for every later load, because `dlopen` runs static initialisers once. Now the args are set just before the call, and the dependencies the pre-pass resolved just before `dlopen`. `ProjectDependency()` from a static initialiser used to find no dependencies and terminate the process. - **Host tooling keeps its own project-cache entries.** This bug predates the branch. A project and its `#pragma crafter dependency` that both load one sibling with the same args shared a single `Configuration`. `ForceHostTooling` rewrote it to host settings in place, so the project built against `-march=native` BMIs. A per-thread count of pragma loads now goes into the cache key. Shared clones stay shared. - **`--` ends crafter-build's options.** This also predates the branch. The help has always advertised `[-- project-args...]`, but `--` itself landed in the project args. It changed the variant hash of the project and, with this branch, of every sibling. The words after it were still read as crafter-build's own options. After `--`, every word is now an operand: a project arg, a glob after `test`, `lint` or `format`, the script and its args after `run`. - **Tests:** a new `HostToolingSibling` test, `--` cases in `Script`, and a namespace-scope spec case in `ProjectArgs`. That test now checks through one helper that prints what each sibling got. `ProjectDependency` gains a static-initialiser case in a child process. Its args check is folded into the first load, because the separate load's check could never fail and its early return skipped every later check. - **Docs:** the README no longer suggests filtering one sibling's list; it says to give a changed list to every sibling (see #41). It also says when the default is read, and that `cfg.projectArgs` is no substitute for `ProjectArgs()`. The spec comment no longer names `--local` as a flag that carries down a chain, and the `GitProject` comment no longer says the clone is keyed by the args. AGENTS.md notes that a stale harness gives no symptom for a changed default member initialiser. #41, two builds of one sibling ending up in one program, gets its own branch and PR. ## Testing - `format` and `lint` are clean with the 8 lite rules. - Full suite: 36 passed, 2 skipped. `WasmHelloRuns` skips because wasmtime isn't on PATH, and `ConcurrentCacheRace` skips because no installed share dir is found. It passes when run with `CRAFTER_BUILD_HOME=$PWD/share/crafter-build`. - `tests/ProjectArgs` covers a LocalProject chain `app → lib → leaf`: - Loads without `.args` inherit the args, transitively. - `.args = {}` gets nothing, and an explicit list replaces the args. - The `GitProjectSpec` default is checked on the spec itself. - A spec made at namespace scope forwards nothing. - Each new check fails with its fix taken out: - Without the host-tooling key, `HostToolingSibling` fails 3 checks. - With `--` falling through to the project args, both `Script` checks fail. - With the args set before `dlopen`, the namespace-scope check fails with `projectArgs ["--flavor=x"]`. - With the dependencies set after `dlopen`, the static-initialiser load exits 134 ("declared: none"). - With the args set when the frame is pushed, as in the first commits, the pragma dependency gets `["--flavor=x"]`. - `./bin/crafter-build` and `./bin/crafter-build --debug` build with the depArgs-free `project.cpp`. Needs `./build.sh` after pulling: interfaces changed, and `project.cpp` reaches the new exported `ProjectArgs` symbol. ## Follow-up after the next release - **Remove the copy and its `.args`:** - Asset, AI, AI.Coding, Graphics. - The inline copies in the Graphics and Graphics-imsd-messages examples (HelloWindow, HelloUI, Decompression, InputSystem, CustomShader). - The `graphicsArgs` copies (VulkanTriangle, RTStress, Sponza, SponzaBench, RayQueryPick, HDRBloom, RTMultiShadow, RTVolume). - **Keep the copies that forward a different list:** - Network drops `--no-quic`. - Graphics-imsd-messages drops `--local`. - HelloDom and SimpleClient add `--target=`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Every project.cpp with a sibling dependency opened with the same line,

    std::vector<std::string> depArgs(args.begin(), args.end());

and handed depArgs to each GitProject and LocalProject. CrafterBuildProject
receives a span of string_views and the specs wanted owned strings, and
without the copy the sibling got none of the project's args: it was built
for the host while the project cross-compiled, and in release while it
built --debug. GitProject then turned the copy straight back into
string_views for LoadProject.

Both specs now default their args to ProjectArgs(), the args LoadProject
was given for the project file whose CrafterBuildProject is running. It
reads the per-thread stack LoadProject already keeps for IsTopLevelProject
and ProjectDependency, whose frames now record the args. A default member
initializer rather than a new type or an optional: the member stays a
std::vector<std::string>, so every existing `.args = depArgs` keeps its
exact meaning (each of the ~50 calls across the Catcrafts projects passes
one), and `.args = {}` still forwards nothing. It is the shape of
Configuration::target = HostTarget(). A project.cpp that drops its .args
needs a crafter-build with this change: an older one compiles it and
forwards nothing.

ProjectArgs() is declared in :External, next to the defaults that call
it. :Platform, where it is defined, cannot be imported there: its
unexported `struct Configuration;` would make :External's exported one an
ill-formed redeclaration, which clang rejects.

The #pragma crafter dependency path now passes `.args = {}` explicitly.
Host tooling is built for the host whatever the consumer targets and was
loaded with no args; the default would otherwise hand it the consumer's
--target= and project flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProjectArgs loads a chain app -> lib -> leaf of LocalProjects with --debug
and a project flag. lib, loaded without .args, has to see both, and so
does its own sibling, which takes the default one level further down.
Copies loaded with `.args = {}` and with an explicit list get nothing and
exactly that list. The GitProjectSpec default is checked on the spec
itself, since a GitProject would need a repository to clone. Without the
defaults, five of its checks fail.

ProjectDependency now also loads its consumer with a project flag: the
pragma dependency must still come back with no args. Without the explicit
`.args = {}` in the pragma path it gets the consumer's flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crafter.Math and Crafter.Asset now get this project's args through the
GitProjectSpec default. Loading this file therefore needs a crafter-build
that exports ProjectArgs: after pulling, run ./build.sh before the
bootstrap binary loads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crafter.LintRules now has AddLiteRules: the house rules without the ones
that only decide layout, whitespace and naming. This tree switches to it.
The full set's naming rule now wants camelCase statics and constants,
which this tree spells PascalCase throughout, so `lint` with it reported
49 findings on code that follows the documented convention; with the lite
set `lint` and `format --check` are clean.

AGENTS.md described the 17 rules as the enforced style and told
contributors to let `format` restyle their code. It now says which 8 rules
`lint` enforces, that `format` only rewrites string concatenation and
builtin integer types, and lists layout and naming as conventions kept by
hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: sibling projects forward the parent's args by default
All checks were successful
Flatpak / flatpak (pull_request) Successful in 7m15s
f60268b17a
The sibling-project example dropped its `.args = depArgs`, which no
longer has to be written, and says what a sibling receives instead: the
args the project file was loaded with, or whatever `.args` is set to,
with `{}` for nothing. It also says that releases up to 2.2.0 forward
nothing without `.args`, which matters for a project.cpp that has to
load on both.

The pragma section says that a project file's own dependency is built
for the host and gets none of the project's args, the one place the
default does not apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jorijnvdgraaf force-pushed claude/forward-project-args from f60268b17a
All checks were successful
Flatpak / flatpak (pull_request) Successful in 7m15s
to 5cf295e1d4
All checks were successful
Flatpak / flatpak (pull_request) Successful in 7m57s
2026-10-31 02:21:07 +00:00
Compare
ProjectArgs() read args that LoadProject recorded when it pushed the
frame, so they were there for everything that ran on that frame, not
only for CrafterBuildProject. Two things went wrong with that.

The pragma pre-pass runs on the frame too, so both of its specs needed
a hand-written `.args = {}` to keep the consumer's args from reaching
host tooling, and anything else built there would have needed one as
well. And dlopen runs a project file's static initialisers only on the
first load of that file: a later load gets the same mapping back. A
spec made at namespace scope therefore kept the first load's args for
every later load of the file, a silently wrong variant.

The frame now gets the args just before CrafterBuildProject is called.
The pre-pass and the static initialisers see none, on every load, so
the two opt-outs go. The dependencies the pre-pass resolved move the
other way, to just before dlopen: they are the same on every load, and
ProjectDependency() called from a static initialiser used to find none
and terminate the process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProjectArgs gets a sibling that makes its own sibling's spec at
namespace scope. Before the fix that spec took the args the file was
first loaded with; now it takes none on every load. The checks go
through one CheckLoaded helper, which prints what each sibling was
given, so a failure says which args a wrong frame handed over.

ProjectDependency loads a fixture whose static initialiser asks for its
pragma dependency, in a child, because a throw there terminates the
process: before the fix it found none. The check that the dependency
gets none of the consumer's args moves into the first load, which now
passes the app an arg it ignores; the separate load could not fail its
own check, and its early return skipped every later one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ForceHostTooling rewrites a `#pragma crafter dependency` and everything
it loads in place: host target, march native, release, -fPIC. Those are
entries of the project cache like any other, keyed by project file and
args alone. A project that loaded the same sibling as its tool, with
the same args, got the rewritten entry back. That happens whenever the
project gets no args, since host tooling gets none either: a consumer
built for x86-64-v2 then imported a sibling's BMI compiled with
-march=native, and clang refused it. Any arg on the command line split
the keys and hid it.

A thread-local count of pragma loads now marks the tool's load and
every load inside it, its project file's siblings included, and
GitProject and LocalProject put that mark in the key. The clone stays
shared; only the Configuration is separate, as for two arg lists. A
module-internal function rather than a spec field, because the
siblings of a tool are loaded by the tool's own project.cpp, which
cannot know it is host tooling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixture app has `#pragma crafter dependency "../tool"` and loads
../shared for itself, which the tool loads too; the app is loaded with
no args, the case where the two cache keys used to match. With
CRAFTER_BUILD_MARCH=x86-64-v2, the app's copy of shared has to keep
that march and no -fPIC, and the tool's copy has to be host tooling.
Before the fix the app got the tool's copy back: one Configuration,
with march native and -fPIC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The help has advertised `[options] [-- project-args...]`, but Run never
looked for `--`. It fell through to projectArgs as a word of its own,
and the words after it were still read as this tool's options and
verbs: `crafter-build -- -v` turned verbose output on. In projectArgs
`--` changed the project's variant hash, and now that siblings inherit
the args, the hash of every sibling down the chain, each building into
directories of its own that the project's clean never removes.

`--` now ends this tool's options and goes nowhere itself. Every later
word is an operand of the verb, as POSIX has it: a project arg for a
build, a glob for test, lint and format, the script's name and then its
args for run. A glob rather than a project arg after `test` because
`crafter-build test -- 'House*'` is how such a filter gets typed; a
project arg goes before the `--`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`run -- record -- a` has to run the record script with the args `--`
and `a`: the first `--` ends the options, the second comes after the
script's name and is the script's. The project args keep only what
stood before `run`. And `run -- -x` has to look for a script called
-x, since after `--` an option-like word is an operand. Before the fix
the first `--` showed up in the project args, and `-x` joined it there
while the bare `run` listed the scripts and succeeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: correct what the sibling-args docs promise
All checks were successful
Flatpak / flatpak (pull_request) Successful in 7m27s
84d9d66649
The README suggested giving one sibling a list without a flag only the
project reads, so that the sibling would not get a build directory of
its own. When another path loads the same sibling with the full list,
that filter is what gives it the second directory: two builds of one
project, both in the program (#41). It now says to build such a list
once and give it to every sibling. It also says the default is read
when the spec is made, so a spec made at namespace scope, on another
thread or in a script forwards nothing, and that cfg.projectArgs, the
part ApplyStandardArgs did not interpret, is no substitute: it drops
the target and the build type. The comments on ProjectArgs() and on
Configuration::projectArgs say the same.

The spec comment named --local as a flag the default carries down a
chain of siblings. Through a GitProject hop it breaks: the clone has no
checkouts beside it. The GitProject comment said the clone is keyed by
the args; only the Configuration is, and every arg list builds in the
one clone. AGENTS.md now notes that a stale harness gives no symptom
for a changed default member initialiser: project.cpp silently gets the
old default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Merged into master as 7514227 (Merge branch 'claude/forward-project-args'), on top of claude/dependency-notes. The conflicts were all places both branches added to; the merge commit's message lists how each was resolved. Closed by hand because the repository does not allow marking a PR as manually merged.

Merged into master as 7514227 (Merge branch 'claude/forward-project-args'), on top of claude/dependency-notes. The conflicts were all places both branches added to; the merge commit's message lists how each was resolved. Closed by hand because the repository does not allow marking a PR as manually merged.
jorijnvdgraaf closed this pull request 2026-10-31 04:32:08 +00:00
All checks were successful
Flatpak / flatpak (pull_request) Successful in 7m27s

Pull request closed

Sign in to join this conversation.
No description provided.