151 lines
7.6 KiB
C++
151 lines
7.6 KiB
C++
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
#include <stdlib.h>
|
||
|
|
import std;
|
||
|
|
import Crafter.Build;
|
||
|
|
namespace fs = std::filesystem;
|
||
|
|
using namespace Crafter;
|
||
|
|
|
||
|
|
// `#pragma crafter dependency`: a project.cpp that imports a module from a
|
||
|
|
// sibling crafter-build project. LoadProject has to fetch and build that
|
||
|
|
// project before project.cpp compiles, put its BMI and archive on the
|
||
|
|
// project.so command line, and treat the archive as an input to the
|
||
|
|
// staleness check. The fixture is copied to a scratch dir because one case
|
||
|
|
// edits a source and every build drops bin/ and build/ trees next to it.
|
||
|
|
//
|
||
|
|
// A recompiled project.so can't be observed from the process that already
|
||
|
|
// dlopened the old one — the loader hands back the mapping it has for that
|
||
|
|
// path — so the "dependency changed" cases load through a child process:
|
||
|
|
// this binary re-run with `--load <project.cpp>` prints the name the project
|
||
|
|
// file produced.
|
||
|
|
namespace {
|
||
|
|
std::int32_t Failures = 0;
|
||
|
|
|
||
|
|
void Check(bool cond, std::string_view msg) {
|
||
|
|
if (!cond) {
|
||
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
||
|
|
++Failures;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Expect LoadProject to throw; the message, or "" when it didn't.
|
||
|
|
std::string LoadFails(const fs::path& projectFile) {
|
||
|
|
std::array<std::string_view, 0> noArgs = {};
|
||
|
|
try {
|
||
|
|
(void)LoadProject(projectFile, noArgs);
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
return e.what();
|
||
|
|
}
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
// LoadProject in a fresh process; returns what it printed, trimmed.
|
||
|
|
std::string LoadInChild(const fs::path& projectFile) {
|
||
|
|
const fs::path self = fs::read_symlink("/proc/self/exe");
|
||
|
|
const auto r = RunCommandWithTimeout(std::format("{} --load {}", self.string(), projectFile.string()), std::chrono::seconds(300));
|
||
|
|
std::string out = r.output;
|
||
|
|
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||
|
|
if (r.exitCode != 0) return std::format("<exit {}> {}", r.exitCode, out);
|
||
|
|
// The dependency build's progress lines share stdout; the name is the
|
||
|
|
// last line.
|
||
|
|
const std::size_t lastBreak = out.rfind('\n');
|
||
|
|
return lastBreak == std::string::npos ? out : out.substr(lastBreak + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ChangeGreeting(const fs::path& scratch) {
|
||
|
|
const fs::path iface = scratch / "greeter" / "interfaces" / "Greeter.cppm";
|
||
|
|
fs::copy_file(scratch / "greeter" / "interfaces" / "Greeter-changed.cppm.in", iface, fs::copy_options::overwrite_existing);
|
||
|
|
// copy_file carries the source's mtime across, which would leave the
|
||
|
|
// rewritten interface looking older than the BMI built from it.
|
||
|
|
fs::last_write_time(iface, fs::file_time_type::clock::now());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(int argc, char** argv) {
|
||
|
|
if (argc == 3 && std::string_view(argv[1]) == "--load") {
|
||
|
|
std::array<std::string_view, 0> noArgs = {};
|
||
|
|
Configuration cfg = LoadProject(argv[2], noArgs);
|
||
|
|
std::println("{}", cfg.name);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fs::path fixture = fs::current_path() / "tests" / "ProjectDependency" / "fixture";
|
||
|
|
const fs::path scratch = fs::temp_directory_path() / "crafter-build-project-dependency";
|
||
|
|
std::error_code ec;
|
||
|
|
fs::remove_all(scratch, ec);
|
||
|
|
fs::create_directories(scratch);
|
||
|
|
fs::copy(fixture, scratch, fs::copy_options::recursive);
|
||
|
|
|
||
|
|
// The dependency's project.cpp applies the standard args, which read the
|
||
|
|
// march from the environment. project.so is compiled for the host, so
|
||
|
|
// the pre-pass must override it or the dependency's BMIs won't load.
|
||
|
|
setenv("CRAFTER_BUILD_MARCH", "x86-64-v2", 1);
|
||
|
|
|
||
|
|
const fs::path app = scratch / "app" / "project.cpp";
|
||
|
|
const fs::path appSo = scratch / "app" / "build" / "project.so";
|
||
|
|
std::array<std::string_view, 0> noArgs = {};
|
||
|
|
|
||
|
|
// The dependency is fetched, built and linked before project.cpp runs,
|
||
|
|
// and ProjectDependency() hands it back by either name.
|
||
|
|
try {
|
||
|
|
Configuration cfg = LoadProject(app, noArgs);
|
||
|
|
Check(cfg.name == "hello from greeter", std::format("project.cpp imported and called the dependency's module (got '{}')", cfg.name));
|
||
|
|
Check(cfg.dependencies.size() == 2, "ProjectDependency resolved by module name and by configuration name");
|
||
|
|
if (cfg.dependencies.size() == 2) {
|
||
|
|
Check(cfg.dependencies[0] == cfg.dependencies[1], "both names resolve to the same dependency");
|
||
|
|
const Configuration& dep = *cfg.dependencies[0];
|
||
|
|
Check(dep.name == "greeter", "the dependency is the fixture's greeter project");
|
||
|
|
Check(dep.target == "x86_64-pc-linux-gnu", "the dependency was built for the host triple LoadProject uses");
|
||
|
|
Check(dep.march == "native", "the environment's march was overridden for host tooling");
|
||
|
|
Check(std::ranges::contains(dep.compileFlags, "-fPIC"), "the dependency archive was built position-independent");
|
||
|
|
Check(fs::exists(ExpectedOutput(dep)), "the dependency archive exists");
|
||
|
|
}
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
std::println(std::cerr, "FAIL: LoadProject threw: {}", e.what());
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
Check(fs::exists(appSo), "project.so was produced");
|
||
|
|
if (!fs::exists(appSo)) return 1;
|
||
|
|
const fs::file_time_type firstSo = fs::last_write_time(appSo);
|
||
|
|
|
||
|
|
// Editing the dependency rebuilds it and recompiles project.so against
|
||
|
|
// it — without touching project.cpp.
|
||
|
|
ChangeGreeting(scratch);
|
||
|
|
const std::string changed = LoadInChild(app);
|
||
|
|
Check(changed == "hello again", std::format("a changed dependency reaches project.cpp (got '{}')", changed));
|
||
|
|
Check(fs::last_write_time(appSo) > firstSo, "project.so was recompiled after the dependency changed");
|
||
|
|
const fs::file_time_type secondSo = fs::last_write_time(appSo);
|
||
|
|
|
||
|
|
// And an unchanged dependency doesn't: "always stale" would be invisible
|
||
|
|
// to every other case here.
|
||
|
|
const std::string unchanged = LoadInChild(app);
|
||
|
|
Check(unchanged == "hello again", std::format("an unchanged dependency still loads (got '{}')", unchanged));
|
||
|
|
Check(fs::last_write_time(appSo) == secondSo, "project.so is left alone when nothing changed");
|
||
|
|
|
||
|
|
// Directive errors name the file and line.
|
||
|
|
const std::string noSource = LoadFails(scratch / "bad-nosource" / "project.cpp");
|
||
|
|
Check(noSource.contains("project.cpp:3:") && noSource.contains("expected a quoted source"), std::format("a directive without a source is rejected with its location (got '{}')", noSource));
|
||
|
|
const std::string both = LoadFails(scratch / "bad-both" / "project.cpp");
|
||
|
|
Check(both.contains("project.cpp:3:") && both.contains("mutually exclusive"), std::format("branch and commit together are rejected (got '{}')", both));
|
||
|
|
|
||
|
|
// Two project files that depend on each other stop instead of recursing.
|
||
|
|
const std::string cycle = LoadFails(scratch / "cycle-a" / "project.cpp");
|
||
|
|
Check(cycle.contains("circular project dependency"), std::format("a dependency cycle is reported (got '{}')", cycle));
|
||
|
|
|
||
|
|
// The accessor is only meaningful while a project file is running.
|
||
|
|
try {
|
||
|
|
(void)ProjectDependency("Greeter");
|
||
|
|
Check(false, "ProjectDependency outside a project file throws");
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
Check(std::string_view(e.what()).contains("outside a project file"), std::format("ProjectDependency outside a project file explains itself (got '{}')", e.what()));
|
||
|
|
}
|
||
|
|
|
||
|
|
return Failures > 0 ? 1 : 0;
|
||
|
|
}
|