diff --git a/AGENTS.md b/AGENTS.md index f55da0d..a97508b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,17 +95,14 @@ Cross-target runs use `test --target=` with `--runner=local` or `--runne ## Code style -`project.cpp` pulls in [Crafter.LintRules](https://forgejo.catcrafts.net/Catcrafts/Crafter.LintRules) through `#pragma crafter dependency` and applies its lite set, `LintRules::AddLiteRules`: the 8 rules that ask for different code, without the ones that only decide layout, whitespace and naming. `lint` reports all 8; `format` applies the two transforms among them, `format-concat` and `fixed-width-types`, and leaves everything else as written. Enforced: +Style is enforced by the 17 rules in [Crafter.LintRules](https://forgejo.catcrafts.net/Catcrafts/Crafter.LintRules), which `project.cpp` pulls in through `#pragma crafter dependency` and applies with `crafter-build format` and `lint`. Most rules are transforms, so run `format` and let it restyle rather than hand-formatting. In short: - The first three lines of every source: `// SPDX-License-Identifier: LGPL-3.0-only`, `// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®`, blank. Examples use MIT. -- `enum class` only. `std::println` instead of iostream. `std::format` instead of string concatenation. `std::int32_t` and friends instead of `int` or `long` (exempt: `main`, `argc`/`argv`, `extern "C"` and foreign API signatures). No `char*` outside C interop. -- `const` on scalar locals that are never written; `constexpr` on constants with constant initialisers. - -Not checked, so match the surrounding code: - - Four-space indentation, no tabs, no trailing whitespace, final newline. - `PascalCase` for types, functions, statics and constants; `camelCase` for variables; a trailing `_` is allowed. -- One declaration per statement. K&R braces. `if (x) return;` on one line when it fits, and no hand-wrapping of a statement that fits on one line. `if (` and `for (` with a space; no space before a call's `(`. +- `enum class` only. `std::println` instead of iostream. `std::format` instead of string concatenation. `std::int32_t` and friends instead of `int` or `long` (exempt: `main`, `argc`/`argv`, `extern "C"` and foreign API signatures). No `char*` outside C interop. +- One declaration per statement. K&R braces. `if (x) return;` on one line when it fits. The formatter joins wrapped statements that fit on one line, so do not hand-wrap short ones. `if (` and `for (` with a space; no space before a call's `(`. +- `const` on scalar locals that are never written; `constexpr` on constants with constant initialisers. Suppress one finding with `// lint-disable-next-line ` or a whole file with `// lint-disable-file `. Both are rare in this tree and need a reason. diff --git a/README.md b/README.md index 2029038..b7e0d46 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,12 @@ Clones live at `~/.cache/crafter.build/external/-/`, keyed by `(url, ```cpp Configuration* math = GitProject({ .source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" }, // .branch or .commit to pin + .args = depArgs, // forwarded to its CrafterBuildProject }); -Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project.cpp" }); +Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project.cpp", .args = depArgs }); 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. 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. A project loaded this way runs its `CrafterBuildProject` as a dependency, and `IsTopLevelProject()` is `false` while it does (`true` in the project crafter-build was pointed at, and outside any project file). Wire test-only dependencies behind it: a library whose test links crafter-build itself would otherwise load crafter-build's `project.cpp` from inside crafter-build and recurse. @@ -95,8 +94,6 @@ import Crafter.LintRules; #pragma crafter dependency "" [branch "" | commit ""] [project ""] ``` -Such a dependency is built for the host, whatever the project targets, and its `project.cpp` gets none of the project's args. - ## Tests Tests live under `tests//`. The simplest case is a single C++ file: diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index 1d21c9c..e4ead32 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -413,10 +413,6 @@ 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 args; std::vector dependencies; }; @@ -430,10 +426,7 @@ namespace { // already on the stack is a cycle, which used to recurse forever. class ProjectFileGuard { public: - // 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) { + explicit ProjectFileGuard(const fs::path& absProject) { for (const ProjectFileFrame& frame : LoadingFrames) { if (frame.project != absProject) continue; std::string chain; @@ -441,7 +434,7 @@ namespace { throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string())); } index_ = LoadingFrames.size(); - LoadingFrames.push_back({ absProject, args, {} }); + LoadingFrames.push_back({ absProject, {} }); } ~ProjectFileGuard() { LoadingFrames.pop_back(); } ProjectFileGuard(const ProjectFileGuard&) = delete; @@ -458,9 +451,8 @@ 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 is loaded with no arguments and so passes - // none on, while its own deps still read CRAFTER_BUILD_MARCH from the - // environment. + // the dependency's project.cpp forwards no arguments but still reads + // CRAFTER_BUILD_MARCH from the environment for its own deps. void ForceHostTooling(Configuration& cfg, const Configuration& host, std::unordered_set& seen) { if (!seen.insert(&cfg).second) return; cfg.target = host.target; @@ -503,16 +495,10 @@ namespace { std::set 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{ .source = { pragma.source, pragma.branch, pragma.commit }, .args = {} }; + GitProjectSpec spec; + spec.source = { pragma.source, pragma.branch, pragma.commit }; if (!pragma.project.empty()) spec.projectFile = pragma.project; cfg = GitProject(spec); } else { @@ -526,7 +512,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, .args = {} }); + cfg = LocalProject({ .projectFile = local }); } if (std::ranges::contains(out.configs, cfg)) continue; @@ -594,12 +580,6 @@ bool Crafter::IsTopLevelProject() { return LoadingFrames.size() <= 1; } -std::vector Crafter::ProjectArgs() { - if (LoadingFrames.empty()) return {}; - const std::span args = LoadingFrames.back().args; - return std::vector(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 @@ -902,7 +882,7 @@ namespace { Configuration Crafter::LoadProject(const fs::path& projectFile, std::span args) { RequireToolchain(); fs::path absProject = fs::canonical(projectFile); - ProjectFileGuard guard(absProject, args); + ProjectFileGuard guard(absProject); fs::path buildDir = absProject.parent_path() / "build"; if (!fs::exists(buildDir)) { fs::create_directories(buildDir); diff --git a/interfaces/Crafter.Build-External.cppm b/interfaces/Crafter.Build-External.cppm index 2ab2760..fe8e229 100644 --- a/interfaces/Crafter.Build-External.cppm +++ b/interfaces/Crafter.Build-External.cppm @@ -54,31 +54,16 @@ export namespace Crafter { std::vector ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target); ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic& cancelled); - // 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. // 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 - // 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. + // forwarded verbatim to the dep's CrafterBuildProject — typically you + // pass through the parent's args so target/march/debug propagate. struct GitProjectSpec { GitSource source; fs::path projectFile = "project.cpp"; - std::vector args = ProjectArgs(); + std::vector args; }; // Clones the spec's git URL into the per-project external cache (keyed by @@ -92,8 +77,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 args = ProjectArgs(); // as GitProjectSpec::args + fs::path projectFile; // relative to cwd or absolute + std::vector args; // forwarded to dep's CrafterBuildProject }; // Same as GitProject but for a local on-disk project — no fetch, no diff --git a/project.cpp b/project.cpp index ce191d6..8ba59fe 100644 --- a/project.cpp +++ b/project.cpp @@ -137,8 +137,15 @@ Remove it (do this before `crafter-build clean`, which deletes the repo the remo } extern "C" Configuration CrafterBuildProject(std::span args) { - Configuration* math = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" } }); - Configuration* asset = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git" } }); + std::vector depArgs(args.begin(), args.end()); + Configuration* math = GitProject({ + .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" }, + .args = depArgs, + }); + Configuration* asset = GitProject({ + .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git" }, + .args = depArgs, + }); static auto CrafterBuildLib = std::make_unique(); CrafterBuildLib->path = "./"; @@ -279,22 +286,16 @@ extern "C" Configuration CrafterBuildProject(std::span a .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() }) .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); - cfg.AddTest("ProjectArgs").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() }); cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() }); } - // Dogfood: the lite house rules, imported from their own repo through - // the pragma above. Lite is the house rules without the ones that only - // decide layout, whitespace and naming, which this tree keeps by hand: - // the full set's naming rule, for one, wants camelCase where this tree - // spells its statics and constants PascalCase. Report rules gate - // `crafter-build lint`; the two transforms also auto-fix under - // `crafter-build format`. - LintRules::AddLiteRules(cfg); + // Dogfood: the house-style rules, imported from their own repo through + // the pragma above. Report rules gate `crafter-build lint`; transform + // rules also auto-fix under `crafter-build format`. + LintRules::AddRules(cfg); // See the top of this file. cfg.scripts.push_back({"flatpak", BuildFlatpak}); diff --git a/tests/ProjectArgs/fixture/app/project.cpp b/tests/ProjectArgs/fixture/app/project.cpp deleted file mode 100644 index 353c592..0000000 --- a/tests/ProjectArgs/fixture/app/project.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -import std; -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. -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 cfg; - cfg.path = "./"; - cfg.name = "app"; - cfg.outputName = "app"; - cfg.type = ConfigurationType::Executable; - ApplyStandardArgs(cfg, args); - 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" } }; - const bool gitDefault = git.args == std::vector(args.begin(), args.end()); - cfg.compileFlags.push_back(gitDefault ? "-DGIT_DEFAULT=1" : "-DGIT_DEFAULT=0"); - return cfg; -} diff --git a/tests/ProjectArgs/fixture/leaf/project.cpp b/tests/ProjectArgs/fixture/leaf/project.cpp deleted file mode 100644 index 15b16d4..0000000 --- a/tests/ProjectArgs/fixture/leaf/project.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -import std; -import Crafter.Build; -using namespace Crafter; - -// The end of the chain: records what it was loaded with and nothing else. -extern "C" Configuration CrafterBuildProject(std::span args) { - Configuration cfg; - cfg.path = "./"; - cfg.name = "leaf"; - cfg.outputName = "leaf"; - cfg.type = ConfigurationType::LibraryStatic; - ApplyStandardArgs(cfg, args); - return cfg; -} diff --git a/tests/ProjectArgs/fixture/lib/project.cpp b/tests/ProjectArgs/fixture/lib/project.cpp deleted file mode 100644 index f1702c9..0000000 --- a/tests/ProjectArgs/fixture/lib/project.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -import std; -import Crafter.Build; -using namespace Crafter; - -// The middle of the chain. Its own sibling takes the default too, so -// whatever this file was loaded with has to reach the leaf unchanged. -extern "C" Configuration CrafterBuildProject(std::span args) { - Configuration* leaf = LocalProject({ .projectFile = "../leaf/project.cpp" }); - - Configuration cfg; - cfg.path = "./"; - cfg.name = "lib"; - cfg.outputName = "lib"; - cfg.type = ConfigurationType::LibraryStatic; - ApplyStandardArgs(cfg, args); - cfg.dependencies = { leaf }; - return cfg; -} diff --git a/tests/ProjectArgs/main.cpp b/tests/ProjectArgs/main.cpp deleted file mode 100644 index 4b47938..0000000 --- a/tests/ProjectArgs/main.cpp +++ /dev/null @@ -1,91 +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; - -// A GitProject or LocalProject without `.args` forwards the args of the -// project file that declares it: both specs default their args to -// ProjectArgs(). Before that default, every project.cpp with a sibling -// dependency copied its span into a std::vector for each spec, -// and a spec that missed the copy built its sibling with none of the -// project's flags: for the host while the project cross-compiled, and in -// 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. -namespace { - std::int32_t Failures = 0; - - void Check(bool cond, std::string_view msg) { - if (!cond) { - std::println(std::cerr, "FAIL: {}", msg); - ++Failures; - } - } - - // The single sibling lib and leaf declare, or nullptr. - const Configuration* OnlyDependency(const Configuration& cfg) { - return cfg.dependencies.size() == 1 ? cfg.dependencies[0] : nullptr; - } -} - -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. - const fs::path fixture = fs::current_path() / "tests" / "ProjectArgs" / "fixture"; - const fs::path scratch = fs::temp_directory_path() / "crafter-build-project-args"; - std::error_code ec; - fs::remove_all(scratch, ec); - fs::create_directories(scratch); - fs::copy(fixture, scratch, fs::copy_options::recursive); - - Check(ProjectArgs().empty(), "no project args outside a project file"); - - // Loaded from the project'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 args = { "--debug", "--flavor=x" }; - const std::vector flavor = { "--flavor=x" }; - const std::vector only = { "--only" }; - 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"); - - // 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"); - } - } catch (const std::exception& e) { - fs::current_path(cwd); - std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what()); - return 1; - } - fs::current_path(cwd); - - Check(ProjectArgs().empty(), "no project args once the project file has returned"); - return Failures > 0 ? 1 : 0; -} diff --git a/tests/ProjectDependency/main.cpp b/tests/ProjectDependency/main.cpp index de01130..fcf1c57 100644 --- a/tests/ProjectDependency/main.cpp +++ b/tests/ProjectDependency/main.cpp @@ -115,21 +115,6 @@ int main(int argc, char** argv) { std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what()); 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; - } 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);