// 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> 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 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 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; }