fix: let a project file ask whether it is the top-level project

Crafter.LintRules declares a test that links crafter-build, fetched
with GitProject. crafter-build's own project.cpp loads Crafter.LintRules
as a `#pragma crafter dependency`, so the chain was: crafter-build loads
LintRules, LintRules' test wiring loads crafter-build's project.cpp from
the external cache, which loads LintRules again, and the cycle guard
threw. Every command that loads project.cpp in this repository has
failed since that test was added, including the last CI run on master.

A project file had no way to tell whether it was the one the user
pointed crafter-build at or a dependency being loaded for somebody
else, so it could not skip wiring that only its own build needs.
IsTopLevelProject() answers that from the loading-frame stack
ProjectDependency() already reads: one frame is the top-level project
file, a second is only ever pushed for a dependency. Outside any project
file it is true as well, which is what a test harness constructing
Configurations expects.

The ProjectDependency test records the answer from both the app and the
greeter project files and checks each.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-02 22:02:40 +02:00
commit be88a194f4
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
5 changed files with 24 additions and 0 deletions

View file

@ -92,11 +92,14 @@ int main(int argc, char** argv) {
const fs::path appSo = scratch / "app" / "build" / "project.so";
std::array<std::string_view, 0> noArgs = {};
Check(IsTopLevelProject(), "outside any project file counts as top level");
// The dependency is fetched, built and linked before project.cpp runs,
// and ProjectDependency() hands it back by either name.
try {
Configuration cfg = LoadProject(app, noArgs);
Check(cfg.name == "hello from greeter", std::format("project.cpp imported and called the dependency's module (got '{}')", cfg.name));
Check(std::ranges::contains(cfg.compileFlags, "-DTOP_LEVEL=1"), "the project LoadProject was pointed at sees itself as top level");
Check(cfg.dependencies.size() == 2, "ProjectDependency resolved by module name and by configuration name");
if (cfg.dependencies.size() == 2) {
Check(cfg.dependencies[0] == cfg.dependencies[1], "both names resolve to the same dependency");
@ -105,6 +108,7 @@ int main(int argc, char** argv) {
Check(dep.target == HostTarget(), "the dependency was built for the host triple LoadProject uses");
Check(dep.march == "native", "the environment's march was overridden for host tooling");
Check(std::ranges::contains(dep.compileFlags, "-fPIC"), "the dependency archive was built position-independent");
Check(std::ranges::contains(dep.compileFlags, "-DTOP_LEVEL=0"), "a dependency's project file does not see itself as top level");
Check(fs::exists(ExpectedOutput(dep)), "the dependency archive exists");
}
} catch (const std::exception& e) {