diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index 5d6a493..5ffaa17 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -554,6 +554,12 @@ Configuration* Crafter::ProjectDependency(std::string_view name) { throw std::runtime_error(std::format("ProjectDependency('{}'): not declared by {} (declared: {})", name, frame.project.string(), declared.empty() ? "none" : declared)); } +bool Crafter::IsTopLevelProject() { + // One frame is the project file crafter-build was pointed at; a second + // is only ever pushed for a dependency's project file. + return LoadingFrames.size() <= 1; +} + #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) CommandResult Crafter::RunCommandChecked(std::string_view cmd) { std::array buffer; diff --git a/interfaces/Crafter.Build-Platform.cppm b/interfaces/Crafter.Build-Platform.cppm index c120cc0..0b37177 100644 --- a/interfaces/Crafter.Build-Platform.cppm +++ b/interfaces/Crafter.Build-Platform.cppm @@ -41,6 +41,15 @@ namespace Crafter { // Throws when no such dependency was declared, or when called from // anywhere other than inside LoadProject. export CRAFTER_API Configuration* ProjectDependency(std::string_view name); + // True while the project file crafter-build was pointed at runs its + // CrafterBuildProject, and when no project file is running at all; false + // inside a dependency's project file (a `#pragma crafter dependency`, + // GitProject or LocalProject target). Wire anything only the project's + // own build or test run needs behind it. The case that forced it: a + // library's test links crafter-build itself, so when crafter-build loads + // that library as a dependency, the test's GitProject loads crafter-build's + // project file in turn and the two recurse until the cycle guard throws. + export CRAFTER_API bool IsTopLevelProject(); // Resolves the directory holding distributed runtime assets (Crafter.Build // module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise // derives /share/crafter-build from the running executable's path. diff --git a/tests/ProjectDependency/fixture/app/project.cpp b/tests/ProjectDependency/fixture/app/project.cpp index 329f662..b6744d4 100644 --- a/tests/ProjectDependency/fixture/app/project.cpp +++ b/tests/ProjectDependency/fixture/app/project.cpp @@ -16,5 +16,8 @@ extern "C" Configuration CrafterBuildProject(std::span) // Once by the module it exports, once by its configuration name; the // test expects the same object back for both. cfg.dependencies = { ProjectDependency("Greeter"), ProjectDependency("greeter") }; + // Reported through a flag the test can read back: this file is the one + // LoadProject was pointed at, the greeter's is not. + cfg.compileFlags.push_back(IsTopLevelProject() ? "-DTOP_LEVEL=1" : "-DTOP_LEVEL=0"); return cfg; } diff --git a/tests/ProjectDependency/fixture/greeter/project.cpp b/tests/ProjectDependency/fixture/greeter/project.cpp index 82eb6c1..f9dcbbf 100644 --- a/tests/ProjectDependency/fixture/greeter/project.cpp +++ b/tests/ProjectDependency/fixture/greeter/project.cpp @@ -19,5 +19,7 @@ extern "C" Configuration CrafterBuildProject(std::span a std::array ifaces = { "interfaces/Greeter" }; std::array impls = {}; cfg.GetInterfacesAndImplementations(ifaces, impls); + // Loaded as a dependency, so this must come out false (see app/). + cfg.compileFlags.push_back(IsTopLevelProject() ? "-DTOP_LEVEL=1" : "-DTOP_LEVEL=0"); return cfg; } diff --git a/tests/ProjectDependency/main.cpp b/tests/ProjectDependency/main.cpp index 3d80370..fcf1c57 100644 --- a/tests/ProjectDependency/main.cpp +++ b/tests/ProjectDependency/main.cpp @@ -92,11 +92,14 @@ int main(int argc, char** argv) { const fs::path appSo = scratch / "app" / "build" / "project.so"; std::array 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) {