feat(external): forward a project's args to its sibling projects by default

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>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 02:56:14 +01:00 • committed by jorijnvdgraaf
commit 17021c6055
2 changed files with 48 additions and 13 deletions

View file

@ -413,6 +413,10 @@ namespace {
struct ProjectFileFrame {
fs::path project;
// What LoadProject was given, for ProjectArgs(). A view is enough:
// the caller's storage outlives LoadProject, and the frame is popped
// before LoadProject returns.
std::span<const std::string_view> args;
std::vector<Configuration*> dependencies;
};
@ -426,7 +430,10 @@ namespace {
// already on the stack is a cycle, which used to recurse forever.
class ProjectFileGuard {
public:
explicit ProjectFileGuard(const fs::path& absProject) {
// The args are recorded with the frame rather than just before
// CrafterBuildProject runs, so a spec built while dlopen runs the
// project file's static initialisers sees them too.
ProjectFileGuard(const fs::path& absProject, std::span<const std::string_view> args) {
for (const ProjectFileFrame& frame : LoadingFrames) {
if (frame.project != absProject) continue;
std::string chain;
@ -434,7 +441,7 @@ namespace {
throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string()));
}
index_ = LoadingFrames.size();
LoadingFrames.push_back({ absProject, {} });
LoadingFrames.push_back({ absProject, args, {} });
}
~ProjectFileGuard() { LoadingFrames.pop_back(); }
ProjectFileGuard(const ProjectFileGuard&) = delete;
@ -451,8 +458,9 @@ namespace {
// same PCM cache LoadProject populates — the hostConfig LoadProject itself
// compiles project.so with, so the dependency's std/Crafter.Build PCMs
// land in the cache dir project.so is built against. Recursive, because
// the dependency's project.cpp forwards no arguments but still reads
// CRAFTER_BUILD_MARCH from the environment for its own deps.
// the dependency's project.cpp is loaded with no arguments and so passes
// none on, while its own deps still read CRAFTER_BUILD_MARCH from the
// environment.
void ForceHostTooling(Configuration& cfg, const Configuration& host, std::unordered_set<Configuration*>& seen) {
if (!seen.insert(&cfg).second) return;
cfg.target = host.target;
@ -495,10 +503,16 @@ namespace {
std::set<std::string> libs;
for (const PragmaDependency& pragma : pragmas) {
// Host tooling is loaded with none of the consumer's args, so both
// specs pass `.args = {}` instead of defaulting to them. The tool
// is built for the host whatever the consumer targets: given the
// consumer's --target=, its project.cpp would pick sources and
// flags for the wrong triple before ForceHostTooling resets the
// fields, and the consumer's own flags would hash it apart from
// the build every other consumer of the tool shares.
Configuration* cfg = nullptr;
if (IsGitSource(pragma.source)) {
GitProjectSpec spec;
spec.source = { pragma.source, pragma.branch, pragma.commit };
GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit }, .args = {} };
if (!pragma.project.empty()) spec.projectFile = pragma.project;
cfg = GitProject(spec);
} else {
@ -512,7 +526,7 @@ namespace {
PragmaError(absProject, pragma.line, "'project' only applies to a directory source");
}
if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
cfg = LocalProject({ .projectFile = local });
cfg = LocalProject({ .projectFile = local, .args = {} });
}
if (std::ranges::contains(out.configs, cfg)) continue;
@ -580,6 +594,12 @@ bool Crafter::IsTopLevelProject() {
return LoadingFrames.size() <= 1;
}
std::vector<std::string> Crafter::ProjectArgs() {
if (LoadingFrames.empty()) return {};
const std::span<const std::string_view> args = LoadingFrames.back().args;
return std::vector<std::string>(args.begin(), args.end());
}
namespace {
// The one reading of a wait status (pclose, system) for every helper
// that runs a command: a signal death is reported the way a shell
@ -882,7 +902,7 @@ namespace {
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
RequireToolchain();
fs::path absProject = fs::canonical(projectFile);
ProjectFileGuard guard(absProject);
ProjectFileGuard guard(absProject, args);
fs::path buildDir = absProject.parent_path() / "build";
if (!fs::exists(buildDir)) {
fs::create_directories(buildDir);

View file

@ -71,16 +71,31 @@ 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.
std::vector<std::string> ProjectArgs();
// Specification for a sibling crafter-build project to fetch and depend on.
// GitSource picks the revision: leave branch + commit empty for the
// remote's default branch HEAD (sloppy but convenient), set branch to
// track a branch tip, or set commit to pin to an immutable SHA. args are
// forwarded verbatim to the dep's CrafterBuildProject — typically you
// pass through the parent's args so target/march/debug propagate.
// what the dep's CrafterBuildProject receives. They default to this
// project's own, so --target=, --march=, --debug and flags of its own
// such as --local reach a whole chain of sibling projects without being
// passed. Set them to forward something else: a copy without one of this
// project's flags, one with an extra --target=, or {} for nothing.
struct GitProjectSpec {
GitSource source;
fs::path projectFile = "project.cpp";
std::vector<std::string> args;
std::vector<std::string> args = ProjectArgs();
};
// Clones the spec's git URL into the per-project external cache (keyed by
@ -94,8 +109,8 @@ export namespace Crafter {
// example folder depending on its parent project, or a workspace where
// multiple projects live side-by-side without going through git).
struct LocalProjectSpec {
fs::path projectFile; // relative to cwd or absolute
std::vector<std::string> args; // forwarded to dep's CrafterBuildProject
fs::path projectFile; // relative to cwd or absolute
std::vector<std::string> args = ProjectArgs(); // as GitProjectSpec::args
};
// Same as GitProject but for a local on-disk project — no fetch, no