diff --git a/project.cpp b/project.cpp index 3c0915c..38db3a4 100644 --- a/project.cpp +++ b/project.cpp @@ -258,6 +258,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.AddTest("GitSubmodules").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() }); + cfg.AddTest("ExternalReconfigure").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ConcurrentExternalFetch").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("SharedExternalFetch").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() }); diff --git a/tests/ExternalReconfigure/main.cpp b/tests/ExternalReconfigure/main.cpp new file mode 100644 index 0000000..57d50ba --- /dev/null +++ b/tests/ExternalReconfigure/main.cpp @@ -0,0 +1,97 @@ +// 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 CMake dependency configured with other inputs than this build's (another +// compiler or --sysroot, the flags crafter-build injects, which it records in +// /.crafter-options) has to be built again from nothing. It was +// reconfigured in place, and CMake does not recompile what a changed +// --sysroot compiled (the Makefiles generator writes it into build.make, +// which make does not compare) and keeps the check results of the old +// configure. A mingw dependency built by a release that gave clang the +// host's /usr as the sysroot (issue #43) kept its objects reading +// __libc_single_threaded after the update. +// +// The dependency is a local repository with a one-file C library. A file +// planted in its build directory stands for what the old configure left, +// and the recorded inputs are rewritten the way another crafter-build would +// have left them. 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; + } +} + +int main() { + const fs::path scratch = fs::temp_directory_path() / "crafter-build-external-reconfigure"; + 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 repo = scratch / "reconf"; + fs::create_directories(repo); + Write(repo / "lib.c", "int Reconfigured(void) { return 1; }\n"); + Write(repo / "CMakeLists.txt", + "cmake_minimum_required(VERSION 3.16)\n" + "project(reconf C)\n" + "add_library(reconf STATIC lib.c)\n"); + Git(repo, "init -q -b main"); + Git(repo, "add ."); + Git(repo, "commit -q -m reconf"); + + ExternalDependency dep; + dep.name = "reconf"; + dep.source.url = std::format("file://{}", repo.string()); + dep.builder = ExternalBuilder::CMake; + dep.libs = { "reconf" }; + std::string error = BuildExternal(dep, HostTarget()); + Check(error.empty(), std::format("the dependency builds: {}", error)); + + const fs::path build = ExternalBuildDir(dep, HostTarget()); + const fs::path leftover = build / "left-by-the-old-configure"; + Write(leftover, ""); + error = BuildExternal(dep, HostTarget()); + Check(error.empty(), std::format("and builds again: {}", error)); + Check(fs::exists(leftover), "with the same inputs its build directory is kept"); + + Write(build / ".crafter-options", " -DCMAKE_SYSROOT=/usr\n"); + error = BuildExternal(dep, HostTarget()); + Check(error.empty(), std::format("configured with other inputs, it builds: {}", error)); + Check(!fs::exists(leftover), "from an empty build directory"); + Check(fs::exists(build / "libreconf.a"), "and the library is built again"); + + std::error_code ec; + fs::remove_all(scratch, ec); + if (failures > 0) { + std::println(std::cerr, "{} assertions failed", failures); + return 1; + } + return 0; +}