// 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 change to what an executable is linked with has to relink it, even when // no object changed. The relink check compared modification times only, so a // link flag added in project.cpp, or a dependency's library moving (an // ExternalDependency whose options changed is fetched and built into a new // cache directory, and its -L and rpath follow), left the old binary in // place: Crafter.Network's tests kept loading the msquic they were first // linked against. // // The test adds an rpath to an executable's link flags, takes it away again, // and then builds once more with nothing changed, which must not relink. // The flags come out of sets the dependency threads fill as they finish, so // the recorded command lists them sorted: in the order they happened to land // in, an unchanged build could look like a changed one. namespace { std::int32_t failures = 0; void Check(bool cond, std::string_view msg) { if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++failures; } } constexpr std::string_view marker = "/crafter-build-link-flags-change-marker"; BuildResult BuildOnce(const fs::path& project, std::vector linkFlags) { Configuration app; app.path = project; app.name = "linked"; app.outputName = "linked"; app.target = HostTarget(); app.type = ConfigurationType::Executable; app.linkFlags = std::move(linkFlags); std::array ifaces = {}; std::array impls = { "main" }; app.GetInterfacesAndImplementations(ifaces, impls); std::unordered_map> depResults; std::mutex depMutex; BuildResult result = Build(app, depResults, depMutex); return result; } Configuration Plain(const fs::path& project) { Configuration app; app.path = project; app.name = "linked"; app.outputName = "linked"; app.target = HostTarget(); app.type = ConfigurationType::Executable; return app; } fs::path Binary(const fs::path& project) { return Plain(project).BinDir() / "linked"; } bool HasMarker(const fs::path& binary) { std::ifstream stream(binary, std::ios::binary); const std::string bytes((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); return bytes.find(marker) != std::string::npos; } } int main() { const fs::path project = fs::temp_directory_path() / "crafter-build-link-flags-change"; fs::remove_all(project); fs::copy(fs::current_path() / "tests" / "LinkFlagsChange" / "fixture", project, fs::copy_options::recursive); const std::string rpath = std::format("-Wl,-rpath,{}", marker); const BuildResult first = BuildOnce(project, {}); Check(first.result.empty(), std::format("the plain build succeeds: {}", first.result)); const fs::path binary = Binary(project); Check(fs::exists(binary) && !HasMarker(binary), "the plain binary has no rpath to the marker"); const BuildResult added = BuildOnce(project, { rpath }); Check(added.result.empty(), std::format("the build with the rpath succeeds: {}", added.result)); Check(HasMarker(binary), "adding a link flag relinks the binary"); const BuildResult removed = BuildOnce(project, {}); Check(removed.result.empty(), std::format("the build without it succeeds: {}", removed.result)); Check(!HasMarker(binary), "taking it away relinks it again"); const fs::file_time_type linked = fs::last_write_time(binary); const BuildResult again = BuildOnce(project, {}); Check(again.result.empty() && fs::last_write_time(binary) == linked, "an unchanged build doesn't relink"); const std::vector flags = { "-Wl,-z,now", "-Wl,-O1", "-Wl,--as-needed", "-Wl,-z,relro", "-Wl,--build-id", "-Wl,--hash-style=gnu" }; const BuildResult many = BuildOnce(project, flags); Check(many.result.empty(), std::format("the build with six flags succeeds: {}", many.result)); std::ifstream recorded(Plain(project).BuildDir() / "linked.link", std::ios::binary); const std::string record((std::istreambuf_iterator(recorded)), std::istreambuf_iterator()); std::vector at; for (const std::string& flag : flags) at.push_back(record.find(std::format(" {}", flag))); std::vector sorted = flags; std::ranges::sort(sorted); bool inOrder = !std::ranges::contains(at, std::string::npos); for (std::size_t k = 0; inOrder && k + 1 < sorted.size(); ++k) { const auto index = [&](const std::string& flag) { return at[static_cast(std::ranges::find(flags, flag) - flags.begin())]; }; inOrder = index(sorted[k]) < index(sorted[k + 1]); } Check(inOrder, std::format("the link record lists the flags sorted: {}", record)); std::error_code ec; fs::remove_all(project, ec); if (failures > 0) { std::println(std::cerr, "{} assertions failed", failures); return 1; } return 0; }