Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cf295e1d4 | |||
| 8fadbdae37 | |||
| 973c2708fe | |||
| 3ad1951057 | |||
| 17021c6055 | |||
|
6e2d835aa6 |
|||
|
7b548b9ce4 |
|||
|
0a06faec6e |
|||
|
f77d913171 |
16 changed files with 558 additions and 157 deletions
13
AGENTS.md
13
AGENTS.md
|
|
@ -86,7 +86,7 @@ Conventions the existing tests follow:
|
||||||
- Collect failures with a `Check(cond, msg)` helper that prints `FAIL: ...` to stderr and counts, then return nonzero at the end. Do not stop at the first failure unless later steps are meaningless without it.
|
- Collect failures with a `Check(cond, msg)` helper that prints `FAIL: ...` to stderr and counts, then return nonzero at the end. Do not stop at the first failure unless later steps are meaningless without it.
|
||||||
- Drive the engine in-process: `Build(cfg, depResults, depMutex)`, `RunTests`, the lint driver. Do not shell out to `bin/crafter-build` and grep its stdout.
|
- Drive the engine in-process: `Build(cfg, depResults, depMutex)`, `RunTests`, the lint driver. Do not shell out to `bin/crafter-build` and grep its stdout.
|
||||||
- Fixtures under `tests/<Name>/fixture/` are inputs. A test that edits or touches files first stages a copy under `fs::temp_directory_path() / "crafter-build-<name>"` and removes it on the next run. Variant sources are checked in as `*.in` files and copied over the original inside the staged copy.
|
- Fixtures under `tests/<Name>/fixture/` are inputs. A test that edits or touches files first stages a copy under `fs::temp_directory_path() / "crafter-build-<name>"` and removes it on the next run. Variant sources are checked in as `*.in` files and copied over the original inside the staged copy.
|
||||||
- Use a fresh `depResults` map per build pass: the map memoises each configuration's result for one pass, so reusing it skips rebuilds you meant to observe. `Build()` itself is safe to call repeatedly on the same `Configuration`.
|
- Use a fresh `depResults` map per build pass: the map memoises each configuration's result, and each external dependency's fetch and build, for one pass, so reusing it skips rebuilds and pulls you meant to observe. `Build()` itself is safe to call repeatedly on the same `Configuration`.
|
||||||
- Set `cfg.path` before calling `GetInterfacesAndImplementations`, and spell `cFiles` as absolute paths: both resolve against the working directory, not the configuration.
|
- Set `cfg.path` before calling `GetInterfacesAndImplementations`, and spell `cFiles` as absolute paths: both resolve against the working directory, not the configuration.
|
||||||
|
|
||||||
A bug fix ships with a test that fails before the fix and passes after it, in its own `test:` commit when it documents the behaviour on its own.
|
A bug fix ships with a test that fails before the fix and passes after it, in its own `test:` commit when it documents the behaviour on its own.
|
||||||
|
|
@ -95,14 +95,17 @@ Cross-target runs use `test --target=<triple>` with `--runner=local` or `--runne
|
||||||
|
|
||||||
## Code style
|
## Code style
|
||||||
|
|
||||||
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:
|
`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:
|
||||||
|
|
||||||
- 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.
|
||||||
- `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, and no hand-wrapping of a statement that fits on one line. `if (` and `for (` with a space; no space before a call's `(`.
|
||||||
- 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ glslang.includeDirs = { "" };
|
||||||
glslang.libs = { "SPIRV", "glslang", "OSDependent" };
|
glslang.libs = { "SPIRV", "glslang", "OSDependent" };
|
||||||
```
|
```
|
||||||
|
|
||||||
Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url, branch, commit, options)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes. Submodules are checked out at the revisions the fetched commit records, after every clone, switch, checkout and pull, so a pinned commit builds against its own submodules and not the default branch's.
|
Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url, branch, commit, options, target)` so different projects sharing the same dep with the same spec reuse the clone and cmake build, while projects with diverging specs each get their own cache entry. Without a `commit`, the clone is `git pull`ed once per build: every configuration of the build that declares the dependency, such as each test of a project whose tests copy its `externalDependencies`, shares that one fetch and cmake build. The clone is resilient to partial-clone recovery and CMake reconfigure is triggered automatically on `options` changes. Submodules are checked out at the revisions the fetched commit records, after every clone, switch, checkout and pull, so a pinned commit builds against its own submodules and not the default branch's.
|
||||||
|
|
||||||
**Header-only git deps** — same as cmake deps but `builder = ExternalBuilder::None`. Just clones and propagates `-I` flags.
|
**Header-only git deps** — same as cmake deps but `builder = ExternalBuilder::None`. Just clones and propagates `-I` flags.
|
||||||
|
|
||||||
|
|
@ -69,12 +69,13 @@ 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", .args = depArgs });
|
Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project.cpp" });
|
||||||
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. 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.
|
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.
|
||||||
|
|
@ -94,6 +95,8 @@ 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.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
Tests live under `tests/<Name>/`. The simplest case is a single C++ file:
|
Tests live under `tests/<Name>/`. The simplest case is a single C++ file:
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,28 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The outcome of `work` for `key` in this build pass. The first caller
|
||||||
|
// runs it; every other caller, whether at the same time or later, waits
|
||||||
|
// for that run and gets its result or its exception. The keys are the
|
||||||
|
// directories the work writes, a configuration's PcmDir and an external
|
||||||
|
// dependency's ExternalBuildDir, so the two kinds never collide.
|
||||||
|
std::shared_future<BuildResult> OncePerPass(std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex, const fs::path& key, const std::function<BuildResult()>& work) {
|
||||||
|
std::promise<BuildResult> promise;
|
||||||
|
std::shared_future<BuildResult> outcome = promise.get_future().share();
|
||||||
|
{
|
||||||
|
std::lock_guard lock(depMutex);
|
||||||
|
if (auto [it, inserted] = depResults.try_emplace(key, outcome); !inserted) return it->second;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
promise.set_value(work());
|
||||||
|
} catch (...) {
|
||||||
|
promise.set_exception(std::current_exception());
|
||||||
|
}
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
// Before anything is spawned: one message naming the missing compiler
|
// Before anything is spawned: one message naming the missing compiler
|
||||||
// beats a clang++-not-found per translation unit.
|
// beats a clang++-not-found per translation unit.
|
||||||
|
|
@ -678,14 +700,30 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
externalThreads.reserve(config.externalDependencies.size());
|
externalThreads.reserve(config.externalDependencies.size());
|
||||||
for (std::size_t i = 0; i < config.externalDependencies.size(); ++i) {
|
for (std::size_t i = 0; i < config.externalDependencies.size(); ++i) {
|
||||||
externalThreads.emplace_back([&, i]() {
|
externalThreads.emplace_back([&, i]() {
|
||||||
Progress::Task task(std::format("Building external dep {}", config.externalDependencies[i].name));
|
const ExternalDependency& dep = config.externalDependencies[i];
|
||||||
|
Progress::Task task(std::format("Building external dep {}", dep.name));
|
||||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||||
externalResults[i] = BuildExternal(config.externalDependencies[i], config.target, buildCancelled);
|
std::string error;
|
||||||
if (!externalResults[i].error.empty()) {
|
try {
|
||||||
bool expected = false;
|
// Every configuration of the pass that declares this
|
||||||
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
// dependency shares one fetch and build of it. A project's
|
||||||
buildError = externalResults[i].error;
|
// tests often each copy its externalDependencies, and RunTests
|
||||||
}
|
// builds them side by side: each of them pulling the clone
|
||||||
|
// cost a network round trip per test, and before the clone had
|
||||||
|
// a lock, the simultaneous pulls broke its FETCH_HEAD and
|
||||||
|
// failed every test (issue #34).
|
||||||
|
const std::shared_future<BuildResult> built = OncePerPass(depResults, depMutex, ExternalBuildDir(dep, config.target), [&] {
|
||||||
|
return BuildResult{BuildExternal(dep, config.target), false, {}};
|
||||||
|
});
|
||||||
|
error = built.get().result;
|
||||||
|
if (error.empty()) externalResults[i] = ExternalOutputs(dep, config.target);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
error = std::format("external dep {} threw: {}", dep.name, e.what());
|
||||||
|
}
|
||||||
|
if (error.empty()) return;
|
||||||
|
bool expected = false;
|
||||||
|
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
||||||
|
buildError = std::move(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -746,35 +784,8 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
try {
|
try {
|
||||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||||
|
|
||||||
std::shared_ptr<std::promise<BuildResult>> promise;
|
const std::shared_future<BuildResult> built = OncePerPass(depResults, depMutex, dep->PcmDir(), [&] { return Build(*dep, depResults, depMutex); });
|
||||||
std::shared_future<BuildResult> resultFuture;
|
const BuildResult& result = built.get();
|
||||||
bool isBuilder = false;
|
|
||||||
|
|
||||||
depMutex.lock();
|
|
||||||
fs::path cacheKey = dep->PcmDir();
|
|
||||||
auto it = depResults.find(cacheKey);
|
|
||||||
if (it == depResults.end()) {
|
|
||||||
isBuilder = true;
|
|
||||||
promise = std::make_shared<std::promise<BuildResult>>();
|
|
||||||
resultFuture = promise->get_future().share();
|
|
||||||
depResults.emplace(cacheKey, resultFuture);
|
|
||||||
} else {
|
|
||||||
resultFuture = it->second;
|
|
||||||
}
|
|
||||||
depMutex.unlock();
|
|
||||||
|
|
||||||
if (isBuilder) {
|
|
||||||
BuildResult built;
|
|
||||||
try {
|
|
||||||
built = Build(*dep, depResults, depMutex);
|
|
||||||
} catch (...) {
|
|
||||||
promise->set_exception(std::current_exception());
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
promise->set_value(std::move(built));
|
|
||||||
}
|
|
||||||
|
|
||||||
const BuildResult& result = resultFuture.get();
|
|
||||||
fileMutex.lock();
|
fileMutex.lock();
|
||||||
for (const std::string& lib : result.libs) libSet.insert(lib);
|
for (const std::string& lib : result.libs) libSet.insert(lib);
|
||||||
for (const std::string& f : result.publicCompileFlags) publicFlagSet.insert(f);
|
for (const std::string& f : result.publicCompileFlags) publicFlagSet.insert(f);
|
||||||
|
|
|
||||||
|
|
@ -231,15 +231,16 @@ std::vector<std::string> Crafter::ExternalIncludeFlags(const ExternalDependency&
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled) {
|
fs::path Crafter::ExternalBuildDir(const ExternalDependency& dep, std::string_view target) {
|
||||||
ExternalBuildResult result;
|
fs::path cloneDir = ExternalCloneDir(dep, target);
|
||||||
|
if (cloneDir.empty() || dep.builder != ExternalBuilder::CMake) return cloneDir;
|
||||||
if (cancelled.load(std::memory_order_relaxed)) return result;
|
return cloneDir / "build";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Crafter::BuildExternal(const ExternalDependency& dep, std::string_view target) {
|
||||||
std::string name = dep.name.empty() ? DeriveName(dep.source) : dep.name;
|
std::string name = dep.name.empty() ? DeriveName(dep.source) : dep.name;
|
||||||
if (name.empty()) {
|
if (name.empty()) {
|
||||||
result.error = std::format("Could not derive name for external dependency from URL '{}'", dep.source.url);
|
return std::format("Could not derive name for external dependency from URL '{}'", dep.source.url);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::path externalRoot = GetCacheDir() / "external";
|
fs::path externalRoot = GetCacheDir() / "external";
|
||||||
|
|
@ -247,77 +248,73 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::create_directories(externalRoot, ec);
|
fs::create_directories(externalRoot, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
result.error = std::format("Failed to create {}: {}", externalRoot.string(), ec.message());
|
return std::format("Failed to create {}: {}", externalRoot.string(), ec.message());
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::path cloneDir = ExternalCloneDir(dep, target);
|
fs::path cloneDir = ExternalCloneDir(dep, target);
|
||||||
// Builds running side by side (tests built in parallel, two
|
// Build() makes one call for all the configurations of a pass, but two
|
||||||
// crafter-builds) share the clone: one fetches and builds it at a time.
|
// crafter-builds, or two passes, can still reach the same clone at once,
|
||||||
|
// and two pulls in one clone race on its .git/FETCH_HEAD.
|
||||||
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
FileLock lock(fs::path(std::format("{}.lock", cloneDir.string())));
|
||||||
|
|
||||||
std::string fetchErr = FetchGit(dep.source, cloneDir);
|
if (std::string err = FetchGit(dep.source, cloneDir); !err.empty()) {
|
||||||
if (!fetchErr.empty()) {
|
return std::format("git fetch for '{}': {}", name, err);
|
||||||
result.error = std::format("git fetch for '{}': {}", name, fetchErr);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
if (dep.builder != ExternalBuilder::CMake) return "";
|
||||||
|
|
||||||
if (cancelled.load(std::memory_order_relaxed)) return result;
|
const fs::path cmakeBuildDir = ExternalBuildDir(dep, target);
|
||||||
|
if (std::string err = ConfigureCMake(cloneDir, cmakeBuildDir, dep.options, target); !err.empty()) {
|
||||||
fs::path cmakeBuildDir;
|
return std::format("cmake configure for '{}': {}", name, err);
|
||||||
if (dep.builder == ExternalBuilder::CMake) {
|
|
||||||
cmakeBuildDir = cloneDir / "build";
|
|
||||||
if (std::string err = ConfigureCMake(cloneDir, cmakeBuildDir, dep.options, target); !err.empty()) {
|
|
||||||
result.error = std::format("cmake configure for '{}': {}", name, err);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (cancelled.load(std::memory_order_relaxed)) return result;
|
|
||||||
if (std::string err = BuildCMake(cmakeBuildDir); !err.empty()) {
|
|
||||||
result.error = std::format("cmake build for '{}': {}", name, err);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (std::string err = BuildCMake(cmakeBuildDir); !err.empty()) {
|
||||||
|
return std::format("cmake build for '{}': {}", name, err);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalBuildResult Crafter::ExternalOutputs(const ExternalDependency& dep, std::string_view target) {
|
||||||
|
ExternalBuildResult result;
|
||||||
result.compileFlags = ExternalIncludeFlags(dep, target);
|
result.compileFlags = ExternalIncludeFlags(dep, target);
|
||||||
|
|
||||||
if (dep.builder == ExternalBuilder::CMake) {
|
const fs::path cmakeBuildDir = ExternalBuildDir(dep, target);
|
||||||
// Each search path gets both a -L (link-time) and a -Wl,-rpath
|
if (dep.builder != ExternalBuilder::CMake || cmakeBuildDir.empty()) return result;
|
||||||
// (runtime-loader). The rpath stays embedded in the produced
|
|
||||||
// binary so it picks up shared deps from the cache without any
|
|
||||||
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
|
||||||
// harmlessly. The PE/COFF target (mingw) resolves DLLs via PATH or
|
|
||||||
// adjacency rather than rpath, so lld warns if we pass it.
|
|
||||||
const bool isPe = target == "x86_64-w64-mingw32";
|
|
||||||
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
|
||||||
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
|
||||||
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
|
||||||
for (const fs::path& libDir : dep.libDirs) {
|
|
||||||
fs::path full = libDir.is_absolute() ? libDir : cmakeBuildDir / libDir;
|
|
||||||
std::string fullAbs = fs::absolute(full).string();
|
|
||||||
result.linkFlags.push_back(std::format("-L{}", fullAbs));
|
|
||||||
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", fullAbs));
|
|
||||||
}
|
|
||||||
for (const std::string& lib : dep.libs) {
|
|
||||||
result.linkFlags.push_back(std::format("-l{}", lib));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The newest library in every directory the link searches: the build
|
// Each search path gets both a -L (link-time) and a -Wl,-rpath
|
||||||
// directory and the libDirs, where projects like msquic put theirs,
|
// (runtime-loader). The rpath stays embedded in the produced
|
||||||
// versioned names (libmsquic.so.2) included.
|
// binary so it picks up shared deps from the cache without any
|
||||||
auto isLibrary = [](const std::string& name) {
|
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
||||||
return name.ends_with(".a") || name.ends_with(".so") || name.contains(".so.") || name.ends_with(".dll");
|
// harmlessly. The PE/COFF target (mingw) resolves DLLs via PATH or
|
||||||
};
|
// adjacency rather than rpath, so lld warns if we pass it.
|
||||||
std::vector<fs::path> searched = { cmakeBuildDir };
|
const bool isPe = target == "x86_64-w64-mingw32";
|
||||||
for (const fs::path& libDir : dep.libDirs) searched.push_back(libDir.is_absolute() ? libDir : cmakeBuildDir / libDir);
|
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
||||||
for (const fs::path& dir : searched) {
|
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
||||||
std::error_code listEc;
|
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
||||||
for (fs::directory_iterator it(dir, listEc), end; !listEc && it != end; it.increment(listEc)) {
|
for (const fs::path& libDir : dep.libDirs) {
|
||||||
std::error_code ec;
|
fs::path full = libDir.is_absolute() ? libDir : cmakeBuildDir / libDir;
|
||||||
if (!it->is_regular_file(ec) || !isLibrary(it->path().filename().string())) continue;
|
std::string fullAbs = fs::absolute(full).string();
|
||||||
fs::file_time_type t = it->last_write_time(ec);
|
result.linkFlags.push_back(std::format("-L{}", fullAbs));
|
||||||
if (!ec && t > result.latestArtifact) result.latestArtifact = t;
|
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", fullAbs));
|
||||||
}
|
}
|
||||||
|
for (const std::string& lib : dep.libs) {
|
||||||
|
result.linkFlags.push_back(std::format("-l{}", lib));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The newest library in every directory the link searches: the build
|
||||||
|
// directory and the libDirs, where projects like msquic put theirs,
|
||||||
|
// versioned names (libmsquic.so.2) included.
|
||||||
|
auto isLibrary = [](const std::string& name) {
|
||||||
|
return name.ends_with(".a") || name.ends_with(".so") || name.contains(".so.") || name.ends_with(".dll");
|
||||||
|
};
|
||||||
|
std::vector<fs::path> searched = { cmakeBuildDir };
|
||||||
|
for (const fs::path& libDir : dep.libDirs) searched.push_back(libDir.is_absolute() ? libDir : cmakeBuildDir / libDir);
|
||||||
|
for (const fs::path& dir : searched) {
|
||||||
|
std::error_code listEc;
|
||||||
|
for (fs::directory_iterator it(dir, listEc), end; !listEc && it != end; it.increment(listEc)) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (!it->is_regular_file(ec) || !isLibrary(it->path().filename().string())) continue;
|
||||||
|
fs::file_time_type t = it->last_write_time(ec);
|
||||||
|
if (!ec && t > result.latestArtifact) result.latestArtifact = t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,10 @@ namespace {
|
||||||
|
|
||||||
struct ProjectFileFrame {
|
struct ProjectFileFrame {
|
||||||
fs::path project;
|
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<const std::string_view> args;
|
||||||
std::vector<Configuration*> dependencies;
|
std::vector<Configuration*> dependencies;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -426,7 +430,10 @@ namespace {
|
||||||
// already on the stack is a cycle, which used to recurse forever.
|
// already on the stack is a cycle, which used to recurse forever.
|
||||||
class ProjectFileGuard {
|
class ProjectFileGuard {
|
||||||
public:
|
public:
|
||||||
explicit ProjectFileGuard(const fs::path& absProject) {
|
// The args are recorded with the frame rather than just before
|
||||||
|
// CrafterBuildProject runs, so a spec built while dlopen runs the
|
||||||
|
// project file's static initialisers sees them too.
|
||||||
|
ProjectFileGuard(const fs::path& absProject, std::span<const std::string_view> args) {
|
||||||
for (const ProjectFileFrame& frame : LoadingFrames) {
|
for (const ProjectFileFrame& frame : LoadingFrames) {
|
||||||
if (frame.project != absProject) continue;
|
if (frame.project != absProject) continue;
|
||||||
std::string chain;
|
std::string chain;
|
||||||
|
|
@ -434,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, args, {} });
|
||||||
}
|
}
|
||||||
~ProjectFileGuard() { LoadingFrames.pop_back(); }
|
~ProjectFileGuard() { LoadingFrames.pop_back(); }
|
||||||
ProjectFileGuard(const ProjectFileGuard&) = delete;
|
ProjectFileGuard(const ProjectFileGuard&) = delete;
|
||||||
|
|
@ -451,8 +458,9 @@ namespace {
|
||||||
// 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 forwards no arguments but still reads
|
// the dependency's project.cpp is loaded with no arguments and so passes
|
||||||
// CRAFTER_BUILD_MARCH from the environment for its own deps.
|
// none on, while its own deps still read CRAFTER_BUILD_MARCH from the
|
||||||
|
// 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;
|
||||||
|
|
@ -495,10 +503,16 @@ namespace {
|
||||||
std::set<std::string> libs;
|
std::set<std::string> libs;
|
||||||
|
|
||||||
for (const PragmaDependency& pragma : pragmas) {
|
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;
|
Configuration* cfg = nullptr;
|
||||||
if (IsGitSource(pragma.source)) {
|
if (IsGitSource(pragma.source)) {
|
||||||
GitProjectSpec spec;
|
GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit }, .args = {} };
|
||||||
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 {
|
||||||
|
|
@ -512,7 +526,7 @@ namespace {
|
||||||
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()));
|
if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
|
||||||
cfg = LocalProject({ .projectFile = local });
|
cfg = LocalProject({ .projectFile = local, .args = {} });
|
||||||
}
|
}
|
||||||
if (std::ranges::contains(out.configs, cfg)) continue;
|
if (std::ranges::contains(out.configs, cfg)) continue;
|
||||||
|
|
||||||
|
|
@ -580,6 +594,12 @@ bool Crafter::IsTopLevelProject() {
|
||||||
return LoadingFrames.size() <= 1;
|
return LoadingFrames.size() <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -882,7 +902,7 @@ namespace {
|
||||||
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
|
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
|
||||||
RequireToolchain();
|
RequireToolchain();
|
||||||
fs::path absProject = fs::canonical(projectFile);
|
fs::path absProject = fs::canonical(projectFile);
|
||||||
ProjectFileGuard guard(absProject);
|
ProjectFileGuard guard(absProject, args);
|
||||||
fs::path buildDir = absProject.parent_path() / "build";
|
fs::path buildDir = absProject.parent_path() / "build";
|
||||||
if (!fs::exists(buildDir)) {
|
if (!fs::exists(buildDir)) {
|
||||||
fs::create_directories(buildDir);
|
fs::create_directories(buildDir);
|
||||||
|
|
|
||||||
|
|
@ -656,6 +656,13 @@ export namespace Crafter {
|
||||||
};
|
};
|
||||||
CompileCommand GetCompileCommand(const Configuration& config);
|
CompileCommand GetCompileCommand(const Configuration& config);
|
||||||
|
|
||||||
|
// Builds `config` and the configurations it depends on. depResults is the
|
||||||
|
// record of one build pass, guarded by depMutex and keyed by the directory
|
||||||
|
// each piece of work writes: a dependency's PcmDir, an external
|
||||||
|
// dependency's ExternalBuildDir. Configurations built with the same map,
|
||||||
|
// as RunTests builds its tests, build a shared dependency and fetch a
|
||||||
|
// shared external once between them. A fresh map starts a new pass, which
|
||||||
|
// checks everything again and pulls unpinned externals again.
|
||||||
BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
|
||||||
|
|
||||||
// Takes main's argv verbatim so the CLI entry point is a one-liner;
|
// Takes main's argv verbatim so the CLI entry point is a one-liner;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ export namespace Crafter {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ExternalBuildResult {
|
struct ExternalBuildResult {
|
||||||
std::string error;
|
|
||||||
std::vector<std::string> compileFlags;
|
std::vector<std::string> compileFlags;
|
||||||
std::vector<std::string> linkFlags;
|
std::vector<std::string> linkFlags;
|
||||||
fs::file_time_type latestArtifact = fs::file_time_type::min();
|
fs::file_time_type latestArtifact = fs::file_time_type::min();
|
||||||
|
|
@ -47,23 +46,56 @@ export namespace Crafter {
|
||||||
// through this, so a caller that only needs to know where the headers ended
|
// through this, so a caller that only needs to know where the headers ended
|
||||||
// up cannot drift from where they actually are.
|
// up cannot drift from where they actually are.
|
||||||
fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
|
fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
|
||||||
|
// Where BuildExternal leaves this dependency: the CMake build directory,
|
||||||
|
// or the clone itself when it is only fetched. Pure, like ExternalCloneDir.
|
||||||
|
// Build() keys a pass's record of the work on it rather than on the clone,
|
||||||
|
// so a declaration that shares the clone but builds it with CMake is not
|
||||||
|
// handed the outcome of a fetch alone.
|
||||||
|
fs::path ExternalBuildDir(const ExternalDependency& dep, std::string_view target);
|
||||||
// The -I flags this dependency contributes, from its declared includeDirs.
|
// The -I flags this dependency contributes, from its declared includeDirs.
|
||||||
// Also pure, which is what lets the linter's AST layer parse a source that
|
// Also pure, which is what lets the linter's AST layer parse a source that
|
||||||
// includes an external's headers without running a build to find out where
|
// includes an external's headers without running a build to find out where
|
||||||
// they are. The paths only resolve once something has fetched the clone.
|
// they are. The paths only resolve once something has fetched the clone.
|
||||||
std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
|
std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
|
||||||
ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled);
|
// Fetches the clone and, for ExternalBuilder::CMake, configures and builds
|
||||||
|
// it. Empty on success, otherwise what failed. A lock on the clone is held
|
||||||
|
// throughout, so another crafter-build, or another build pass in this one,
|
||||||
|
// works on it before or after this call and never during it. Not
|
||||||
|
// cancellable once started: Build() hands the outcome to every
|
||||||
|
// configuration of the pass that declares the dependency, and a run cut
|
||||||
|
// short would leave them a clone nobody fetched.
|
||||||
|
std::string BuildExternal(const ExternalDependency& dep, std::string_view target);
|
||||||
|
// What a configuration that declares `dep` compiles and links with once
|
||||||
|
// BuildExternal has run: the -I flags and, for CMake, the -L, -rpath and -l
|
||||||
|
// flags and the newest library. Per declaration, because two that share a
|
||||||
|
// clone can still differ in includeDirs, libDirs and libs.
|
||||||
|
ExternalBuildResult ExternalOutputs(const ExternalDependency& dep, std::string_view target);
|
||||||
|
|
||||||
|
// 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<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
|
||||||
// forwarded verbatim to the dep's CrafterBuildProject — typically you
|
// what the dep's CrafterBuildProject receives. They default to this
|
||||||
// pass through the parent's args so target/march/debug propagate.
|
// project's own, so --target=, --march=, --debug and flags of its own
|
||||||
|
// such as --local reach a whole chain of sibling projects without being
|
||||||
|
// passed. Set them to forward something else: a copy without one of this
|
||||||
|
// project's flags, one with an extra --target=, or {} for nothing.
|
||||||
struct GitProjectSpec {
|
struct GitProjectSpec {
|
||||||
GitSource source;
|
GitSource source;
|
||||||
fs::path projectFile = "project.cpp";
|
fs::path projectFile = "project.cpp";
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args = ProjectArgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clones the spec's git URL into the per-project external cache (keyed by
|
// Clones the spec's git URL into the per-project external cache (keyed by
|
||||||
|
|
@ -77,8 +109,8 @@ export namespace Crafter {
|
||||||
// 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; // forwarded to dep's CrafterBuildProject
|
std::vector<std::string> args = ProjectArgs(); // as GitProjectSpec::args
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
||||||
26
project.cpp
26
project.cpp
|
|
@ -137,15 +137,8 @@ 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) {
|
||||||
std::vector<std::string> depArgs(args.begin(), args.end());
|
Configuration* math = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" } });
|
||||||
Configuration* math = GitProject({
|
Configuration* asset = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git" } });
|
||||||
.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 = "./";
|
||||||
|
|
@ -262,6 +255,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("ConcurrentExternalFetch").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ConcurrentExternalFetch").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
cfg.AddTest("SharedExternalFetch").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
|
@ -286,16 +280,22 @@ 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("Script").Dependencies({ CrafterBuildLib.get() })
|
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
|
||||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||||
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dogfood: the house-style rules, imported from their own repo through
|
// Dogfood: the lite house rules, imported from their own repo through
|
||||||
// the pragma above. Report rules gate `crafter-build lint`; transform
|
// the pragma above. Lite is the house rules without the ones that only
|
||||||
// rules also auto-fix under `crafter-build format`.
|
// decide layout, whitespace and naming, which this tree keeps by hand:
|
||||||
LintRules::AddRules(cfg);
|
// 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);
|
||||||
|
|
||||||
// See the top of this file.
|
// See the top of this file.
|
||||||
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,19 @@ namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
// Builds that run at the same time share their dependencies' clones in the
|
// Builds that run at the same time share their dependencies' clones in the
|
||||||
// cache: the tests `crafter-build test --jobs=N` builds side by side each
|
// cache, and two crafter-builds can run at once. Fetching a clone that
|
||||||
// declare the same ExternalDependency, and two crafter-builds can run at
|
// already exists ran `git pull` (and the CMake build) in it with nothing to
|
||||||
// once. Fetching a clone that already exists ran `git pull` (and the CMake
|
// keep a second fetch out, and the second git fails on the first one's
|
||||||
// build) in it with nothing to keep a second fetch out, and the second git
|
// index.lock. A clone's fetch and build now hold a lock of their own. Within
|
||||||
// fails on the first one's index.lock. A clone's fetch and build now hold a
|
// one run, the configurations that declare the same dependency share one
|
||||||
// lock of their own.
|
// fetch of it instead (SharedExternalFetch).
|
||||||
//
|
//
|
||||||
// Eight threads fetch the same tracked branch of a local repository at
|
// Eight threads fetch the same tracked branch of a local repository at
|
||||||
// once, a few rounds over, the branch moving between rounds so every pull
|
// once, a few rounds over, the branch moving between rounds so every pull
|
||||||
// has work to do; every fetch has to succeed. Everything is local;
|
// has work to do; every fetch has to succeed. Each thread fetches through
|
||||||
// XDG_CACHE_HOME keeps the clone out of the real cache.
|
// GitFetch and through BuildExternal, which every build of an
|
||||||
|
// ExternalDependency calls, each into a clone of its own. Everything is
|
||||||
|
// local; XDG_CACHE_HOME keeps the clones out of the real cache.
|
||||||
|
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||||
|
|
||||||
|
|
@ -42,6 +44,13 @@ namespace {
|
||||||
Git(repo, "add round.txt");
|
Git(repo, "add round.txt");
|
||||||
Git(repo, std::format("commit -q -m round-{}", round));
|
Git(repo, std::format("commit -q -m round-{}", round));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::uint32_t Round(const fs::path& clone) {
|
||||||
|
std::ifstream in(clone / "round.txt");
|
||||||
|
std::uint32_t round = 0;
|
||||||
|
in >> round;
|
||||||
|
return round;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
|
|
@ -60,6 +69,12 @@ int main() {
|
||||||
Commit(repo, 0);
|
Commit(repo, 0);
|
||||||
const GitSource source { .url = std::format("file://{}", repo.string()), .branch = "main" };
|
const GitSource source { .url = std::format("file://{}", repo.string()), .branch = "main" };
|
||||||
const fs::path clone = GitFetch(source);
|
const fs::path clone = GitFetch(source);
|
||||||
|
// A clone of its own: BuildExternal keys the cache differently.
|
||||||
|
ExternalDependency dep;
|
||||||
|
dep.name = "shared";
|
||||||
|
dep.source = source;
|
||||||
|
const std::string cloned = BuildExternal(dep, HostTarget());
|
||||||
|
Check(cloned.empty(), std::format("BuildExternal clones the repository: {}", cloned));
|
||||||
|
|
||||||
constexpr std::uint32_t threads = 8;
|
constexpr std::uint32_t threads = 8;
|
||||||
constexpr std::uint32_t rounds = 4;
|
constexpr std::uint32_t rounds = 4;
|
||||||
|
|
@ -70,20 +85,23 @@ int main() {
|
||||||
std::vector<std::jthread> fetchers;
|
std::vector<std::jthread> fetchers;
|
||||||
for (std::uint32_t i = 0; i < threads; ++i) {
|
for (std::uint32_t i = 0; i < threads; ++i) {
|
||||||
fetchers.emplace_back([&] {
|
fetchers.emplace_back([&] {
|
||||||
|
std::string error;
|
||||||
try {
|
try {
|
||||||
(void)GitFetch(source);
|
(void)GitFetch(source);
|
||||||
|
error = BuildExternal(dep, HostTarget());
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
std::lock_guard lock(errorsMutex);
|
error = e.what();
|
||||||
errors.push_back(e.what());
|
|
||||||
}
|
}
|
||||||
|
if (error.empty()) return;
|
||||||
|
std::lock_guard lock(errorsMutex);
|
||||||
|
errors.push_back(std::move(error));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Check(errors.empty(), std::format("{} fetches at once, {} rounds: every one succeeds ({} failed{}{})", threads, rounds, errors.size(), errors.empty() ? "" : ", the first: ", errors.empty() ? "" : errors.front()));
|
Check(errors.empty(), std::format("{} threads fetching at once, {} rounds: every fetch succeeds ({} failed{}{})", threads, rounds, errors.size(), errors.empty() ? "" : ", the first: ", errors.empty() ? "" : errors.front()));
|
||||||
std::ifstream round(clone / "round.txt");
|
Check(Round(clone) == rounds, std::format("and GitFetch's clone ends at the last round ({})", Round(clone)));
|
||||||
std::uint32_t last = 0;
|
const fs::path externalClone = ExternalCloneDir(dep, HostTarget());
|
||||||
round >> last;
|
Check(Round(externalClone) == rounds, std::format("and so does BuildExternal's ({})", Round(externalClone)));
|
||||||
Check(last == rounds, std::format("and the clone ends at the last round ({})", last));
|
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::remove_all(scratch, ec);
|
fs::remove_all(scratch, ec);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
// A CMake dependency's newest library decides when its consumers are stale
|
// A CMake dependency's newest library decides when its consumers are stale
|
||||||
// (BuildExternal's latestArtifact). Only files directly in the CMake build
|
// (the latestArtifact of ExternalOutputs). Only files directly in the CMake
|
||||||
// directory named *.a or *.so were looked at, so msquic's
|
// build directory named *.a or *.so were looked at, so msquic's
|
||||||
// bin/Release/libmsquic.so.2, in one of the dependency's libDirs and with a
|
// bin/Release/libmsquic.so.2, in one of the dependency's libDirs and with a
|
||||||
// version after the .so, never counted: a rebuilt msquic left everything
|
// version after the .so, never counted: a rebuilt msquic left everything
|
||||||
// built against the old one in place.
|
// built against the old one in place.
|
||||||
|
|
@ -69,9 +69,9 @@ int main() {
|
||||||
dep.builder = ExternalBuilder::CMake;
|
dep.builder = ExternalBuilder::CMake;
|
||||||
dep.libDirs = { "bin/Release" };
|
dep.libDirs = { "bin/Release" };
|
||||||
dep.libs = { "versioned" };
|
dep.libs = { "versioned" };
|
||||||
std::atomic<bool> cancelled = false;
|
const std::string error = BuildExternal(dep, HostTarget());
|
||||||
const ExternalBuildResult result = BuildExternal(dep, HostTarget(), cancelled);
|
Check(error.empty(), std::format("the dependency builds: {}", error));
|
||||||
Check(result.error.empty(), std::format("the dependency builds: {}", result.error));
|
const ExternalBuildResult result = ExternalOutputs(dep, HostTarget());
|
||||||
|
|
||||||
const fs::path library = ExternalCloneDir(dep, HostTarget()) / "build" / "bin" / "Release" / "libversioned.so.2.0.0";
|
const fs::path library = ExternalCloneDir(dep, HostTarget()) / "build" / "bin" / "Release" / "libversioned.so.2.0.0";
|
||||||
Check(fs::exists(library), std::format("the library is where its CMake project puts it: {}", library.string()));
|
Check(fs::exists(library), std::format("the library is where its CMake project puts it: {}", library.string()));
|
||||||
|
|
|
||||||
29
tests/ProjectArgs/fixture/app/project.cpp
Normal file
29
tests/ProjectArgs/fixture/app/project.cpp
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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<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 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<std::string>(args.begin(), args.end());
|
||||||
|
cfg.compileFlags.push_back(gitDefault ? "-DGIT_DEFAULT=1" : "-DGIT_DEFAULT=0");
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
17
tests/ProjectArgs/fixture/leaf/project.cpp
Normal file
17
tests/ProjectArgs/fixture/leaf/project.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
21
tests/ProjectArgs/fixture/lib/project.cpp
Normal file
21
tests/ProjectArgs/fixture/lib/project.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
91
tests/ProjectArgs/main.cpp
Normal file
91
tests/ProjectArgs/main.cpp
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// 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.
|
||||||
|
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<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() == 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;
|
||||||
|
}
|
||||||
|
|
@ -115,6 +115,21 @@ int main(int argc, char** argv) {
|
||||||
std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what());
|
std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what());
|
||||||
return 1;
|
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<std::string_view, 1> 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");
|
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);
|
||||||
|
|
|
||||||
137
tests/SharedExternalFetch/main.cpp
Normal file
137
tests/SharedExternalFetch/main.cpp
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
// 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 project whose tests each copy its externalDependencies gives every test
|
||||||
|
// configuration the same unpinned dependency, and `crafter-build test`
|
||||||
|
// builds those configurations side by side. Each of them pulled the shared
|
||||||
|
// clone: at first all at once, which broke the clone's FETCH_HEAD and failed
|
||||||
|
// every test (issue #34), and then, under the clone's lock, one after
|
||||||
|
// another, a network round trip per test. The configurations of one build
|
||||||
|
// pass now share a single fetch and build of the dependency, and the next
|
||||||
|
// pass fetches it again.
|
||||||
|
//
|
||||||
|
// Eight tests that include a header from the same tracked branch of a local
|
||||||
|
// repository run through RunTests at once, and again after the branch has
|
||||||
|
// moved. GIT_TRACE logs every git process, and every fetch of the
|
||||||
|
// repository, clone or pull, runs `git upload-pack` in it once. Everything
|
||||||
|
// is local; XDG_CACHE_HOME keeps the clone out of the real cache.
|
||||||
|
|
||||||
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::int32_t failures = 0;
|
||||||
|
|
||||||
|
void Check(bool cond, std::string_view msg) {
|
||||||
|
if (!cond) {
|
||||||
|
std::println(std::cerr, "FAIL: {}", msg);
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Git(const fs::path& dir, std::string_view args) {
|
||||||
|
const std::string command = std::format("git -C {} {}", ShellQuote(dir.string()), args);
|
||||||
|
auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
||||||
|
if (result.exitCode != 0) throw std::runtime_error(std::format("{} failed: {}", command, result.output));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Write(const fs::path& file, std::string_view text) {
|
||||||
|
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Commit(const fs::path& repo, std::uint32_t round) {
|
||||||
|
Write(repo / "round.h", std::format("#define ROUND {}\n", round));
|
||||||
|
Git(repo, "add round.h");
|
||||||
|
Git(repo, std::format("commit -q -m round-{}", round));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t Fetches(const fs::path& trace) {
|
||||||
|
std::ifstream in(trace);
|
||||||
|
std::uint32_t count = 0;
|
||||||
|
for (std::string line; std::getline(in, line);) {
|
||||||
|
if (line.contains("trace: built-in: git upload-pack")) ++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first test that did not pass, for the message.
|
||||||
|
std::string FirstFailure(const TestSummary& summary) {
|
||||||
|
for (const TestResult& result : summary.results) {
|
||||||
|
if (result.outcome != TestOutcome::Pass) return std::format(", the first: {}: {}", result.name, result.output);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const fs::path scratch = fs::temp_directory_path() / "crafter-build-shared-external-fetch";
|
||||||
|
fs::remove_all(scratch);
|
||||||
|
fs::create_directories(scratch);
|
||||||
|
setenv("XDG_CACHE_HOME", (scratch / "cache").c_str(), 1);
|
||||||
|
setenv("GIT_CONFIG_NOSYSTEM", "1", 1);
|
||||||
|
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
||||||
|
for (const char* name : { "GIT_AUTHOR_NAME", "GIT_COMMITTER_NAME" }) setenv(name, "crafter-build test", 1);
|
||||||
|
for (const char* name : { "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_EMAIL" }) setenv(name, "test@example.invalid", 1);
|
||||||
|
const fs::path trace = scratch / "git-trace.log";
|
||||||
|
setenv("GIT_TRACE", trace.c_str(), 1);
|
||||||
|
|
||||||
|
const fs::path repo = scratch / "shared";
|
||||||
|
fs::create_directories(repo);
|
||||||
|
Git(repo, "init -q -b main");
|
||||||
|
Commit(repo, 1);
|
||||||
|
|
||||||
|
ExternalDependency shared;
|
||||||
|
shared.name = "shared";
|
||||||
|
shared.source = { .url = std::format("file://{}", repo.string()), .branch = "main" };
|
||||||
|
shared.includeDirs = { "" };
|
||||||
|
|
||||||
|
const fs::path project = scratch / "project";
|
||||||
|
fs::create_directories(project);
|
||||||
|
Write(project / "main.cpp", "#include \"round.h\"\n\nint main() { return ROUND > 0 ? 0 : 1; }\n");
|
||||||
|
|
||||||
|
constexpr std::int32_t tests = 8;
|
||||||
|
Configuration cfg;
|
||||||
|
cfg.path = project;
|
||||||
|
cfg.name = "project";
|
||||||
|
cfg.target = HostTarget();
|
||||||
|
for (std::int32_t i = 0; i < tests; ++i) {
|
||||||
|
Test& test = cfg.tests.emplace_back();
|
||||||
|
test.config.path = project;
|
||||||
|
test.config.name = std::format("t{}", i);
|
||||||
|
test.config.outputName = test.config.name;
|
||||||
|
test.config.target = HostTarget();
|
||||||
|
test.config.type = ConfigurationType::Executable;
|
||||||
|
test.config.externalDependencies = { shared };
|
||||||
|
std::array<fs::path, 0> interfaces = {};
|
||||||
|
std::array<fs::path, 1> implementations = { "main" };
|
||||||
|
test.config.GetInterfacesAndImplementations(interfaces, implementations);
|
||||||
|
}
|
||||||
|
const RunTestsOptions options { .jobs = tests };
|
||||||
|
|
||||||
|
TestSummary summary = RunTests(cfg, options);
|
||||||
|
Check(summary.passed == tests, std::format("{} tests that share the dependency build and pass at once ({} passed{})", tests, summary.passed, FirstFailure(summary)));
|
||||||
|
const std::uint32_t first = Fetches(trace);
|
||||||
|
Check(first == 1, std::format("and fetch it once between them, not once each ({} fetches)", first));
|
||||||
|
|
||||||
|
Commit(repo, 2);
|
||||||
|
summary = RunTests(cfg, options);
|
||||||
|
Check(summary.passed == tests, std::format("after the branch moved, they pass again ({} passed{})", summary.passed, FirstFailure(summary)));
|
||||||
|
const std::uint32_t second = Fetches(trace) - first;
|
||||||
|
Check(second == 1, std::format("and the next run fetches it once more ({} fetches)", second));
|
||||||
|
std::ifstream header(ExternalCloneDir(shared, HostTarget()) / "round.h");
|
||||||
|
std::string round;
|
||||||
|
std::getline(header, round);
|
||||||
|
Check(round == "#define ROUND 2", std::format("which brings the clone to the new commit ({})", round));
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove_all(scratch, ec);
|
||||||
|
if (failures > 0) {
|
||||||
|
std::println(std::cerr, "{} assertions failed", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue