diff --git a/project.cpp b/project.cpp index 4ea9679..bfd3e9b 100644 --- a/project.cpp +++ b/project.cpp @@ -282,6 +282,8 @@ extern "C" Configuration CrafterBuildProject(std::span 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() }); diff --git a/tests/HostToolingSibling/fixture/app/project.cpp b/tests/HostToolingSibling/fixture/app/project.cpp new file mode 100644 index 0000000..d9f107a --- /dev/null +++ b/tests/HostToolingSibling/fixture/app/project.cpp @@ -0,0 +1,22 @@ +// 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 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; +} diff --git a/tests/HostToolingSibling/fixture/shared/interfaces/Shared.cppm b/tests/HostToolingSibling/fixture/shared/interfaces/Shared.cppm new file mode 100644 index 0000000..af40390 --- /dev/null +++ b/tests/HostToolingSibling/fixture/shared/interfaces/Shared.cppm @@ -0,0 +1,5 @@ +export module Shared; +import std; +export std::int32_t SharedValue() { + return 7; +} diff --git a/tests/HostToolingSibling/fixture/shared/project.cpp b/tests/HostToolingSibling/fixture/shared/project.cpp new file mode 100644 index 0000000..3cd5bdc --- /dev/null +++ b/tests/HostToolingSibling/fixture/shared/project.cpp @@ -0,0 +1,23 @@ +// 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 args) { + Configuration cfg; + cfg.path = "./"; + cfg.name = "shared"; + cfg.outputName = "shared"; + cfg.type = ConfigurationType::LibraryStatic; + ApplyStandardArgs(cfg, args); + std::array ifaces = { "interfaces/Shared" }; + std::array impls = {}; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/HostToolingSibling/fixture/tool/interfaces/Tool.cppm b/tests/HostToolingSibling/fixture/tool/interfaces/Tool.cppm new file mode 100644 index 0000000..c6d5490 --- /dev/null +++ b/tests/HostToolingSibling/fixture/tool/interfaces/Tool.cppm @@ -0,0 +1,6 @@ +export module Tool; +import std; +import Shared; +export std::int32_t ToolValue() { + return SharedValue(); +} diff --git a/tests/HostToolingSibling/fixture/tool/project.cpp b/tests/HostToolingSibling/fixture/tool/project.cpp new file mode 100644 index 0000000..51ad781 --- /dev/null +++ b/tests/HostToolingSibling/fixture/tool/project.cpp @@ -0,0 +1,24 @@ +// 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 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 ifaces = { "interfaces/Tool" }; + std::array impls = {}; + cfg.GetInterfacesAndImplementations(ifaces, impls); + return cfg; +} diff --git a/tests/HostToolingSibling/main.cpp b/tests/HostToolingSibling/main.cpp new file mode 100644 index 0000000..f6ad160 --- /dev/null +++ b/tests/HostToolingSibling/main.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +#include +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 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; +}