diff --git a/AGENTS.md b/AGENTS.md index d5227e8..ffd0057 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ Prerequisites on Linux: `clang` (with `libclang`, which the AST lint rules load Three things to know about this loop: - `bin/crafter-build` is the bootstrap binary and the harness for every command below. The self-host build writes to `bin/crafter.build-exe-/crafter-build` and does not replace it. -- Changes under `implementations/` are picked up by the self-host build and by the tests, which link the local library. Changes under `interfaces/` are not: the harness compiles against the copies in `share/crafter-build/` and the PCM cache built from them, so a new or changed declaration is invisible until you run `./build.sh` again. The symptom is `use of undeclared identifier` for the symbol you just added. A changed default member initialiser has no symptom at all: `project.cpp` compiles against the old one and quietly gets the old default, as `project.cpp`'s sibling loads do with a harness that predates `GitProjectSpec::args = ProjectArgs()`. +- Changes under `implementations/` are picked up by the self-host build and by the tests, which link the local library. Changes under `interfaces/` are not: the harness compiles against the copies in `share/crafter-build/` and the PCM cache built from them, so a new or changed declaration is invisible until you run `./build.sh` again. The symptom is `use of undeclared identifier` for the symbol you just added. - A new exported symbol that a `project.cpp` reaches on its default path also needs `./build.sh`: the old binary does not export it, and `LoadProject` fails with an undefined symbol. `./build.sh` clones and builds glslang on first run and takes several minutes; later runs are faster. `./bin/crafter-build clean` deletes `bin/` and `build/` without loading `project.cpp`. That takes `bin/crafter-build` itself with it, and `build/flatpak`, the state of the local Flatpak build (see "CI and publishing"): about 2 GB, and the next `run flatpak` rebuilds LLVM. Set `CRAFTER_BUILD_HOME` only when running a binary from outside this tree; `CRAFTER_BUILD_MARCH` and `CRAFTER_BUILD_MTUNE` default to `native`. diff --git a/README.md b/README.md index 2620f3f..89fc5de 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,7 @@ Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project. cfg.dependencies = { math }; ``` -The sibling's `CrafterBuildProject` gets the args this `project.cpp` was loaded with (`ProjectArgs()`), so `--target=`, `--march=`, `--debug` and the project's own flags reach every sibling down the chain. That is all of them: `cfg.projectArgs` holds only what `ApplyStandardArgs` did not interpret, and passing it on would drop the target and the build type. Set `.args` to send something else: `{}` for nothing, or a list built from the project's args, such as one without a flag only this project reads. Build such a list once and give it to every sibling. A project that two paths load with different lists is built twice, and the program ends up with both builds. Releases up to 2.2.0 forward nothing when `.args` is left out. - -The default is read when the spec is made, so make it inside `CrafterBuildProject`, on the thread that calls it. A spec made at namespace scope, on a thread of your own or in a script forwards nothing. +The sibling's `CrafterBuildProject` gets the args this `project.cpp` was loaded with (`ProjectArgs()`), so `--target=`, `--march=`, `--debug` and the project's own flags reach every sibling down the chain. Set `.args` to send something else: `{}` for nothing, or a list built from the project's args, say one without a flag only this project reads (it would otherwise give the sibling a build directory of its own) or with an extra `--target=`. Releases up to 2.2.0 forward nothing when `.args` is left out. Clones share the external cache and identical specs share one `Configuration`. An unpinned source is `git pull`ed on every invocation, set `.commit` for reproducible, offline-after-first-fetch builds. diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index f93451a..550023e 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -1651,10 +1651,6 @@ Build options: -v, --verbose Verbose progress output. -q, --quiet Suppress progress output. --project= Path to the project file (default: ./project.cpp). - -- Ends this tool's options. Every later word is a - project arg, even one that looks like an option or - a verb; after test, lint or format, a glob; after - run, the script's name and then its args. Test options (after the `test` subcommand): --list Enumerate matching tests without running them. @@ -1766,9 +1762,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) { RunLintOptions lintOpts; RunScriptOptions scriptOpts; Progress::Verbosity verbosity = Progress::Verbosity::Default; - // Set by `--`: every later word is an operand of the verb, never an - // option or a verb of this tool. - bool operandsOnly = false; for (std::int32_t i = 1; i < argc; ++i) { std::string_view arg = argv[i]; @@ -1780,25 +1773,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) { // variant hash and so the directory the project builds into. if (scriptOpts.named) { scriptOpts.args.emplace_back(arg); - } else if (runScript && (operandsOnly || !arg.starts_with("-"))) { + } else if (runScript && !arg.starts_with("-")) { // The first word after `run`, even one that is a verb // elsewhere. Options before it keep their usual meaning. scriptOpts.name = arg; scriptOpts.named = true; - } else if (operandsOnly) { - if (runTests) { - testOpts.globs.emplace_back(arg); - } else if (runLint || runFormat) { - lintOpts.globs.emplace_back(arg); - } else { - projectArgs.push_back(arg); - } - } else if (arg == "--") { - // Ends this tool's options and is never forwarded itself. It - // used to land in projectArgs as a word of its own, giving the - // project a variant hash of its own, and every sibling project - // that inherits the args one as well. - operandsOnly = true; } else if (arg == "--version") { // Answered here, before the project file is looked for, so // it works in any directory and is never forwarded to diff --git a/implementations/Crafter.Build-External.cpp b/implementations/Crafter.Build-External.cpp index ff0c766..6e12988 100644 --- a/implementations/Crafter.Build-External.cpp +++ b/implementations/Crafter.Build-External.cpp @@ -360,12 +360,11 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) { // source revision (forwarded args don't change what gets cloned, just // how the dep's project.cpp interprets them at runtime), while the // in-memory Configuration is keyed by the full spec so each (args) - // permutation gets its own Configuration object, and host tooling one - // apart from the target's even for the same args. + // permutation gets its own Configuration object. std::string srcKey = std::format("git|{}|{}|{}|{}", spec.source.url, spec.source.branch, spec.source.commit, spec.projectFile.string()); std::string srcHash = std::format("{:016x}", std::hash{}(srcKey)); - std::string fullKey = std::format("{}{}", LoadingHostTooling() ? "host|" : "", srcKey); + std::string fullKey = srcKey; for (const std::string& a : spec.args) { fullKey += '|'; fullKey += a; @@ -450,7 +449,7 @@ Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) { } fs::path canonical = fs::canonical(projectPath); - std::string fullKey = std::format("{}local|{}", LoadingHostTooling() ? "host|" : "", canonical.string()); + std::string fullKey = std::format("local|{}", canonical.string()); for (const std::string& a : spec.args) { fullKey += '|'; fullKey += a; diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index b727cbb..1d21c9c 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -413,10 +413,9 @@ namespace { struct ProjectFileFrame { fs::path project; - // What LoadProject was given, for ProjectArgs(), and only once - // CrafterBuildProject is about to be called (see LoadProject). A view - // is enough: the caller's storage outlives LoadProject, and the frame - // is popped before LoadProject returns. + // 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 args; std::vector dependencies; }; @@ -431,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 args) { for (const ProjectFileFrame& frame : LoadingFrames) { if (frame.project != absProject) continue; std::string chain; @@ -439,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,21 +453,6 @@ namespace { std::size_t index_ = 0; }; - // How many `#pragma crafter dependency` loads this thread is inside. The - // tool's project file runs within its load, and so does every sibling it - // loads in turn, which is what LoadingHostTooling() reports to GitProject - // and LocalProject. Per thread like LoadingFrames: a sibling loaded on a - // thread of the tool's own goes uncounted, as it gets no ProjectArgs(). - thread_local std::int32_t HostToolingLoads = 0; - - class HostToolingScope { - public: - HostToolingScope() { ++HostToolingLoads; } - ~HostToolingScope() { --HostToolingLoads; } - HostToolingScope(const HostToolingScope&) = delete; - HostToolingScope& operator=(const HostToolingScope&) = delete; - }; - // A pre-dependency is host tooling: project.so is loaded into the running // crafter-build, so the dependency is built for the host and against the // same PCM cache LoadProject populates — the hostConfig LoadProject itself @@ -516,36 +503,30 @@ namespace { std::set libs; for (const PragmaDependency& pragma : pragmas) { - // Neither spec sets args, and the tool still gets none of the - // consumer's: this pre-pass runs before LoadProject hands the - // frame its args, so ProjectArgs() is empty. Given the consumer's - // --target=, the tool's project.cpp would pick sources and flags - // for the wrong triple, and its flags would split the tool's - // variant from the one every other consumer shares. The scope - // keeps the tool and everything it loads in a project cache of - // their own: ForceHostTooling rewrites them in place, and a - // consumer loading the same sibling for itself got that host - // build back whenever the two cache keys matched. + // 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; - { - HostToolingScope hostTooling; - if (IsGitSource(pragma.source)) { - GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit } }; - if (!pragma.project.empty()) spec.projectFile = pragma.project; - cfg = GitProject(spec); - } else { - if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source"); - // Relative to the project file, not the cwd: LoadProject runs - // from wherever the consumer was invoked. - fs::path local = (absProject.parent_path() / pragma.source).lexically_normal(); - if (fs::is_directory(local)) { - local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project); - } else if (!pragma.project.empty()) { - 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 }); + if (IsGitSource(pragma.source)) { + GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit }, .args = {} }; + if (!pragma.project.empty()) spec.projectFile = pragma.project; + cfg = GitProject(spec); + } else { + if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source"); + // Relative to the project file, not the cwd: LoadProject runs + // from wherever the consumer was invoked. + fs::path local = (absProject.parent_path() / pragma.source).lexically_normal(); + if (fs::is_directory(local)) { + local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project); + } else if (!pragma.project.empty()) { + 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, .args = {} }); } if (std::ranges::contains(out.configs, cfg)) continue; @@ -619,10 +600,6 @@ std::vector Crafter::ProjectArgs() { return std::vector(args.begin(), args.end()); } -bool Crafter::LoadingHostTooling() { - return HostToolingLoads > 0; -} - 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 @@ -925,7 +902,7 @@ namespace { Configuration Crafter::LoadProject(const fs::path& projectFile, std::span 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); @@ -997,9 +974,6 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span projectArgs; std::vector tests; // Lint rules for `crafter-build lint`. Populate via AddLintRule. diff --git a/interfaces/Crafter.Build-External.cppm b/interfaces/Crafter.Build-External.cppm index 23ebe6e..f33cb3d 100644 --- a/interfaces/Crafter.Build-External.cppm +++ b/interfaces/Crafter.Build-External.cppm @@ -71,21 +71,16 @@ export namespace Crafter { // clone can still differ in includeDirs, libDirs and libs. ExternalBuildResult ExternalOutputs(const ExternalDependency& dep, std::string_view target); - // 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. + // 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 ProjectArgs(); // Specification for a sibling crafter-build project to fetch and depend on. @@ -93,26 +88,21 @@ export namespace Crafter { // 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 // what the dep's CrafterBuildProject receives. They default to this - // project's own, read when the spec is made, so --target=, --march=, - // --debug and the project's own flags reach every sibling down a chain - // without being passed. Set them to forward something else: {} for - // nothing, or a list built from this project's args, such as one without - // a flag only this project reads. Give such a list to every sibling: a - // project that two paths load with two different lists is built twice, - // and the program ends up with both builds. + // 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 args = ProjectArgs(); }; - // Clones the spec's git URL into the external cache, recursively - // LoadProject's the remote's project.cpp, and returns a stable - // Configuration* you can drop into cfg.dependencies. The clone is keyed by - // url+branch+commit+projectFile alone, so every arg list builds in the - // same working tree, each into variant directories of its own. The - // Configuration is owned by an internal cache for the lifetime of the - // build, keyed by the clone and the args: identical specs share one. + // Clones the spec's git URL into the per-project external cache (keyed by + // url+branch+commit+args+projectFile), recursively LoadProject's the + // remote's project.cpp, and returns a stable Configuration* you can drop + // into cfg.dependencies. The Configuration is owned by an internal cache + // for the lifetime of the build; identical specs share one Configuration. Configuration* GitProject(const GitProjectSpec& spec); // Specification for a local sibling crafter-build project (e.g. an diff --git a/interfaces/Crafter.Build-Platform.cppm b/interfaces/Crafter.Build-Platform.cppm index eb9110c..2601275 100644 --- a/interfaces/Crafter.Build-Platform.cppm +++ b/interfaces/Crafter.Build-Platform.cppm @@ -75,12 +75,6 @@ namespace Crafter { // 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 bool IsTopLevelProject(); - // True while this thread loads a `#pragma crafter dependency`: the tool's - // project file and every sibling it loads in turn. GitProject and - // LocalProject key the project cache on it, so host tooling, which - // ForceHostTooling rewrites in place, never shares a Configuration with a - // load for the target. - bool LoadingHostTooling(); // 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/project.cpp b/project.cpp index bfd3e9b..4ea9679 100644 --- a/project.cpp +++ b/project.cpp @@ -282,8 +282,6 @@ extern "C" Configuration CrafterBuildProject(std::span a .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); cfg.AddTest("ProjectArgs").Dependencies({ CrafterBuildLib.get() }) .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); - cfg.AddTest("HostToolingSibling").Dependencies({ CrafterBuildLib.get() }) - .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() }) .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() }); diff --git a/tests/HostToolingSibling/fixture/app/project.cpp b/tests/HostToolingSibling/fixture/app/project.cpp deleted file mode 100644 index d9f107a..0000000 --- a/tests/HostToolingSibling/fixture/app/project.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -import std; -import Crafter.Build; -#pragma crafter dependency "../tool" -using namespace Crafter; - -// Loads for itself the sibling its tool loads, by the same path and, when -// the app gets no args, with the same args. -extern "C" Configuration CrafterBuildProject(std::span args) { - Configuration* shared = LocalProject({ .projectFile = "../shared/project.cpp" }); - - Configuration cfg; - cfg.path = "./"; - cfg.name = "app"; - cfg.outputName = "app"; - cfg.type = ConfigurationType::Executable; - ApplyStandardArgs(cfg, args); - cfg.dependencies = { shared, ProjectDependency("tool") }; - return cfg; -} diff --git a/tests/HostToolingSibling/fixture/shared/interfaces/Shared.cppm b/tests/HostToolingSibling/fixture/shared/interfaces/Shared.cppm deleted file mode 100644 index af40390..0000000 --- a/tests/HostToolingSibling/fixture/shared/interfaces/Shared.cppm +++ /dev/null @@ -1,5 +0,0 @@ -export module Shared; -import std; -export std::int32_t SharedValue() { - return 7; -} diff --git a/tests/HostToolingSibling/fixture/shared/project.cpp b/tests/HostToolingSibling/fixture/shared/project.cpp deleted file mode 100644 index 3cd5bdc..0000000 --- a/tests/HostToolingSibling/fixture/shared/project.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -import std; -import Crafter.Build; -namespace fs = std::filesystem; -using namespace Crafter; - -// The sibling both the tool and the app load. ApplyStandardArgs on purpose: -// it reads CRAFTER_BUILD_MARCH from the environment, which is how the test -// tells the app's copy from the one built as host tooling. -extern "C" Configuration CrafterBuildProject(std::span args) { - Configuration cfg; - cfg.path = "./"; - cfg.name = "shared"; - cfg.outputName = "shared"; - cfg.type = ConfigurationType::LibraryStatic; - ApplyStandardArgs(cfg, args); - std::array ifaces = { "interfaces/Shared" }; - std::array impls = {}; - cfg.GetInterfacesAndImplementations(ifaces, impls); - return cfg; -} diff --git a/tests/HostToolingSibling/fixture/tool/interfaces/Tool.cppm b/tests/HostToolingSibling/fixture/tool/interfaces/Tool.cppm deleted file mode 100644 index c6d5490..0000000 --- a/tests/HostToolingSibling/fixture/tool/interfaces/Tool.cppm +++ /dev/null @@ -1,6 +0,0 @@ -export module Tool; -import std; -import Shared; -export std::int32_t ToolValue() { - return SharedValue(); -} diff --git a/tests/HostToolingSibling/fixture/tool/project.cpp b/tests/HostToolingSibling/fixture/tool/project.cpp deleted file mode 100644 index 51ad781..0000000 --- a/tests/HostToolingSibling/fixture/tool/project.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -import std; -import Crafter.Build; -namespace fs = std::filesystem; -using namespace Crafter; - -// The app's `#pragma crafter dependency`, with a sibling of its own. -extern "C" Configuration CrafterBuildProject(std::span args) { - Configuration* shared = LocalProject({ .projectFile = "../shared/project.cpp" }); - - Configuration cfg; - cfg.path = "./"; - cfg.name = "tool"; - cfg.outputName = "tool"; - cfg.type = ConfigurationType::LibraryStatic; - ApplyStandardArgs(cfg, args); - cfg.dependencies = { shared }; - std::array ifaces = { "interfaces/Tool" }; - std::array impls = {}; - cfg.GetInterfacesAndImplementations(ifaces, impls); - return cfg; -} diff --git a/tests/HostToolingSibling/main.cpp b/tests/HostToolingSibling/main.cpp deleted file mode 100644 index f6ad160..0000000 --- a/tests/HostToolingSibling/main.cpp +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -#include -import std; -import Crafter.Build; -namespace fs = std::filesystem; -using namespace Crafter; - -// A sibling that a project and its `#pragma crafter dependency` both load -// is two configurations: the tool's copy is host tooling, built for the -// host and position-independent, and the project's is built the way the -// project builds. ForceHostTooling rewrites the tool's graph in place, and -// the project cache used to hand the project that same rewritten -// Configuration whenever the two loads had the same key, as they do when -// the project gets no args. The project then built against a sibling -// compiled with -march=native while itself building for x86-64-v2, and -// clang refused the sibling's BMI. -namespace { - std::int32_t Failures = 0; - - void Check(bool cond, std::string_view msg) { - if (!cond) { - std::println(std::cerr, "FAIL: {}", msg); - ++Failures; - } - } -} - -int main() { - const fs::path installedHome = GetCrafterBuildHome(); - if (!fs::exists(installedHome / "Crafter.Build.cppm")) { - std::println(std::cerr, "SKIP: no module sources at {}", installedHome.string()); - return 77; - } - - // Staged because LoadProject drops a build/ tree next to every project - // file it compiles, and the pre-pass builds the tool and its sibling. - const fs::path fixture = fs::current_path() / "tests" / "HostToolingSibling" / "fixture"; - const fs::path scratch = fs::temp_directory_path() / "crafter-build-host-tooling-sibling"; - std::error_code ec; - fs::remove_all(scratch, ec); - fs::create_directories(scratch); - fs::copy(fixture, scratch, fs::copy_options::recursive); - - // The march the project builds for. Host tooling overrides it to - // native, so a sibling that comes back with native was rewritten. - setenv("CRAFTER_BUILD_MARCH", "x86-64-v2", 1); - - // From the app's own directory, as crafter-build runs it: the fixture's - // LocalProject paths are relative to the working directory. - const fs::path cwd = fs::current_path(); - fs::current_path(scratch / "app"); - std::array noArgs = {}; - try { - Configuration cfg = LoadProject("project.cpp", noArgs); - Check(cfg.dependencies.size() == 2, std::format("the app declared its sibling and its tool (got {})", cfg.dependencies.size())); - if (cfg.dependencies.size() == 2) { - const Configuration* shared = cfg.dependencies[0]; - const Configuration* tool = cfg.dependencies[1]; - const Configuration* toolShared = tool->dependencies.size() == 1 ? tool->dependencies[0] : nullptr; - Check(toolShared != nullptr, "the tool declared its sibling"); - Check(shared != toolShared, "the app's sibling is not the tool's copy of it"); - Check(shared->march == "x86-64-v2", std::format("the app's sibling keeps the app's march (got {})", shared->march)); - Check(!std::ranges::contains(shared->compileFlags, "-fPIC"), "the app's sibling is not built as host tooling"); - if (toolShared) { - Check(toolShared->march == "native", std::format("the tool's copy is built for the host (got {})", toolShared->march)); - Check(std::ranges::contains(toolShared->compileFlags, "-fPIC"), "the tool's copy is position-independent"); - } - } - } catch (const std::exception& e) { - fs::current_path(cwd); - std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what()); - return 1; - } - fs::current_path(cwd); - - return Failures > 0 ? 1 : 0; -} diff --git a/tests/ProjectArgs/fixture/app/project.cpp b/tests/ProjectArgs/fixture/app/project.cpp index 7b51816..353c592 100644 --- a/tests/ProjectArgs/fixture/app/project.cpp +++ b/tests/ProjectArgs/fixture/app/project.cpp @@ -6,14 +6,12 @@ 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. 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. +// with nothing forwarded, and with a list of its own. 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 = "./"; @@ -21,7 +19,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.outputName = "app"; cfg.type = ConfigurationType::Executable; ApplyStandardArgs(cfg, args); - cfg.dependencies = { inherited, none, replaced, fromStatic }; + cfg.dependencies = { inherited, none, replaced }; // 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 deleted file mode 100644 index 84f75bd..0000000 --- a/tests/ProjectArgs/fixture/static/project.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// 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 6bf4005..4b47938 100644 --- a/tests/ProjectArgs/main.cpp +++ b/tests/ProjectArgs/main.cpp @@ -15,10 +15,7 @@ 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 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. +// a clone would need a repository for. namespace { std::int32_t Failures = 0; @@ -29,21 +26,10 @@ namespace { } } - // The single sibling lib, static and leaf declare, or nullptr. + // The single sibling lib 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() { @@ -74,25 +60,24 @@ 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() == 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"); + 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"); // One sibling further down, the same default passes on what the // middle project was given, not what the top one was. - 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"); + 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"); } } 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 deleted file mode 100644 index 2090db5..0000000 --- a/tests/ProjectDependency/fixture/static-dep/project.cpp +++ /dev/null @@ -1,22 +0,0 @@ -// 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 595d17c..de01130 100644 --- a/tests/ProjectDependency/main.cpp +++ b/tests/ProjectDependency/main.cpp @@ -90,19 +90,14 @@ 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. 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. + // and ProjectDependency() hands it back by either name. try { - std::array flavored = { "--flavor=x" }; - Configuration cfg = LoadProject(app, flavored); + 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"); @@ -114,7 +109,6 @@ 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) { @@ -122,15 +116,20 @@ int main(int argc, char** argv) { 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)); + // 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; } - 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); diff --git a/tests/Script/main.cpp b/tests/Script/main.cpp index 472682d..8e200a8 100644 --- a/tests/Script/main.cpp +++ b/tests/Script/main.cpp @@ -218,29 +218,6 @@ int main() { Check(fs::exists(binSentinel) && fs::exists(buildSentinel), "`clean` as a script argument deleted nothing"); } - // `--` ends this tool's options and goes nowhere itself. In the - // project's args it gave the project a variant of its own, and every - // sibling that inherits the args one too. The word after it is an - // operand even when it looks like an option, here the script's name; - // after the name, a `--` is the script's like any other word. - { - fs::remove(scratch / "record.txt"); - const std::int32_t code = RunWith({"crafter-build", project, "--flavour=x", "run", "--", "record", "--", "a"}); - Check(code == 42, std::format("the script after `--` ran (got {})", code)); - const std::vector expected = { - "arg:--", "arg:a", - "projectArg:--flavour=x", - "debug:false", - "project:script-fixture", - }; - const std::vector record = ReadLines(scratch / "record.txt"); - Check(record == expected, "`--` reached neither the project args nor the script"); - if (record != expected) { - for (const std::string& line : record) std::println(std::cerr, " recorded: {}", line); - } - Check(RunWith({"crafter-build", project, "run", "--", "-x"}) == 1, "after `--` a word that looks like an option names the script"); - } - // The first word after `run` is the name even when it is a verb. { fs::remove(scratch / "record.txt");