Compare commits

..
11 changed files with 4 additions and 233 deletions

View file

@ -74,7 +74,7 @@ Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project.
cfg.dependencies = { math }; cfg.dependencies = { math };
``` ```
The sibling's `CrafterBuildProject` gets the args this `project.cpp` was loaded with (`ProjectArgs()`), so `--target=`, `--march=`, `--debug` and the project's own flags reach every sibling down the chain. That is all of them: `cfg.projectArgs` holds only what `ApplyStandardArgs` did not interpret, and passing it on would drop the target and the build type. Set `.args` to send something else: `{}` for nothing, or a list built from the project's args, such as one without a flag only this project reads. Build such a list once and give it to every sibling. A project that two paths load with different lists would end up in the program twice, so the build stops with an error that names both paths. Releases up to 2.2.0 forward nothing when `.args` is left out. The 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 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.

View file

@ -443,42 +443,6 @@ namespace {
} }
return outcome; return outcome;
} }
// Two configurations among `config` and its transitive dependencies that
// export a module of the same name: the same project loaded along two
// paths with different args, say, or from a checkout on one path and a
// clone on the other. A program can hold one module of each name. Built
// anyway, both PCM dirs went onto the consumer's -fprebuilt-module-path,
// clang resolved the module by name to the first for every import, the
// ones inside the other build's consumers included, and nothing checked
// that it was the build they had been compiled against. The program then
// linked code compiled against two layouts of the same types, and ran
// (issue #41). Empty when the dependencies agree, otherwise the error,
// naming the path to each of the two builds.
std::string DuplicateModuleError(const Configuration& config) {
struct Owner {
const Configuration* cfg;
std::string chain;
};
const auto describe = [](const Owner& owner) {
return std::format("{} ({}, {}, project args {})", owner.chain, owner.cfg->path.string(), owner.cfg->VariantId(), owner.cfg->projectArgs);
};
std::unordered_map<std::string, Owner> owners;
std::unordered_set<const Configuration*> visited;
std::string error;
std::function<void(const Configuration&, const std::string&)> walk = [&](const Configuration& c, const std::string& chain) {
if (!error.empty() || !visited.insert(&c).second) return;
for (const std::unique_ptr<Module>& iface : c.interfaces) {
const auto [it, inserted] = owners.emplace(iface->name, Owner{ &c, chain });
if (inserted || it->second.cfg == &c) continue;
error = std::format("module {} is built twice for {}:\n {}\n {}\nA program can hold one build of each module: load {} with the same args, and from the same place, along every path.", iface->name, config.name, describe(it->second), describe(Owner{ &c, chain }), c.name);
return;
}
for (const Configuration* dep : c.dependencies) walk(*dep, std::format("{} -> {}", chain, dep->name));
};
walk(config, config.name);
return error;
}
} }
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) { BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
@ -487,11 +451,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) { if (const Toolchain& tc = GetToolchain(); !tc.error.empty()) {
return {tc.error, false, {}}; return {tc.error, false, {}};
} }
// Before any directory is made or dependency built, so a refused graph
// leaves nothing behind.
if (std::string duplicate = DuplicateModuleError(config); !duplicate.empty()) {
return {std::move(duplicate), false, {}};
}
// Reset per-build cached state on every Module/ModulePartition so that // Reset per-build cached state on every Module/ModulePartition so that
// successive Build() calls on the same Configuration re-evaluate mtimes // successive Build() calls on the same Configuration re-evaluate mtimes
// (incremental-rebuild test scenarios). // (incremental-rebuild test scenarios).

View file

@ -97,9 +97,9 @@ export namespace Crafter {
// --debug and the project's own flags reach every sibling down a chain // --debug and the project's own flags reach every sibling down a chain
// without being passed. Set them to forward something else: {} for // without being passed. Set them to forward something else: {} for
// nothing, or a list built from this project's args, such as one without // 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 flag only this project reads. Give such a list to every sibling: a
// Build() refuses a graph in which two paths load one project with two // project that two paths load with two different lists is built twice,
// different lists, since the program would end up with both builds. // and the program ends up with both builds.
struct GitProjectSpec { struct GitProjectSpec {
GitSource source; GitSource source;
fs::path projectFile = "project.cpp"; fs::path projectFile = "project.cpp";

View file

@ -284,8 +284,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
cfg.AddTest("HostToolingSibling").Dependencies({ CrafterBuildLib.get() }) cfg.AddTest("HostToolingSibling").Dependencies({ CrafterBuildLib.get() })
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
cfg.AddTest("DuplicateModule").Dependencies({ CrafterBuildLib.get() })
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() }) cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl"); .LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });

View file

@ -1,6 +0,0 @@
export module A;
import std;
import M;
export std::int32_t ASize() {
return Size();
}

View file

@ -1,24 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Loads M with the default, so M gets whatever A was given.
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
Configuration* m = LocalProject({ .projectFile = "../m/project.cpp" });
Configuration cfg;
cfg.path = "./";
cfg.name = "A";
cfg.outputName = "A";
cfg.type = ConfigurationType::LibraryStatic;
ApplyStandardArgs(cfg, args);
cfg.dependencies = { m };
std::array<fs::path, 1> ifaces = { "interfaces/A" };
std::array<fs::path, 0> impls = {};
cfg.GetInterfacesAndImplementations(ifaces, impls);
return cfg;
}

View file

@ -1,12 +0,0 @@
export module M;
import std;
// --wide gives S more members, so the two builds of M the test asks for
// differ in what they declare.
#ifdef WIDE
export struct S { std::int32_t a; std::int32_t b; std::int64_t pad[4]; };
#else
export struct S { std::int32_t a; std::int32_t b; };
#endif
export std::int32_t Size() {
return static_cast<std::int32_t>(sizeof(S));
}

View file

@ -1,22 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// --wide is this project's own flag, and it changes what M declares.
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
Configuration cfg;
cfg.path = "./";
cfg.name = "M";
cfg.outputName = "M";
cfg.type = ConfigurationType::LibraryStatic;
ApplyStandardArgs(cfg, args);
if (std::ranges::contains(args, std::string_view("--wide"))) cfg.compileFlags.push_back("-DWIDE");
std::array<fs::path, 1> ifaces = { "interfaces/M" };
std::array<fs::path, 0> impls = {};
cfg.GetInterfacesAndImplementations(ifaces, impls);
return cfg;
}

View file

@ -1,6 +0,0 @@
import std;
import M;
import A;
int main() {
return Size() == ASize() ? 0 : 1;
}

View file

@ -1,32 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Loads M itself and through A, which passes on all of its args. With
// --split, the list P gives M leaves --wide out, as a filter meant for one
// sibling would, and the two paths load M with two different lists.
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
const bool split = std::ranges::contains(args, std::string_view("--split"));
std::vector<std::string> mArgs;
for (std::string_view arg : args) {
if (!split || arg != "--wide") mArgs.emplace_back(arg);
}
Configuration* m = LocalProject({ .projectFile = "../m/project.cpp", .args = mArgs });
Configuration* a = LocalProject({ .projectFile = "../a/project.cpp" });
Configuration cfg;
cfg.path = "./";
cfg.name = "P";
cfg.outputName = "P";
cfg.type = ConfigurationType::Executable;
ApplyStandardArgs(cfg, args);
cfg.dependencies = { m, a };
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
return cfg;
}

View file

@ -1,84 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// A program can hold one module of each name, so Build() refuses a graph in
// which two configurations export the same module: the same project loaded
// along two paths with two arg lists, say. It used to build both and put
// both on the consumer's module path. Clang took the first for every import
// of the module, the ones inside the other build's consumers included, and
// the program linked code compiled against two layouts of one struct: it
// built cleanly, then read garbage or crashed (issue #41). The fixture is
// P -> M and P -> A -> M. With --split, P gives M its args without --wide,
// which changes what M declares, while A passes them on whole.
namespace {
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
BuildResult BuildPass(Configuration& cfg) {
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
return Build(cfg, depResults, depMutex);
}
}
int main() {
const fs::path installedHome = GetCrafterBuildHome();
if (!fs::exists(installedHome / "Crafter.Build.cppm")) {
std::println(std::cerr, "SKIP: no module sources at {}", installedHome.string());
return 77;
}
// Staged because LoadProject and Build drop build/ and bin/ trees next
// to the project files.
const fs::path fixture = fs::current_path() / "tests" / "DuplicateModule" / "fixture";
const fs::path scratch = fs::temp_directory_path() / "crafter-build-duplicate-module";
std::error_code ec;
fs::remove_all(scratch, ec);
fs::create_directories(scratch);
fs::copy(fixture, scratch, fs::copy_options::recursive);
// From P's own directory, as crafter-build runs it: the fixture's
// LocalProject paths are relative to the working directory.
const fs::path cwd = fs::current_path();
fs::current_path(scratch / "p");
try {
// Two lists for M: refused before anything is built.
{
std::array<std::string_view, 2> args = { "--wide", "--split" };
Configuration cfg = LoadProject("project.cpp", args);
const BuildResult built = BuildPass(cfg);
Check(!built.result.empty(), "a graph with two builds of one module fails to build");
Check(built.result.contains("module M is built twice for P"), std::format("the error names the module and the program (got: {})", built.result));
Check(built.result.contains("P -> M (") && built.result.contains("P -> A -> M ("), std::format("the error names the path to each build (got: {})", built.result));
Check(!fs::exists(scratch / "m" / "bin") && !fs::exists(scratch / "a" / "bin") && !fs::exists(scratch / "p" / "bin"), "nothing was built");
}
// One list along both paths: one M, and the build goes through.
{
std::array<std::string_view, 1> args = { "--wide" };
Configuration cfg = LoadProject("project.cpp", args);
const bool oneM = cfg.dependencies.size() == 2 && cfg.dependencies[1]->dependencies.size() == 1 && cfg.dependencies[0] == cfg.dependencies[1]->dependencies[0];
Check(oneM, "the same list along both paths loads one M");
const BuildResult built = BuildPass(cfg);
Check(built.result.empty(), std::format("the graph with one M builds (got: {})", built.result));
}
} catch (const std::exception& e) {
fs::current_path(cwd);
std::println(std::cerr, "FAIL: threw: {}", e.what());
return 1;
}
fs::current_path(cwd);
return Failures > 0 ? 1 : 0;
}