Compare commits
21 changed files with 94 additions and 418 deletions
|
|
@ -45,7 +45,7 @@ Prerequisites on Linux: `clang` (with `libclang`, which the AST lint rules load
|
|||
Three things to know about this loop:
|
||||
|
||||
- `bin/crafter-build` is the bootstrap binary and the harness for every command below. The self-host build writes to `bin/crafter.build-exe-<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.
|
||||
|
||||
`./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`.
|
||||
|
|
|
|||
|
|
@ -74,9 +74,7 @@ Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project.
|
|||
cfg.dependencies = { math };
|
||||
```
|
||||
|
||||
The sibling's `CrafterBuildProject` gets the args this `project.cpp` was loaded with (`ProjectArgs()`), so `--target=`, `--march=`, `--debug` and the project's own flags reach every sibling down the chain. That is all of them: `cfg.projectArgs` holds only what `ApplyStandardArgs` did not interpret, and passing it on would drop the target and the build type. Set `.args` to send something else: `{}` for nothing, or a list built from the project's args, such as one without a flag only this project reads. Build such a list once and give it to every sibling. A project that two paths load with different lists is built twice, and the program ends up with both builds. Releases up to 2.2.0 forward nothing when `.args` is left out.
|
||||
|
||||
The default is read when the spec is made, so make it inside `CrafterBuildProject`, on the thread that calls it. A spec made at namespace scope, on a thread of your own or in a script forwards nothing.
|
||||
The sibling's `CrafterBuildProject` gets the args this `project.cpp` was loaded with (`ProjectArgs()`), so `--target=`, `--march=`, `--debug` and the project's own flags reach every sibling down the chain. Set `.args` to send something else: `{}` for nothing, or a list built from the project's args, say one without a flag only this project reads (it would otherwise give the sibling a build directory of its own) or with an extra `--target=`. Releases up to 2.2.0 forward nothing when `.args` is left out.
|
||||
|
||||
Clones share the external cache and identical specs share one `Configuration`. An unpinned source is `git pull`ed on every invocation, set `.commit` for reproducible, offline-after-first-fetch builds.
|
||||
|
||||
|
|
|
|||
|
|
@ -1651,10 +1651,6 @@ Build options:
|
|||
-v, --verbose Verbose progress output.
|
||||
-q, --quiet Suppress progress output.
|
||||
--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):
|
||||
--list Enumerate matching tests without running them.
|
||||
|
|
@ -1766,9 +1762,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
RunLintOptions lintOpts;
|
||||
RunScriptOptions scriptOpts;
|
||||
Progress::Verbosity verbosity = Progress::Verbosity::Default;
|
||||
// Set by `--`: every later word is an operand of the verb, never an
|
||||
// option or a verb of this tool.
|
||||
bool operandsOnly = false;
|
||||
|
||||
for (std::int32_t i = 1; i < argc; ++i) {
|
||||
std::string_view arg = argv[i];
|
||||
|
|
@ -1780,25 +1773,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
// variant hash and so the directory the project builds into.
|
||||
if (scriptOpts.named) {
|
||||
scriptOpts.args.emplace_back(arg);
|
||||
} else if (runScript && (operandsOnly || !arg.starts_with("-"))) {
|
||||
} else if (runScript && !arg.starts_with("-")) {
|
||||
// The first word after `run`, even one that is a verb
|
||||
// elsewhere. Options before it keep their usual meaning.
|
||||
scriptOpts.name = arg;
|
||||
scriptOpts.named = true;
|
||||
} else if (operandsOnly) {
|
||||
if (runTests) {
|
||||
testOpts.globs.emplace_back(arg);
|
||||
} else if (runLint || runFormat) {
|
||||
lintOpts.globs.emplace_back(arg);
|
||||
} else {
|
||||
projectArgs.push_back(arg);
|
||||
}
|
||||
} else if (arg == "--") {
|
||||
// Ends this tool's options and is never forwarded itself. It
|
||||
// used to land in projectArgs as a word of its own, giving the
|
||||
// project a variant hash of its own, and every sibling project
|
||||
// that inherits the args one as well.
|
||||
operandsOnly = true;
|
||||
} else if (arg == "--version") {
|
||||
// Answered here, before the project file is looked for, so
|
||||
// it works in any directory and is never forwarded to
|
||||
|
|
|
|||
|
|
@ -360,12 +360,11 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
|||
// source revision (forwarded args don't change what gets cloned, just
|
||||
// how the dep's project.cpp interprets them at runtime), while the
|
||||
// in-memory Configuration is keyed by the full spec so each (args)
|
||||
// permutation gets its own Configuration object, and host tooling one
|
||||
// apart from the target's even for the same args.
|
||||
// permutation gets its own Configuration object.
|
||||
std::string srcKey = std::format("git|{}|{}|{}|{}", spec.source.url, spec.source.branch, spec.source.commit, spec.projectFile.string());
|
||||
std::string srcHash = std::format("{:016x}", std::hash<std::string>{}(srcKey));
|
||||
|
||||
std::string fullKey = std::format("{}{}", LoadingHostTooling() ? "host|" : "", srcKey);
|
||||
std::string fullKey = srcKey;
|
||||
for (const std::string& a : spec.args) {
|
||||
fullKey += '|';
|
||||
fullKey += a;
|
||||
|
|
@ -450,7 +449,7 @@ Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) {
|
|||
}
|
||||
fs::path canonical = fs::canonical(projectPath);
|
||||
|
||||
std::string fullKey = std::format("{}local|{}", LoadingHostTooling() ? "host|" : "", canonical.string());
|
||||
std::string fullKey = std::format("local|{}", canonical.string());
|
||||
for (const std::string& a : spec.args) {
|
||||
fullKey += '|';
|
||||
fullKey += a;
|
||||
|
|
|
|||
|
|
@ -413,10 +413,9 @@ namespace {
|
|||
|
||||
struct ProjectFileFrame {
|
||||
fs::path project;
|
||||
// What LoadProject was given, for ProjectArgs(), and only once
|
||||
// CrafterBuildProject is about to be called (see LoadProject). A view
|
||||
// is enough: the caller's storage outlives LoadProject, and the frame
|
||||
// is popped before LoadProject returns.
|
||||
// What LoadProject was given, for ProjectArgs(). A view is enough:
|
||||
// the caller's storage outlives LoadProject, and the frame is popped
|
||||
// before LoadProject returns.
|
||||
std::span<const std::string_view> args;
|
||||
std::vector<Configuration*> dependencies;
|
||||
};
|
||||
|
|
@ -431,7 +430,10 @@ namespace {
|
|||
// already on the stack is a cycle, which used to recurse forever.
|
||||
class ProjectFileGuard {
|
||||
public:
|
||||
explicit ProjectFileGuard(const fs::path& absProject) {
|
||||
// The args are recorded with the frame rather than just before
|
||||
// CrafterBuildProject runs, so a spec built while dlopen runs the
|
||||
// project file's static initialisers sees them too.
|
||||
ProjectFileGuard(const fs::path& absProject, std::span<const std::string_view> args) {
|
||||
for (const ProjectFileFrame& frame : LoadingFrames) {
|
||||
if (frame.project != absProject) continue;
|
||||
std::string chain;
|
||||
|
|
@ -439,7 +441,7 @@ namespace {
|
|||
throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string()));
|
||||
}
|
||||
index_ = LoadingFrames.size();
|
||||
LoadingFrames.push_back({ absProject, {}, {} });
|
||||
LoadingFrames.push_back({ absProject, args, {} });
|
||||
}
|
||||
~ProjectFileGuard() { LoadingFrames.pop_back(); }
|
||||
ProjectFileGuard(const ProjectFileGuard&) = delete;
|
||||
|
|
@ -451,21 +453,6 @@ namespace {
|
|||
std::size_t index_ = 0;
|
||||
};
|
||||
|
||||
// How many `#pragma crafter dependency` loads this thread is inside. The
|
||||
// tool's project file runs within its load, and so does every sibling it
|
||||
// loads in turn, which is what LoadingHostTooling() reports to GitProject
|
||||
// and LocalProject. Per thread like LoadingFrames: a sibling loaded on a
|
||||
// thread of the tool's own goes uncounted, as it gets no ProjectArgs().
|
||||
thread_local std::int32_t HostToolingLoads = 0;
|
||||
|
||||
class HostToolingScope {
|
||||
public:
|
||||
HostToolingScope() { ++HostToolingLoads; }
|
||||
~HostToolingScope() { --HostToolingLoads; }
|
||||
HostToolingScope(const HostToolingScope&) = delete;
|
||||
HostToolingScope& operator=(const HostToolingScope&) = delete;
|
||||
};
|
||||
|
||||
// A pre-dependency is host tooling: project.so is loaded into the running
|
||||
// crafter-build, so the dependency is built for the host and against the
|
||||
// same PCM cache LoadProject populates — the hostConfig LoadProject itself
|
||||
|
|
@ -516,36 +503,30 @@ namespace {
|
|||
std::set<std::string> libs;
|
||||
|
||||
for (const PragmaDependency& pragma : pragmas) {
|
||||
// Neither spec sets args, and the tool still gets none of the
|
||||
// consumer's: this pre-pass runs before LoadProject hands the
|
||||
// frame its args, so ProjectArgs() is empty. Given the consumer's
|
||||
// --target=, the tool's project.cpp would pick sources and flags
|
||||
// for the wrong triple, and its flags would split the tool's
|
||||
// variant from the one every other consumer shares. The scope
|
||||
// keeps the tool and everything it loads in a project cache of
|
||||
// their own: ForceHostTooling rewrites them in place, and a
|
||||
// consumer loading the same sibling for itself got that host
|
||||
// build back whenever the two cache keys matched.
|
||||
// Host tooling is loaded with none of the consumer's args, so both
|
||||
// specs pass `.args = {}` instead of defaulting to them. The tool
|
||||
// is built for the host whatever the consumer targets: given the
|
||||
// consumer's --target=, its project.cpp would pick sources and
|
||||
// flags for the wrong triple before ForceHostTooling resets the
|
||||
// fields, and the consumer's own flags would hash it apart from
|
||||
// the build every other consumer of the tool shares.
|
||||
Configuration* cfg = nullptr;
|
||||
{
|
||||
HostToolingScope hostTooling;
|
||||
if (IsGitSource(pragma.source)) {
|
||||
GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit } };
|
||||
if (!pragma.project.empty()) spec.projectFile = pragma.project;
|
||||
cfg = GitProject(spec);
|
||||
} else {
|
||||
if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source");
|
||||
// Relative to the project file, not the cwd: LoadProject runs
|
||||
// from wherever the consumer was invoked.
|
||||
fs::path local = (absProject.parent_path() / pragma.source).lexically_normal();
|
||||
if (fs::is_directory(local)) {
|
||||
local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project);
|
||||
} else if (!pragma.project.empty()) {
|
||||
PragmaError(absProject, pragma.line, "'project' only applies to a directory source");
|
||||
}
|
||||
if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
|
||||
cfg = LocalProject({ .projectFile = local });
|
||||
if (IsGitSource(pragma.source)) {
|
||||
GitProjectSpec spec{ .source = { pragma.source, pragma.branch, pragma.commit }, .args = {} };
|
||||
if (!pragma.project.empty()) spec.projectFile = pragma.project;
|
||||
cfg = GitProject(spec);
|
||||
} else {
|
||||
if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source");
|
||||
// Relative to the project file, not the cwd: LoadProject runs
|
||||
// from wherever the consumer was invoked.
|
||||
fs::path local = (absProject.parent_path() / pragma.source).lexically_normal();
|
||||
if (fs::is_directory(local)) {
|
||||
local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project);
|
||||
} else if (!pragma.project.empty()) {
|
||||
PragmaError(absProject, pragma.line, "'project' only applies to a directory source");
|
||||
}
|
||||
if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
|
||||
cfg = LocalProject({ .projectFile = local, .args = {} });
|
||||
}
|
||||
if (std::ranges::contains(out.configs, cfg)) continue;
|
||||
|
||||
|
|
@ -619,10 +600,6 @@ std::vector<std::string> Crafter::ProjectArgs() {
|
|||
return std::vector<std::string>(args.begin(), args.end());
|
||||
}
|
||||
|
||||
bool Crafter::LoadingHostTooling() {
|
||||
return HostToolingLoads > 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The one reading of a wait status (pclose, system) for every helper
|
||||
// that runs a command: a signal death is reported the way a shell
|
||||
|
|
@ -925,7 +902,7 @@ namespace {
|
|||
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
|
||||
RequireToolchain();
|
||||
fs::path absProject = fs::canonical(projectFile);
|
||||
ProjectFileGuard guard(absProject);
|
||||
ProjectFileGuard guard(absProject, args);
|
||||
fs::path buildDir = absProject.parent_path() / "build";
|
||||
if (!fs::exists(buildDir)) {
|
||||
fs::create_directories(buildDir);
|
||||
|
|
@ -997,9 +974,6 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
WriteStamp(StampPath(soPath), projectStamp);
|
||||
}
|
||||
|
||||
// Before dlopen, so the file's static initialisers can already call
|
||||
// ProjectDependency(): its dependencies are the same on every load.
|
||||
guard.Frame().dependencies = std::move(preDeps.configs);
|
||||
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
|
||||
if (!handle) {
|
||||
throw std::runtime_error(std::format("Failed to load project {}: {}", soPath.string(), dlerror()));
|
||||
|
|
@ -1012,12 +986,6 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
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
|
||||
// later load gets the same mapping back from dlopen, so a spec built at
|
||||
// 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
|
||||
// also ran on this frame and relies on the args not being there yet.
|
||||
guard.Frame().args = args;
|
||||
guard.Frame().dependencies = std::move(preDeps.configs);
|
||||
return fn(args);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,9 +483,7 @@ export namespace Crafter {
|
|||
// cfg.files, say), and without it both flag settings share one bin dir
|
||||
// and interleave their outputs there. Populated by ApplyStandardArgs;
|
||||
// the args it does recognise are excluded, since their effect already
|
||||
// shows up in target/march/mtune/debug/sysroot/type. For the same
|
||||
// reason this is not what to hand a sibling project: ProjectArgs() is
|
||||
// every arg, the recognised ones included.
|
||||
// shows up in target/march/mtune/debug/sysroot/type.
|
||||
std::vector<std::string> projectArgs;
|
||||
std::vector<Test> tests;
|
||||
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
||||
|
|
|
|||
|
|
@ -71,21 +71,16 @@ export namespace Crafter {
|
|||
// clone can still differ in includeDirs, libDirs and libs.
|
||||
ExternalBuildResult ExternalOutputs(const ExternalDependency& dep, std::string_view target);
|
||||
|
||||
// The args the project file now running its CrafterBuildProject was
|
||||
// loaded with: everything that function receives, --target=, --debug and
|
||||
// the other standard flags included. Not cfg.projectArgs, which holds
|
||||
// only what ApplyStandardArgs left uninterpreted: `.args =
|
||||
// cfg.projectArgs` would drop the target and the build type. Empty
|
||||
// everywhere else, because it reads the stack LoadProject keeps per
|
||||
// thread, as IsTopLevelProject does: outside a project file, on any
|
||||
// thread but the one running it, once CrafterBuildProject has returned
|
||||
// (a script runs later), and before it is called, which covers the
|
||||
// file's static initialisers and its `#pragma crafter dependency`
|
||||
// pre-pass. The specs below default their args to it. Defined beside
|
||||
// LoadProject, but declared here so those defaults can see it: importing
|
||||
// :Platform into this partition would make the exported `struct
|
||||
// Configuration;` above a redeclaration of :Platform's unexported one,
|
||||
// which clang rejects.
|
||||
// The args LoadProject was given for the project file currently running
|
||||
// its CrafterBuildProject: what that function receives, standard flags
|
||||
// included (cfg.projectArgs is only the part ApplyStandardArgs left
|
||||
// over). Empty when no project file is running, and on any thread but
|
||||
// the one loading it, because it reads the stack LoadProject keeps per
|
||||
// thread, as IsTopLevelProject does. The specs below default their args
|
||||
// to it. Defined beside LoadProject, but declared here so those defaults
|
||||
// can see it: importing :Platform into this partition would make the
|
||||
// exported `struct Configuration;` above a redeclaration of :Platform's
|
||||
// unexported one, which clang rejects.
|
||||
std::vector<std::string> ProjectArgs();
|
||||
|
||||
// Specification for a sibling crafter-build project to fetch and depend on.
|
||||
|
|
@ -93,26 +88,21 @@ export namespace Crafter {
|
|||
// remote's default branch HEAD (sloppy but convenient), set branch to
|
||||
// track a branch tip, or set commit to pin to an immutable SHA. args are
|
||||
// what the dep's CrafterBuildProject receives. They default to this
|
||||
// project's own, read when the spec is made, so --target=, --march=,
|
||||
// --debug and the project's own flags reach every sibling down a chain
|
||||
// without being passed. Set them to forward something else: {} for
|
||||
// nothing, or a list built from this project's args, such as one without
|
||||
// a flag only this project reads. Give such a list to every sibling: a
|
||||
// project that two paths load with two different lists is built twice,
|
||||
// and the program ends up with both builds.
|
||||
// project's own, so --target=, --march=, --debug and flags of its own
|
||||
// such as --local reach a whole chain of sibling projects without being
|
||||
// passed. Set them to forward something else: a copy without one of this
|
||||
// project's flags, one with an extra --target=, or {} for nothing.
|
||||
struct GitProjectSpec {
|
||||
GitSource source;
|
||||
fs::path projectFile = "project.cpp";
|
||||
std::vector<std::string> args = ProjectArgs();
|
||||
};
|
||||
|
||||
// Clones the spec's git URL into the external cache, recursively
|
||||
// LoadProject's the remote's project.cpp, and returns a stable
|
||||
// Configuration* you can drop into cfg.dependencies. The clone is keyed by
|
||||
// url+branch+commit+projectFile alone, so every arg list builds in the
|
||||
// same working tree, each into variant directories of its own. The
|
||||
// Configuration is owned by an internal cache for the lifetime of the
|
||||
// build, keyed by the clone and the args: identical specs share one.
|
||||
// Clones the spec's git URL into the per-project external cache (keyed by
|
||||
// url+branch+commit+args+projectFile), recursively LoadProject's the
|
||||
// remote's project.cpp, and returns a stable Configuration* you can drop
|
||||
// into cfg.dependencies. The Configuration is owned by an internal cache
|
||||
// for the lifetime of the build; identical specs share one Configuration.
|
||||
Configuration* GitProject(const GitProjectSpec& spec);
|
||||
|
||||
// Specification for a local sibling crafter-build project (e.g. an
|
||||
|
|
|
|||
|
|
@ -75,12 +75,6 @@ namespace Crafter {
|
|||
// that library as a dependency, the test's GitProject loads crafter-build's
|
||||
// project file in turn and the two recurse until the cycle guard throws.
|
||||
export bool IsTopLevelProject();
|
||||
// True while this thread loads a `#pragma crafter dependency`: the tool's
|
||||
// project file and every sibling it loads in turn. GitProject and
|
||||
// LocalProject key the project cache on it, so host tooling, which
|
||||
// ForceHostTooling rewrites in place, never shares a Configuration with a
|
||||
// load for the target.
|
||||
bool LoadingHostTooling();
|
||||
// Resolves the directory holding distributed runtime assets (Crafter.Build
|
||||
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
||||
// derives <prefix>/share/crafter-build from the running executable's path.
|
||||
|
|
|
|||
|
|
@ -282,8 +282,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||
cfg.AddTest("ProjectArgs").Dependencies({ CrafterBuildLib.get() })
|
||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||
cfg.AddTest("HostToolingSibling").Dependencies({ CrafterBuildLib.get() })
|
||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
|
||||
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
||||
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -6,14 +6,12 @@ import Crafter.Build;
|
|||
using namespace Crafter;
|
||||
|
||||
// Loads the same sibling three ways: with its args left at their default,
|
||||
// with nothing forwarded, and with a list of its own. Then a sibling that
|
||||
// makes its own sibling's spec at namespace scope. The test reads what each
|
||||
// copy received back out of cfg.dependencies.
|
||||
// with nothing forwarded, and with a list of its own. The test reads what
|
||||
// each copy received back out of cfg.dependencies.
|
||||
extern "C" Configuration CrafterBuildProject(std::span<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 = "./";
|
||||
|
|
@ -21,7 +19,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
|||
cfg.outputName = "app";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
ApplyStandardArgs(cfg, args);
|
||||
cfg.dependencies = { inherited, none, replaced, fromStatic };
|
||||
cfg.dependencies = { inherited, none, replaced };
|
||||
// GitProjectSpec takes the same default. Checked on the spec itself,
|
||||
// since a GitProject would need a repository to clone.
|
||||
const GitProjectSpec git{ .source = { .url = "unused" } };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -15,10 +15,7 @@ using namespace Crafter;
|
|||
// release while the project built --debug. The fixture is a chain
|
||||
// app -> lib -> leaf of LocalProjects; app also loads lib with an empty and
|
||||
// with a replaced list, and checks the GitProjectSpec default itself, which
|
||||
// a clone would need a repository for. A spec made at namespace scope gets
|
||||
// no args: made while dlopen runs the file's static initialisers, which
|
||||
// happens on its first load only, it would otherwise keep that load's args
|
||||
// for every later one.
|
||||
// a clone would need a repository for.
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
|
|
@ -29,21 +26,10 @@ namespace {
|
|||
}
|
||||
}
|
||||
|
||||
// The single sibling lib, static and leaf declare, or nullptr.
|
||||
// The single sibling lib and leaf declare, or nullptr.
|
||||
const Configuration* OnlyDependency(const Configuration& cfg) {
|
||||
return cfg.dependencies.size() == 1 ? cfg.dependencies[0] : nullptr;
|
||||
}
|
||||
|
||||
// What a sibling was loaded with, as ApplyStandardArgs split it up:
|
||||
// --debug into cfg.debug, the uninterpreted rest into cfg.projectArgs.
|
||||
// Printed either way, so a failure says which args the sibling read.
|
||||
void CheckLoaded(const Configuration* cfg, bool debug, const std::vector<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() {
|
||||
|
|
@ -74,25 +60,24 @@ int main() {
|
|||
try {
|
||||
Configuration cfg = LoadProject("project.cpp", args);
|
||||
Check(std::ranges::contains(cfg.compileFlags, "-DGIT_DEFAULT=1"), "a GitProjectSpec's args default to the project's own");
|
||||
Check(cfg.dependencies.size() == 4, std::format("the project declared its four siblings (got {})", cfg.dependencies.size()));
|
||||
if (cfg.dependencies.size() == 4) {
|
||||
const Configuration* inherited = cfg.dependencies[0];
|
||||
const Configuration* none = cfg.dependencies[1];
|
||||
const Configuration* replaced = cfg.dependencies[2];
|
||||
const Configuration* fromStatic = cfg.dependencies[3];
|
||||
CheckLoaded(inherited, true, flavor, "a LocalProject without .args gets the project's args");
|
||||
CheckLoaded(none, false, {}, "`.args = {}` forwards nothing");
|
||||
CheckLoaded(replaced, false, only, "an explicit list replaces the args rather than adding to them");
|
||||
Check(inherited != none && inherited != replaced && none != replaced, "each list loads its own configuration");
|
||||
Check(cfg.dependencies.size() == 3, std::format("the project declared its three siblings (got {})", cfg.dependencies.size()));
|
||||
if (cfg.dependencies.size() == 3) {
|
||||
const Configuration& inherited = *cfg.dependencies[0];
|
||||
const Configuration& none = *cfg.dependencies[1];
|
||||
const Configuration& replaced = *cfg.dependencies[2];
|
||||
Check(inherited.debug && inherited.projectArgs == flavor, std::format("a LocalProject without .args gets the project's args (debug {}, projectArgs {})", inherited.debug, inherited.projectArgs));
|
||||
Check(!none.debug && none.projectArgs.empty(), std::format("`.args = {{}}` forwards nothing (debug {}, projectArgs {})", none.debug, none.projectArgs));
|
||||
Check(!replaced.debug && replaced.projectArgs == only, std::format("an explicit list replaces the args rather than adding to them (debug {}, projectArgs {})", replaced.debug, replaced.projectArgs));
|
||||
Check(&inherited != &none && &inherited != &replaced && &none != &replaced, "each list loads its own configuration");
|
||||
|
||||
// One sibling further down, the same default passes on what the
|
||||
// middle project was given, not what the top one was.
|
||||
CheckLoaded(OnlyDependency(*inherited), true, flavor, "the project's args reach its sibling's sibling");
|
||||
CheckLoaded(OnlyDependency(*none), false, {}, "a sibling loaded with nothing passes nothing on");
|
||||
CheckLoaded(OnlyDependency(*replaced), false, only, "a sibling passes on the list it was given");
|
||||
|
||||
CheckLoaded(fromStatic, true, flavor, "the sibling with a namespace-scope spec is itself loaded with the args");
|
||||
CheckLoaded(OnlyDependency(*fromStatic), false, {}, "a spec made at namespace scope forwards nothing");
|
||||
const Configuration* inheritedLeaf = OnlyDependency(inherited);
|
||||
Check(inheritedLeaf && inheritedLeaf->debug && inheritedLeaf->projectArgs == flavor, "the project's args reach its sibling's sibling");
|
||||
const Configuration* noneLeaf = OnlyDependency(none);
|
||||
Check(noneLeaf && !noneLeaf->debug && noneLeaf->projectArgs.empty(), "a sibling loaded with nothing passes nothing on");
|
||||
const Configuration* replacedLeaf = OnlyDependency(replaced);
|
||||
Check(replacedLeaf && !replacedLeaf->debug && replacedLeaf->projectArgs == only, "a sibling passes on the list it was given");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fs::current_path(cwd);
|
||||
|
|
|
|||
|
|
@ -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 appSo = scratch / "app" / "build" / "project.so";
|
||||
std::array<std::string_view, 0> noArgs = {};
|
||||
|
||||
Check(IsTopLevelProject(), "outside any project file counts as top level");
|
||||
|
||||
// The dependency is fetched, built and linked before project.cpp runs,
|
||||
// and ProjectDependency() hands it back by either name. The app ignores
|
||||
// the arg it is loaded with, and the dependency must not get it: host
|
||||
// tooling gets none of the consumer's args, although a LocalProject spec
|
||||
// defaults to them. The consumer's flags would split the tool's variant
|
||||
// from every other consumer's, and its --target= would have the tool's
|
||||
// project.cpp choose for the wrong triple.
|
||||
// and ProjectDependency() hands it back by either name.
|
||||
try {
|
||||
std::array<std::string_view, 1> flavored = { "--flavor=x" };
|
||||
Configuration cfg = LoadProject(app, flavored);
|
||||
Configuration cfg = LoadProject(app, noArgs);
|
||||
Check(cfg.name == "hello from greeter", std::format("project.cpp imported and called the dependency's module (got '{}')", cfg.name));
|
||||
Check(std::ranges::contains(cfg.compileFlags, "-DTOP_LEVEL=1"), "the project LoadProject was pointed at sees itself as top level");
|
||||
Check(cfg.dependencies.size() == 2, "ProjectDependency resolved by module name and by configuration name");
|
||||
|
|
@ -114,7 +109,6 @@ int main(int argc, char** argv) {
|
|||
Check(dep.march == "native", "the environment's march was overridden for host tooling");
|
||||
Check(std::ranges::contains(dep.compileFlags, "-fPIC"), "the dependency archive was built position-independent");
|
||||
Check(std::ranges::contains(dep.compileFlags, "-DTOP_LEVEL=0"), "a dependency's project file does not see itself as top level");
|
||||
Check(dep.projectArgs.empty(), std::format("the dependency got none of the consumer's args (got {})", dep.projectArgs));
|
||||
Check(fs::exists(ExpectedOutput(dep)), "the dependency archive exists");
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
|
|
@ -122,15 +116,20 @@ int main(int argc, char** argv) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
// A static initialiser of the project file runs while LoadProject
|
||||
// dlopens it, before CrafterBuildProject is called, and can already ask
|
||||
// for a dependency. In a child, because a static initialiser that
|
||||
// throws terminates the process.
|
||||
{
|
||||
const std::string loaded = LoadInChild(scratch / "static-dep" / "project.cpp");
|
||||
Check(loaded == "static greeter", std::format("ProjectDependency() works in a static initialiser (got '{}')", loaded));
|
||||
// Host tooling gets none of the consumer's args, although a LocalProject
|
||||
// spec defaults to them: the consumer's flags would split the tool's
|
||||
// variant from every other consumer's, and its --target= would have the
|
||||
// tool's project.cpp choose for the wrong triple.
|
||||
try {
|
||||
std::array<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");
|
||||
if (!fs::exists(appSo)) return 1;
|
||||
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");
|
||||
}
|
||||
|
||||
// `--` 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.
|
||||
{
|
||||
fs::remove(scratch / "record.txt");
|
||||
|
|
|
|||
Loading…
Reference in a new issue