diff --git a/tests/ProjectArgs/fixture/app/project.cpp b/tests/ProjectArgs/fixture/app/project.cpp index 353c592..7b51816 100644 --- a/tests/ProjectArgs/fixture/app/project.cpp +++ b/tests/ProjectArgs/fixture/app/project.cpp @@ -6,12 +6,14 @@ import Crafter.Build; using namespace Crafter; // Loads the same sibling three ways: with its args left at their default, -// with nothing forwarded, and with a list of its own. The test reads what -// each copy received back out of cfg.dependencies. +// with nothing forwarded, and with a list of its own. Then a sibling that +// makes its own sibling's spec at namespace scope. The test reads what each +// copy received back out of cfg.dependencies. extern "C" Configuration CrafterBuildProject(std::span args) { Configuration* inherited = LocalProject({ .projectFile = "../lib/project.cpp" }); Configuration* none = LocalProject({ .projectFile = "../lib/project.cpp", .args = {} }); Configuration* replaced = LocalProject({ .projectFile = "../lib/project.cpp", .args = { "--only" } }); + Configuration* fromStatic = LocalProject({ .projectFile = "../static/project.cpp" }); Configuration cfg; cfg.path = "./"; @@ -19,7 +21,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.outputName = "app"; cfg.type = ConfigurationType::Executable; ApplyStandardArgs(cfg, args); - cfg.dependencies = { inherited, none, replaced }; + cfg.dependencies = { inherited, none, replaced, fromStatic }; // GitProjectSpec takes the same default. Checked on the spec itself, // since a GitProject would need a repository to clone. const GitProjectSpec git{ .source = { .url = "unused" } }; diff --git a/tests/ProjectArgs/fixture/static/project.cpp b/tests/ProjectArgs/fixture/static/project.cpp new file mode 100644 index 0000000..84f75bd --- /dev/null +++ b/tests/ProjectArgs/fixture/static/project.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +import std; +import Crafter.Build; +using namespace Crafter; + +// Makes its sibling's spec at namespace scope, so the spec's args default +// while LoadProject dlopens this file: before CrafterBuildProject is called, +// and on the first load of the file only. +namespace { + const LocalProjectSpec LeafSpec{ .projectFile = "../leaf/project.cpp" }; +} + +extern "C" Configuration CrafterBuildProject(std::span args) { + Configuration* leaf = LocalProject(LeafSpec); + + Configuration cfg; + cfg.path = "./"; + cfg.name = "static"; + cfg.outputName = "static"; + cfg.type = ConfigurationType::LibraryStatic; + ApplyStandardArgs(cfg, args); + cfg.dependencies = { leaf }; + return cfg; +} diff --git a/tests/ProjectArgs/main.cpp b/tests/ProjectArgs/main.cpp index 4b47938..6bf4005 100644 --- a/tests/ProjectArgs/main.cpp +++ b/tests/ProjectArgs/main.cpp @@ -15,7 +15,10 @@ using namespace Crafter; // release while the project built --debug. The fixture is a chain // app -> lib -> leaf of LocalProjects; app also loads lib with an empty and // with a replaced list, and checks the GitProjectSpec default itself, which -// a clone would need a repository for. +// a clone would need a repository for. A spec made at namespace scope gets +// no args: made while dlopen runs the file's static initialisers, which +// happens on its first load only, it would otherwise keep that load's args +// for every later one. namespace { std::int32_t Failures = 0; @@ -26,10 +29,21 @@ namespace { } } - // The single sibling lib and leaf declare, or nullptr. + // The single sibling lib, static and leaf declare, or nullptr. const Configuration* OnlyDependency(const Configuration& cfg) { return cfg.dependencies.size() == 1 ? cfg.dependencies[0] : nullptr; } + + // What a sibling was loaded with, as ApplyStandardArgs split it up: + // --debug into cfg.debug, the uninterpreted rest into cfg.projectArgs. + // Printed either way, so a failure says which args the sibling read. + void CheckLoaded(const Configuration* cfg, bool debug, const std::vector& rest, std::string_view what) { + if (!cfg) { + Check(false, std::format("{} (no such configuration)", what)); + return; + } + Check(cfg->debug == debug && cfg->projectArgs == rest, std::format("{} (debug {}, projectArgs {})", what, cfg->debug, cfg->projectArgs)); + } } int main() { @@ -60,24 +74,25 @@ int main() { try { Configuration cfg = LoadProject("project.cpp", args); Check(std::ranges::contains(cfg.compileFlags, "-DGIT_DEFAULT=1"), "a GitProjectSpec's args default to the project's own"); - Check(cfg.dependencies.size() == 3, std::format("the project declared its three siblings (got {})", cfg.dependencies.size())); - if (cfg.dependencies.size() == 3) { - const Configuration& inherited = *cfg.dependencies[0]; - const Configuration& none = *cfg.dependencies[1]; - const Configuration& replaced = *cfg.dependencies[2]; - Check(inherited.debug && inherited.projectArgs == flavor, std::format("a LocalProject without .args gets the project's args (debug {}, projectArgs {})", inherited.debug, inherited.projectArgs)); - Check(!none.debug && none.projectArgs.empty(), std::format("`.args = {{}}` forwards nothing (debug {}, projectArgs {})", none.debug, none.projectArgs)); - Check(!replaced.debug && replaced.projectArgs == only, std::format("an explicit list replaces the args rather than adding to them (debug {}, projectArgs {})", replaced.debug, replaced.projectArgs)); - Check(&inherited != &none && &inherited != &replaced && &none != &replaced, "each list loads its own configuration"); + Check(cfg.dependencies.size() == 4, std::format("the project declared its four siblings (got {})", cfg.dependencies.size())); + if (cfg.dependencies.size() == 4) { + const Configuration* inherited = cfg.dependencies[0]; + const Configuration* none = cfg.dependencies[1]; + const Configuration* replaced = cfg.dependencies[2]; + const Configuration* fromStatic = cfg.dependencies[3]; + CheckLoaded(inherited, true, flavor, "a LocalProject without .args gets the project's args"); + CheckLoaded(none, false, {}, "`.args = {}` forwards nothing"); + CheckLoaded(replaced, false, only, "an explicit list replaces the args rather than adding to them"); + Check(inherited != none && inherited != replaced && none != replaced, "each list loads its own configuration"); // One sibling further down, the same default passes on what the // middle project was given, not what the top one was. - const Configuration* inheritedLeaf = OnlyDependency(inherited); - Check(inheritedLeaf && inheritedLeaf->debug && inheritedLeaf->projectArgs == flavor, "the project's args reach its sibling's sibling"); - const Configuration* noneLeaf = OnlyDependency(none); - Check(noneLeaf && !noneLeaf->debug && noneLeaf->projectArgs.empty(), "a sibling loaded with nothing passes nothing on"); - const Configuration* replacedLeaf = OnlyDependency(replaced); - Check(replacedLeaf && !replacedLeaf->debug && replacedLeaf->projectArgs == only, "a sibling passes on the list it was given"); + CheckLoaded(OnlyDependency(*inherited), true, flavor, "the project's args reach its sibling's sibling"); + CheckLoaded(OnlyDependency(*none), false, {}, "a sibling loaded with nothing passes nothing on"); + CheckLoaded(OnlyDependency(*replaced), false, only, "a sibling passes on the list it was given"); + + CheckLoaded(fromStatic, true, flavor, "the sibling with a namespace-scope spec is itself loaded with the args"); + CheckLoaded(OnlyDependency(*fromStatic), false, {}, "a spec made at namespace scope forwards nothing"); } } catch (const std::exception& e) { fs::current_path(cwd); diff --git a/tests/ProjectDependency/fixture/static-dep/project.cpp b/tests/ProjectDependency/fixture/static-dep/project.cpp new file mode 100644 index 0000000..2090db5 --- /dev/null +++ b/tests/ProjectDependency/fixture/static-dep/project.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +import std; +import Crafter.Build; +#pragma crafter dependency "../greeter" +using namespace Crafter; + +// Asks for its dependency at namespace scope, so the call runs while +// LoadProject dlopens this file, before CrafterBuildProject is called. +namespace { + Configuration* const Greeter = ProjectDependency("greeter"); +} + +extern "C" Configuration CrafterBuildProject(std::span) { + Configuration cfg; + cfg.path = "./"; + cfg.name = std::format("static {}", Greeter->name); + cfg.outputName = "static-dep"; + cfg.type = ConfigurationType::Executable; + return cfg; +} diff --git a/tests/ProjectDependency/main.cpp b/tests/ProjectDependency/main.cpp index de01130..595d17c 100644 --- a/tests/ProjectDependency/main.cpp +++ b/tests/ProjectDependency/main.cpp @@ -90,14 +90,19 @@ int main(int argc, char** argv) { const fs::path app = scratch / "app" / "project.cpp"; 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. + // and ProjectDependency() hands it back by either name. The app ignores + // the arg it is loaded with, and the dependency must not get it: host + // tooling gets none of the consumer's args, although a LocalProject spec + // defaults to them. The consumer's flags would split the tool's variant + // from every other consumer's, and its --target= would have the tool's + // project.cpp choose for the wrong triple. try { - Configuration cfg = LoadProject(app, noArgs); + std::array flavored = { "--flavor=x" }; + Configuration cfg = LoadProject(app, flavored); 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"); @@ -109,6 +114,7 @@ int main(int argc, char** argv) { 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(dep.projectArgs.empty(), std::format("the dependency got none of the consumer's args (got {})", dep.projectArgs)); Check(fs::exists(ExpectedOutput(dep)), "the dependency archive exists"); } } catch (const std::exception& e) { @@ -116,20 +122,15 @@ int main(int argc, char** argv) { return 1; } - // Host tooling gets none of the consumer's args, although a LocalProject - // spec defaults to them: the consumer's flags would split the tool's - // variant from every other consumer's, and its --target= would have the - // tool's project.cpp choose for the wrong triple. - try { - std::array flavored = { "--flavor=x" }; - Configuration cfg = LoadProject(app, flavored); - const bool found = !cfg.dependencies.empty(); - Check(found, "a consumer loaded with args still finds its pragma dependency"); - if (found) Check(cfg.dependencies[0]->projectArgs.empty(), std::format("a pragma dependency is loaded without the consumer's args (got {})", cfg.dependencies[0]->projectArgs)); - } catch (const std::exception& e) { - std::println(std::cerr, "FAIL: LoadProject with args threw: {}", e.what()); - return 1; + // A static initialiser of the project file runs while LoadProject + // dlopens it, before CrafterBuildProject is called, and can already ask + // for a dependency. In a child, because a static initialiser that + // throws terminates the process. + { + const std::string loaded = LoadInChild(scratch / "static-dep" / "project.cpp"); + Check(loaded == "static greeter", std::format("ProjectDependency() works in a static initialiser (got '{}')", loaded)); } + Check(fs::exists(appSo), "project.so was produced"); if (!fs::exists(appSo)) return 1; const fs::file_time_type firstSo = fs::last_write_time(appSo);