fix: give a project file its args only for the CrafterBuildProject call

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>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 05:09:51 +01:00
commit 060ab4380f
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 40 additions and 29 deletions

View file

@ -71,16 +71,21 @@ export namespace Crafter {
// clone can still differ in includeDirs, libDirs and libs.
ExternalBuildResult ExternalOutputs(const ExternalDependency& dep, std::string_view target);
// The args LoadProject was given for the project file currently running
// its CrafterBuildProject: what that function receives, standard flags
// included (cfg.projectArgs is only the part ApplyStandardArgs left
// over). Empty when no project file is running, and on any thread but
// the one loading it, because it reads the stack LoadProject keeps per
// thread, as IsTopLevelProject does. The specs below default their args
// to it. Defined beside LoadProject, but declared here so those defaults
// can see it: importing :Platform into this partition would make the
// exported `struct Configuration;` above a redeclaration of :Platform's
// unexported one, which clang rejects.
// The args the project file now running its CrafterBuildProject was
// loaded with: everything that function receives, --target=, --debug and
// the other standard flags included. Not cfg.projectArgs, which holds
// only what ApplyStandardArgs left uninterpreted: `.args =
// cfg.projectArgs` would drop the target and the build type. Empty
// everywhere else, because it reads the stack LoadProject keeps per
// thread, as IsTopLevelProject does: outside a project file, on any
// thread but the one running it, once CrafterBuildProject has returned
// (a script runs later), and before it is called, which covers the
// file's static initialisers and its `#pragma crafter dependency`
// pre-pass. The specs below default their args to it. Defined beside
// LoadProject, but declared here so those defaults can see it: importing
// :Platform into this partition would make the exported `struct
// Configuration;` above a redeclaration of :Platform's unexported one,
// which clang rejects.
std::vector<std::string> ProjectArgs();
// Specification for a sibling crafter-build project to fetch and depend on.