Compare commits
30 changed files with 62 additions and 828 deletions
13
AGENTS.md
13
AGENTS.md
|
|
@ -46,7 +46,7 @@ Prerequisites on Linux: `clang` (with `libclang`, which the AST lint rules load
|
||||||
Three things to know about this loop:
|
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-<variant>/crafter-build` and does not replace it.
|
- `bin/crafter-build` is the bootstrap binary and the harness for every command below. The self-host build writes to `bin/crafter.build-exe-<variant>/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.
|
- 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`.
|
`./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`.
|
||||||
|
|
@ -96,17 +96,14 @@ Cross-target runs use `test --target=<triple>` with `--runner=local` or `--runne
|
||||||
|
|
||||||
## Code style
|
## 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.
|
- 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.
|
- Four-space indentation, no tabs, no trailing whitespace, final newline.
|
||||||
- `PascalCase` for types, functions, statics and constants; `camelCase` for variables; a trailing `_` is allowed.
|
- `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 <rule>` or a whole file with `// lint-disable-file <rule>`. Both are rare in this tree and need a reason.
|
Suppress one finding with `// lint-disable-next-line <rule>` or a whole file with `// lint-disable-file <rule>`. Both are rare in this tree and need a reason.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,15 +69,12 @@ Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url,
|
||||||
```cpp
|
```cpp
|
||||||
Configuration* math = GitProject({
|
Configuration* math = GitProject({
|
||||||
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" }, // .branch or .commit to pin
|
.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 };
|
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 would end up in the program twice, so the build stops with an error that names both paths. 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.
|
|
||||||
|
|
||||||
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.
|
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.
|
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.
|
||||||
|
|
@ -97,8 +94,6 @@ import Crafter.LintRules;
|
||||||
#pragma crafter dependency "<source>" [branch "<name>" | commit "<sha>"] [project "<file>"]
|
#pragma crafter dependency "<source>" [branch "<name>" | commit "<sha>"] [project "<file>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Such a dependency is built for the host, whatever the project targets, and its `project.cpp` gets none of the project's args.
|
|
||||||
|
|
||||||
### Dependency notes for agents
|
### Dependency notes for agents
|
||||||
|
|
||||||
An agent working on a project reads its `AGENTS.md`, which says nothing about the libraries the project builds against unless someone copied that in by hand. A library ships it instead: an `AGENTS.consumer.md` beside its `project.cpp`, written for whoever uses the library rather than whoever works on it. When a consumer has an `AGENTS.md` beside its own `project.cpp`, `crafter-build` and `crafter-build test` keep its dependencies' notes in a section of it:
|
An agent working on a project reads its `AGENTS.md`, which says nothing about the libraries the project builds against unless someone copied that in by hand. A library ships it instead: an `AGENTS.consumer.md` beside its `project.cpp`, written for whoever uses the library rather than whoever works on it. When a consumer has an `AGENTS.md` beside its own `project.cpp`, `crafter-build` and `crafter-build test` keep its dependencies' notes in a section of it:
|
||||||
|
|
|
||||||
|
|
@ -444,42 +444,6 @@ namespace {
|
||||||
}
|
}
|
||||||
return outcome;
|
return outcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two configurations among `config` and its transitive dependencies that
|
|
||||||
// export a module of the same name: the same project loaded along two
|
|
||||||
// paths with different args, say, or from a checkout on one path and a
|
|
||||||
// clone on the other. A program can hold one module of each name. Built
|
|
||||||
// anyway, both PCM dirs went onto the consumer's -fprebuilt-module-path,
|
|
||||||
// clang resolved the module by name to the first for every import, the
|
|
||||||
// ones inside the other build's consumers included, and nothing checked
|
|
||||||
// that it was the build they had been compiled against. The program then
|
|
||||||
// linked code compiled against two layouts of the same types, and ran
|
|
||||||
// (issue #41). Empty when the dependencies agree, otherwise the error,
|
|
||||||
// naming the path to each of the two builds.
|
|
||||||
std::string DuplicateModuleError(const Configuration& config) {
|
|
||||||
struct Owner {
|
|
||||||
const Configuration* cfg;
|
|
||||||
std::string chain;
|
|
||||||
};
|
|
||||||
const auto describe = [](const Owner& owner) {
|
|
||||||
return std::format("{} ({}, {}, project args {})", owner.chain, owner.cfg->path.string(), owner.cfg->VariantId(), owner.cfg->projectArgs);
|
|
||||||
};
|
|
||||||
std::unordered_map<std::string, Owner> owners;
|
|
||||||
std::unordered_set<const Configuration*> visited;
|
|
||||||
std::string error;
|
|
||||||
std::function<void(const Configuration&, const std::string&)> walk = [&](const Configuration& c, const std::string& chain) {
|
|
||||||
if (!error.empty() || !visited.insert(&c).second) return;
|
|
||||||
for (const std::unique_ptr<Module>& iface : c.interfaces) {
|
|
||||||
const auto [it, inserted] = owners.emplace(iface->name, Owner{ &c, chain });
|
|
||||||
if (inserted || it->second.cfg == &c) continue;
|
|
||||||
error = std::format("module {} is built twice for {}:\n {}\n {}\nA program can hold one build of each module: load {} with the same args, and from the same place, along every path.", iface->name, config.name, describe(it->second), describe(Owner{ &c, chain }), c.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const Configuration* dep : c.dependencies) walk(*dep, std::format("{} -> {}", chain, dep->name));
|
|
||||||
};
|
|
||||||
walk(config, config.name);
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
|
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
|
||||||
|
|
@ -488,11 +452,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
|
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
|
||||||
return {tc.error, false, {}};
|
return {tc.error, false, {}};
|
||||||
}
|
}
|
||||||
// Before any directory is made or dependency built, so a refused graph
|
|
||||||
// leaves nothing behind.
|
|
||||||
if (std::string duplicate = DuplicateModuleError(config); !duplicate.empty()) {
|
|
||||||
return {std::move(duplicate), false, {}};
|
|
||||||
}
|
|
||||||
// Reset per-build cached state on every Module/ModulePartition so that
|
// Reset per-build cached state on every Module/ModulePartition so that
|
||||||
// successive Build() calls on the same Configuration re-evaluate mtimes
|
// successive Build() calls on the same Configuration re-evaluate mtimes
|
||||||
// (incremental-rebuild test scenarios).
|
// (incremental-rebuild test scenarios).
|
||||||
|
|
@ -1693,10 +1652,6 @@ Build options:
|
||||||
-v, --verbose Verbose progress output.
|
-v, --verbose Verbose progress output.
|
||||||
-q, --quiet Suppress progress output.
|
-q, --quiet Suppress progress output.
|
||||||
--project=<path> Path to the project file (default: ./project.cpp).
|
--project=<path> 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):
|
Test options (after the `test` subcommand):
|
||||||
--list Enumerate matching tests without running them.
|
--list Enumerate matching tests without running them.
|
||||||
|
|
@ -1815,9 +1770,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
RunLintOptions lintOpts;
|
RunLintOptions lintOpts;
|
||||||
RunScriptOptions scriptOpts;
|
RunScriptOptions scriptOpts;
|
||||||
Progress::Verbosity verbosity = Progress::Verbosity::Default;
|
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) {
|
for (std::int32_t i = 1; i < argc; ++i) {
|
||||||
std::string_view arg = argv[i];
|
std::string_view arg = argv[i];
|
||||||
|
|
@ -1829,25 +1781,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
// variant hash and so the directory the project builds into.
|
// variant hash and so the directory the project builds into.
|
||||||
if (scriptOpts.named) {
|
if (scriptOpts.named) {
|
||||||
scriptOpts.args.emplace_back(arg);
|
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
|
// The first word after `run`, even one that is a verb
|
||||||
// elsewhere. Options before it keep their usual meaning.
|
// elsewhere. Options before it keep their usual meaning.
|
||||||
scriptOpts.name = arg;
|
scriptOpts.name = arg;
|
||||||
scriptOpts.named = true;
|
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") {
|
} else if (arg == "--version") {
|
||||||
// Answered here, before the project file is looked for, so
|
// Answered here, before the project file is looked for, so
|
||||||
// it works in any directory and is never forwarded to
|
// it works in any directory and is never forwarded to
|
||||||
|
|
|
||||||
|
|
@ -394,12 +394,11 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
||||||
// source revision (forwarded args don't change what gets cloned, just
|
// source revision (forwarded args don't change what gets cloned, just
|
||||||
// how the dep's project.cpp interprets them at runtime), while the
|
// how the dep's project.cpp interprets them at runtime), while the
|
||||||
// in-memory Configuration is keyed by the full spec so each (args)
|
// in-memory Configuration is keyed by the full spec so each (args)
|
||||||
// permutation gets its own Configuration object, and host tooling one
|
// permutation gets its own Configuration object.
|
||||||
// apart from the target's even for the same args.
|
|
||||||
std::string srcKey = std::format("git|{}|{}|{}|{}", spec.source.url, spec.source.branch, spec.source.commit, spec.projectFile.string());
|
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<std::string>{}(srcKey));
|
std::string srcHash = std::format("{:016x}", std::hash<std::string>{}(srcKey));
|
||||||
|
|
||||||
std::string fullKey = std::format("{}{}", LoadingHostTooling() ? "host|" : "", srcKey);
|
std::string fullKey = srcKey;
|
||||||
for (const std::string& a : spec.args) {
|
for (const std::string& a : spec.args) {
|
||||||
fullKey += '|';
|
fullKey += '|';
|
||||||
fullKey += a;
|
fullKey += a;
|
||||||
|
|
@ -483,7 +482,7 @@ Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) {
|
||||||
}
|
}
|
||||||
fs::path canonical = fs::canonical(projectPath);
|
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) {
|
for (const std::string& a : spec.args) {
|
||||||
fullKey += '|';
|
fullKey += '|';
|
||||||
fullKey += a;
|
fullKey += a;
|
||||||
|
|
|
||||||
|
|
@ -413,11 +413,6 @@ namespace {
|
||||||
|
|
||||||
struct ProjectFileFrame {
|
struct ProjectFileFrame {
|
||||||
fs::path project;
|
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.
|
|
||||||
std::span<const std::string_view> args;
|
|
||||||
std::vector<Configuration*> dependencies;
|
std::vector<Configuration*> dependencies;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -446,7 +441,7 @@ namespace {
|
||||||
throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string()));
|
throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string()));
|
||||||
}
|
}
|
||||||
index_ = LoadingFrames.size();
|
index_ = LoadingFrames.size();
|
||||||
LoadingFrames.push_back({ absProject, {}, {} });
|
LoadingFrames.push_back({ absProject, {} });
|
||||||
}
|
}
|
||||||
~ProjectFileGuard() { LoadingFrames.pop_back(); }
|
~ProjectFileGuard() { LoadingFrames.pop_back(); }
|
||||||
ProjectFileGuard(const ProjectFileGuard&) = delete;
|
ProjectFileGuard(const ProjectFileGuard&) = delete;
|
||||||
|
|
@ -458,29 +453,13 @@ namespace {
|
||||||
std::size_t index_ = 0;
|
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
|
// 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
|
// crafter-build, so the dependency is built for the host and against the
|
||||||
// same PCM cache LoadProject populates — the hostConfig LoadProject itself
|
// same PCM cache LoadProject populates — the hostConfig LoadProject itself
|
||||||
// compiles project.so with, so the dependency's std/Crafter.Build PCMs
|
// compiles project.so with, so the dependency's std/Crafter.Build PCMs
|
||||||
// land in the cache dir project.so is built against. Recursive, because
|
// 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
|
// the dependency's project.cpp forwards no arguments but still reads
|
||||||
// none on, while its own deps still read CRAFTER_BUILD_MARCH from the
|
// CRAFTER_BUILD_MARCH from the environment for its own deps.
|
||||||
// environment.
|
|
||||||
void ForceHostTooling(Configuration& cfg, const Configuration& host, std::unordered_set<Configuration*>& seen) {
|
void ForceHostTooling(Configuration& cfg, const Configuration& host, std::unordered_set<Configuration*>& seen) {
|
||||||
if (!seen.insert(&cfg).second) return;
|
if (!seen.insert(&cfg).second) return;
|
||||||
cfg.target = host.target;
|
cfg.target = host.target;
|
||||||
|
|
@ -523,36 +502,24 @@ namespace {
|
||||||
std::set<std::string> libs;
|
std::set<std::string> libs;
|
||||||
|
|
||||||
for (const PragmaDependency& pragma : pragmas) {
|
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.
|
|
||||||
Configuration* cfg = nullptr;
|
Configuration* cfg = nullptr;
|
||||||
{
|
if (IsGitSource(pragma.source)) {
|
||||||
HostToolingScope hostTooling;
|
GitProjectSpec spec;
|
||||||
if (IsGitSource(pragma.source)) {
|
spec.source = { pragma.source, pragma.branch, pragma.commit };
|
||||||
GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit } };
|
if (!pragma.project.empty()) spec.projectFile = pragma.project;
|
||||||
if (!pragma.project.empty()) spec.projectFile = pragma.project;
|
cfg = GitProject(spec);
|
||||||
cfg = GitProject(spec);
|
} else {
|
||||||
} else {
|
if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source");
|
||||||
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
|
||||||
// Relative to the project file, not the cwd: LoadProject runs
|
// from wherever the consumer was invoked.
|
||||||
// from wherever the consumer was invoked.
|
fs::path local = (absProject.parent_path() / pragma.source).lexically_normal();
|
||||||
fs::path local = (absProject.parent_path() / pragma.source).lexically_normal();
|
if (fs::is_directory(local)) {
|
||||||
if (fs::is_directory(local)) {
|
local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project);
|
||||||
local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project);
|
} else if (!pragma.project.empty()) {
|
||||||
} else if (!pragma.project.empty()) {
|
PragmaError(absProject, pragma.line, "'project' only applies to a directory source");
|
||||||
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 (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
|
||||||
|
cfg = LocalProject({ .projectFile = local });
|
||||||
}
|
}
|
||||||
if (std::ranges::contains(out.configs, cfg)) continue;
|
if (std::ranges::contains(out.configs, cfg)) continue;
|
||||||
|
|
||||||
|
|
@ -629,16 +596,6 @@ std::vector<Configuration*> Crafter::PragmaDependencies(const fs::path& projectF
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> Crafter::ProjectArgs() {
|
|
||||||
if (LoadingFrames.empty()) return {};
|
|
||||||
const std::span<const std::string_view> args = LoadingFrames.back().args;
|
|
||||||
return std::vector<std::string>(args.begin(), args.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Crafter::LoadingHostTooling() {
|
|
||||||
return HostToolingLoads > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// The one reading of a wait status (pclose, system) for every helper
|
// 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
|
// that runs a command: a signal death is reported the way a shell
|
||||||
|
|
@ -1014,13 +971,6 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
WriteStamp(StampPath(soPath), projectStamp);
|
WriteStamp(StampPath(soPath), projectStamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before dlopen, so the file's static initialisers can already call
|
|
||||||
// ProjectDependency(): its dependencies are the same on every load.
|
|
||||||
{
|
|
||||||
std::lock_guard lock(PragmaDependenciesMutex);
|
|
||||||
PragmaDependenciesByProject[absProject] = preDeps.configs;
|
|
||||||
}
|
|
||||||
guard.Frame().dependencies = std::move(preDeps.configs);
|
|
||||||
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
|
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
throw std::runtime_error(std::format("Failed to load project {}: {}", soPath.string(), dlerror()));
|
throw std::runtime_error(std::format("Failed to load project {}: {}", soPath.string(), dlerror()));
|
||||||
|
|
@ -1033,12 +983,10 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: {}", soPath.string(), err));
|
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: {}", soPath.string(), err));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The args only for the call itself, unlike the dependencies. A static
|
{
|
||||||
// initialiser runs once per process, on the first load of the file: a
|
std::lock_guard lock(PragmaDependenciesMutex);
|
||||||
// later load gets the same mapping back from dlopen, so a spec built at
|
PragmaDependenciesByProject[absProject] = preDeps.configs;
|
||||||
// namespace scope kept the first load's args for every load after it.
|
}
|
||||||
// Now it gets none, whichever load ran it. The pragma pre-pass above
|
guard.Frame().dependencies = std::move(preDeps.configs);
|
||||||
// also ran on this frame and relies on the args not being there yet.
|
|
||||||
guard.Frame().args = args;
|
|
||||||
return fn(args);
|
return fn(args);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -484,9 +484,7 @@ export namespace Crafter {
|
||||||
// cfg.files, say), and without it both flag settings share one bin dir
|
// cfg.files, say), and without it both flag settings share one bin dir
|
||||||
// and interleave their outputs there. Populated by ApplyStandardArgs;
|
// and interleave their outputs there. Populated by ApplyStandardArgs;
|
||||||
// the args it does recognise are excluded, since their effect already
|
// the args it does recognise are excluded, since their effect already
|
||||||
// shows up in target/march/mtune/debug/sysroot/type. For the same
|
// shows up in target/march/mtune/debug/sysroot/type.
|
||||||
// reason this is not what to hand a sibling project: ProjectArgs() is
|
|
||||||
// every arg, the recognised ones included.
|
|
||||||
std::vector<std::string> projectArgs;
|
std::vector<std::string> projectArgs;
|
||||||
std::vector<Test> tests;
|
std::vector<Test> tests;
|
||||||
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
||||||
|
|
|
||||||
|
|
@ -71,56 +71,31 @@ export namespace Crafter {
|
||||||
// clone can still differ in includeDirs, libDirs and libs.
|
// clone can still differ in includeDirs, libDirs and libs.
|
||||||
ExternalBuildResult ExternalOutputs(const ExternalDependency& dep, std::string_view target);
|
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.
|
|
||||||
std::vector<std::string> ProjectArgs();
|
|
||||||
|
|
||||||
// Specification for a sibling crafter-build project to fetch and depend on.
|
// Specification for a sibling crafter-build project to fetch and depend on.
|
||||||
// GitSource picks the revision: leave branch + commit empty for the
|
// GitSource picks the revision: leave branch + commit empty for the
|
||||||
// remote's default branch HEAD (sloppy but convenient), set branch to
|
// 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
|
// 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
|
// forwarded verbatim to the dep's CrafterBuildProject — typically you
|
||||||
// project's own, read when the spec is made, so --target=, --march=,
|
// pass through the parent's args so target/march/debug propagate.
|
||||||
// --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:
|
|
||||||
// Build() refuses a graph in which two paths load one project with two
|
|
||||||
// different lists, since the program would end up with both builds.
|
|
||||||
struct GitProjectSpec {
|
struct GitProjectSpec {
|
||||||
GitSource source;
|
GitSource source;
|
||||||
fs::path projectFile = "project.cpp";
|
fs::path projectFile = "project.cpp";
|
||||||
std::vector<std::string> args = ProjectArgs();
|
std::vector<std::string> args;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clones the spec's git URL into the external cache, recursively
|
// Clones the spec's git URL into the per-project external cache (keyed by
|
||||||
// LoadProject's the remote's project.cpp, and returns a stable
|
// url+branch+commit+args+projectFile), recursively LoadProject's the
|
||||||
// Configuration* you can drop into cfg.dependencies. The clone is keyed by
|
// remote's project.cpp, and returns a stable Configuration* you can drop
|
||||||
// url+branch+commit+projectFile alone, so every arg list builds in the
|
// into cfg.dependencies. The Configuration is owned by an internal cache
|
||||||
// same working tree, each into variant directories of its own. The
|
// for the lifetime of the build; identical specs share one Configuration.
|
||||||
// Configuration is owned by an internal cache for the lifetime of the
|
|
||||||
// build, keyed by the clone and the args: identical specs share one.
|
|
||||||
Configuration* GitProject(const GitProjectSpec& spec);
|
Configuration* GitProject(const GitProjectSpec& spec);
|
||||||
|
|
||||||
// Specification for a local sibling crafter-build project (e.g. an
|
// Specification for a local sibling crafter-build project (e.g. an
|
||||||
// example folder depending on its parent project, or a workspace where
|
// example folder depending on its parent project, or a workspace where
|
||||||
// multiple projects live side-by-side without going through git).
|
// multiple projects live side-by-side without going through git).
|
||||||
struct LocalProjectSpec {
|
struct LocalProjectSpec {
|
||||||
fs::path projectFile; // relative to cwd or absolute
|
fs::path projectFile; // relative to cwd or absolute
|
||||||
std::vector<std::string> args = ProjectArgs(); // as GitProjectSpec::args
|
std::vector<std::string> args; // forwarded to dep's CrafterBuildProject
|
||||||
};
|
};
|
||||||
|
|
||||||
// Same as GitProject but for a local on-disk project — no fetch, no
|
// Same as GitProject but for a local on-disk project — no fetch, no
|
||||||
|
|
|
||||||
|
|
@ -82,12 +82,6 @@ namespace Crafter {
|
||||||
// dependency notes of the project crafter-build was pointed at are
|
// dependency notes of the project crafter-build was pointed at are
|
||||||
// gathered (Crafter.Build:Agents).
|
// gathered (Crafter.Build:Agents).
|
||||||
std::vector<Configuration*> PragmaDependencies(const fs::path& projectFile);
|
std::vector<Configuration*> PragmaDependencies(const fs::path& projectFile);
|
||||||
// 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
|
// Resolves the directory holding distributed runtime assets (Crafter.Build
|
||||||
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
||||||
// derives <prefix>/share/crafter-build from the running executable's path.
|
// derives <prefix>/share/crafter-build from the running executable's path.
|
||||||
|
|
|
||||||
29
project.cpp
29
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<const std::string_view> args) {
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
Configuration* math = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" } });
|
std::vector<std::string> depArgs(args.begin(), args.end());
|
||||||
Configuration* asset = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git" } });
|
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<Configuration>();
|
static auto CrafterBuildLib = std::make_unique<Configuration>();
|
||||||
CrafterBuildLib->path = "./";
|
CrafterBuildLib->path = "./";
|
||||||
|
|
@ -282,12 +289,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||||
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.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("DuplicateModule").Dependencies({ CrafterBuildLib.get() })
|
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
||||||
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||||
cfg.AddTest("DependencyNotes").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("DependencyNotes").Dependencies({ CrafterBuildLib.get() })
|
||||||
|
|
@ -296,14 +297,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dogfood: the lite house rules, imported from their own repo through
|
// Dogfood: the house-style rules, imported from their own repo through
|
||||||
// the pragma above. Lite is the house rules without the ones that only
|
// the pragma above. Report rules gate `crafter-build lint`; transform
|
||||||
// decide layout, whitespace and naming, which this tree keeps by hand:
|
// rules also auto-fix under `crafter-build format`.
|
||||||
// the full set's naming rule, for one, wants camelCase where this tree
|
LintRules::AddRules(cfg);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// See the top of this file.
|
// See the top of this file.
|
||||||
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export module A;
|
|
||||||
import std;
|
|
||||||
import M;
|
|
||||||
export std::int32_t ASize() {
|
|
||||||
return Size();
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
||||||
// Loads M with the default, so M gets whatever A was given.
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
||||||
Configuration* m = LocalProject({ .projectFile = "../m/project.cpp" });
|
|
||||||
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "./";
|
|
||||||
cfg.name = "A";
|
|
||||||
cfg.outputName = "A";
|
|
||||||
cfg.type = ConfigurationType::LibraryStatic;
|
|
||||||
ApplyStandardArgs(cfg, args);
|
|
||||||
cfg.dependencies = { m };
|
|
||||||
std::array<fs::path, 1> ifaces = { "interfaces/A" };
|
|
||||||
std::array<fs::path, 0> impls = {};
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export module M;
|
|
||||||
import std;
|
|
||||||
// --wide gives S more members, so the two builds of M the test asks for
|
|
||||||
// differ in what they declare.
|
|
||||||
#ifdef WIDE
|
|
||||||
export struct S { std::int32_t a; std::int32_t b; std::int64_t pad[4]; };
|
|
||||||
#else
|
|
||||||
export struct S { std::int32_t a; std::int32_t b; };
|
|
||||||
#endif
|
|
||||||
export std::int32_t Size() {
|
|
||||||
return static_cast<std::int32_t>(sizeof(S));
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +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;
|
|
||||||
|
|
||||||
// --wide is this project's own flag, and it changes what M declares.
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "./";
|
|
||||||
cfg.name = "M";
|
|
||||||
cfg.outputName = "M";
|
|
||||||
cfg.type = ConfigurationType::LibraryStatic;
|
|
||||||
ApplyStandardArgs(cfg, args);
|
|
||||||
if (std::ranges::contains(args, std::string_view("--wide"))) cfg.compileFlags.push_back("-DWIDE");
|
|
||||||
std::array<fs::path, 1> ifaces = { "interfaces/M" };
|
|
||||||
std::array<fs::path, 0> impls = {};
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import std;
|
|
||||||
import M;
|
|
||||||
import A;
|
|
||||||
int main() {
|
|
||||||
return Size() == ASize() ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +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;
|
|
||||||
|
|
||||||
// Loads M itself and through A, which passes on all of its args. With
|
|
||||||
// --split, the list P gives M leaves --wide out, as a filter meant for one
|
|
||||||
// sibling would, and the two paths load M with two different lists.
|
|
||||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
||||||
const bool split = std::ranges::contains(args, std::string_view("--split"));
|
|
||||||
std::vector<std::string> mArgs;
|
|
||||||
for (std::string_view arg : args) {
|
|
||||||
if (!split || arg != "--wide") mArgs.emplace_back(arg);
|
|
||||||
}
|
|
||||||
Configuration* m = LocalProject({ .projectFile = "../m/project.cpp", .args = mArgs });
|
|
||||||
Configuration* a = LocalProject({ .projectFile = "../a/project.cpp" });
|
|
||||||
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "./";
|
|
||||||
cfg.name = "P";
|
|
||||||
cfg.outputName = "P";
|
|
||||||
cfg.type = ConfigurationType::Executable;
|
|
||||||
ApplyStandardArgs(cfg, args);
|
|
||||||
cfg.dependencies = { m, a };
|
|
||||||
std::array<fs::path, 0> ifaces = {};
|
|
||||||
std::array<fs::path, 1> impls = { "main" };
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -1,84 +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 program can hold one module of each name, so Build() refuses a graph in
|
|
||||||
// which two configurations export the same module: the same project loaded
|
|
||||||
// along two paths with two arg lists, say. It used to build both and put
|
|
||||||
// both on the consumer's module path. Clang took the first for every import
|
|
||||||
// of the module, the ones inside the other build's consumers included, and
|
|
||||||
// the program linked code compiled against two layouts of one struct: it
|
|
||||||
// built cleanly, then read garbage or crashed (issue #41). The fixture is
|
|
||||||
// P -> M and P -> A -> M. With --split, P gives M its args without --wide,
|
|
||||||
// which changes what M declares, while A passes them on whole.
|
|
||||||
namespace {
|
|
||||||
std::int32_t Failures = 0;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildResult BuildPass(Configuration& cfg) {
|
|
||||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
||||||
std::mutex depMutex;
|
|
||||||
return Build(cfg, depResults, depMutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 and Build drop build/ and bin/ trees next
|
|
||||||
// to the project files.
|
|
||||||
const fs::path fixture = fs::current_path() / "tests" / "DuplicateModule" / "fixture";
|
|
||||||
const fs::path scratch = fs::temp_directory_path() / "crafter-build-duplicate-module";
|
|
||||||
std::error_code ec;
|
|
||||||
fs::remove_all(scratch, ec);
|
|
||||||
fs::create_directories(scratch);
|
|
||||||
fs::copy(fixture, scratch, fs::copy_options::recursive);
|
|
||||||
|
|
||||||
// From P'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 / "p");
|
|
||||||
try {
|
|
||||||
// Two lists for M: refused before anything is built.
|
|
||||||
{
|
|
||||||
std::array<std::string_view, 2> args = { "--wide", "--split" };
|
|
||||||
Configuration cfg = LoadProject("project.cpp", args);
|
|
||||||
const BuildResult built = BuildPass(cfg);
|
|
||||||
Check(!built.result.empty(), "a graph with two builds of one module fails to build");
|
|
||||||
Check(built.result.contains("module M is built twice for P"), std::format("the error names the module and the program (got: {})", built.result));
|
|
||||||
Check(built.result.contains("P -> M (") && built.result.contains("P -> A -> M ("), std::format("the error names the path to each build (got: {})", built.result));
|
|
||||||
Check(!fs::exists(scratch / "m" / "bin") && !fs::exists(scratch / "a" / "bin") && !fs::exists(scratch / "p" / "bin"), "nothing was built");
|
|
||||||
}
|
|
||||||
|
|
||||||
// One list along both paths: one M, and the build goes through.
|
|
||||||
{
|
|
||||||
std::array<std::string_view, 1> args = { "--wide" };
|
|
||||||
Configuration cfg = LoadProject("project.cpp", args);
|
|
||||||
const bool oneM = cfg.dependencies.size() == 2 && cfg.dependencies[1]->dependencies.size() == 1 && cfg.dependencies[0] == cfg.dependencies[1]->dependencies[0];
|
|
||||||
Check(oneM, "the same list along both paths loads one M");
|
|
||||||
const BuildResult built = BuildPass(cfg);
|
|
||||||
Check(built.result.empty(), std::format("the graph with one M builds (got: {})", built.result));
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
fs::current_path(cwd);
|
|
||||||
std::println(std::cerr, "FAIL: threw: {}", e.what());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
fs::current_path(cwd);
|
|
||||||
|
|
||||||
return Failures > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
@ -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<const std::string_view> 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export module Shared;
|
|
||||||
import std;
|
|
||||||
export std::int32_t SharedValue() {
|
|
||||||
return 7;
|
|
||||||
}
|
|
||||||
|
|
@ -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<const std::string_view> args) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "./";
|
|
||||||
cfg.name = "shared";
|
|
||||||
cfg.outputName = "shared";
|
|
||||||
cfg.type = ConfigurationType::LibraryStatic;
|
|
||||||
ApplyStandardArgs(cfg, args);
|
|
||||||
std::array<fs::path, 1> ifaces = { "interfaces/Shared" };
|
|
||||||
std::array<fs::path, 0> impls = {};
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export module Tool;
|
|
||||||
import std;
|
|
||||||
import Shared;
|
|
||||||
export std::int32_t ToolValue() {
|
|
||||||
return SharedValue();
|
|
||||||
}
|
|
||||||
|
|
@ -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<const std::string_view> 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<fs::path, 1> ifaces = { "interfaces/Tool" };
|
|
||||||
std::array<fs::path, 0> impls = {};
|
|
||||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
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<std::string_view, 0> 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +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. 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<const std::string_view> 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 = "./";
|
|
||||||
cfg.name = "app";
|
|
||||||
cfg.outputName = "app";
|
|
||||||
cfg.type = ConfigurationType::Executable;
|
|
||||||
ApplyStandardArgs(cfg, args);
|
|
||||||
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" } };
|
|
||||||
const bool gitDefault = git.args == std::vector<std::string>(args.begin(), args.end());
|
|
||||||
cfg.compileFlags.push_back(gitDefault ? "-DGIT_DEFAULT=1" : "-DGIT_DEFAULT=0");
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -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<const std::string_view> args) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "./";
|
|
||||||
cfg.name = "leaf";
|
|
||||||
cfg.outputName = "leaf";
|
|
||||||
cfg.type = ConfigurationType::LibraryStatic;
|
|
||||||
ApplyStandardArgs(cfg, args);
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -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<const std::string_view> 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;
|
|
||||||
}
|
|
||||||
|
|
@ -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<const std::string_view> 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +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<std::string> 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. 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;
|
|
||||||
|
|
||||||
void Check(bool cond, std::string_view msg) {
|
|
||||||
if (!cond) {
|
|
||||||
std::println(std::cerr, "FAIL: {}", msg);
|
|
||||||
++Failures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<std::string>& 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() {
|
|
||||||
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<std::string_view, 2> args = { "--debug", "--flavor=x" };
|
|
||||||
const std::vector<std::string> flavor = { "--flavor=x" };
|
|
||||||
const std::vector<std::string> 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() == 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.
|
|
||||||
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);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -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<const std::string_view>) {
|
|
||||||
Configuration cfg;
|
|
||||||
cfg.path = "./";
|
|
||||||
cfg.name = std::format("static {}", Greeter->name);
|
|
||||||
cfg.outputName = "static-dep";
|
|
||||||
cfg.type = ConfigurationType::Executable;
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
@ -90,19 +90,14 @@ int main(int argc, char** argv) {
|
||||||
|
|
||||||
const fs::path app = scratch / "app" / "project.cpp";
|
const fs::path app = scratch / "app" / "project.cpp";
|
||||||
const fs::path appSo = scratch / "app" / "build" / "project.so";
|
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");
|
Check(IsTopLevelProject(), "outside any project file counts as top level");
|
||||||
|
|
||||||
// The dependency is fetched, built and linked before project.cpp runs,
|
// The dependency is fetched, built and linked before project.cpp runs,
|
||||||
// and ProjectDependency() hands it back by either name. The app ignores
|
// and ProjectDependency() hands it back by either name.
|
||||||
// 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 {
|
try {
|
||||||
std::array<std::string_view, 1> flavored = { "--flavor=x" };
|
Configuration cfg = LoadProject(app, noArgs);
|
||||||
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(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(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");
|
Check(cfg.dependencies.size() == 2, "ProjectDependency resolved by module name and by configuration name");
|
||||||
|
|
@ -114,23 +109,12 @@ int main(int argc, char** argv) {
|
||||||
Check(dep.march == "native", "the environment's march was overridden for host tooling");
|
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, "-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(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");
|
Check(fs::exists(ExpectedOutput(dep)), "the dependency archive exists");
|
||||||
}
|
}
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what());
|
std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what());
|
||||||
return 1;
|
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");
|
Check(fs::exists(appSo), "project.so was produced");
|
||||||
if (!fs::exists(appSo)) return 1;
|
if (!fs::exists(appSo)) return 1;
|
||||||
const fs::file_time_type firstSo = fs::last_write_time(appSo);
|
const fs::file_time_type firstSo = fs::last_write_time(appSo);
|
||||||
|
|
|
||||||
|
|
@ -218,29 +218,6 @@ int main() {
|
||||||
Check(fs::exists(binSentinel) && fs::exists(buildSentinel), "`clean` as a script argument deleted nothing");
|
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<std::string> expected = {
|
|
||||||
"arg:--", "arg:a",
|
|
||||||
"projectArg:--flavour=x",
|
|
||||||
"debug:false",
|
|
||||||
"project:script-fixture",
|
|
||||||
};
|
|
||||||
const std::vector<std::string> 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.
|
// The first word after `run` is the name even when it is a verb.
|
||||||
{
|
{
|
||||||
fs::remove(scratch / "record.txt");
|
fs::remove(scratch / "record.txt");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue