// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Four configurations: // Crafter.Debug.Core what the app and the client share (values, codecs, // the protocol, discovery, build info) // Crafter.Debug.Cli the client's code, a library so tests can run it // crafter.debug-cli the client, crafter-debug // Crafter.Debug what an app links (returned), depending on the core // and the client // The client is one of the library's dependencies only so that a consumer's // debug build compiles it in the same pass as the app; an executable adds no // link flags to what depends on it. Consumers load this file through // Debug::Use (tooling/), which also writes their launcher. // // The library and the client talk QUIC through Crafter.Network, which comes // from forgejo, with --local from the checkout beside this one, or from the // project.cpp $CRAFTER_DEBUG_NETWORK names (a Network worktree). import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; namespace { // The configurations this file makes besides the one it returns, at // stable addresses for the pointers in `dependencies`. One set per load, // not statics: a static would keep the first load's args for every load. std::deque owned; // This file's directory. A configuration a project file makes for itself // keeps the path it is given, and "./" would later resolve against the // consumer's directory instead of this one. fs::path Root() { return fs::path(std::source_location::current().file_name()).parent_path(); } // Crafter.Network, with the args unchanged: a consumer that also uses // it passes the same ones, so both resolve to one configuration rather // than one module built twice. Configuration* Network(std::span args) { const std::vector forwarded(args.begin(), args.end()); if (const char* local = std::getenv("CRAFTER_DEBUG_NETWORK"); local != nullptr && *local != '\0') { return LocalProject({ .projectFile = local, .args = forwarded }); } if (std::ranges::find(args, std::string_view("--local")) != args.end()) { return LocalProject({ .projectFile = Root().parent_path() / "Crafter.Network" / "project.cpp", .args = forwarded }); } return GitProject({ .source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git" }, .args = forwarded, }); } // Tests of the core, linked against it alone. constexpr std::array coreTests = { "ShouldRoundTripJson", "ShouldRoundTripAggregates", "ShouldReportDecodeErrorsWithPaths", "ShouldDescribeDefaultsAndRequired", "ShouldDiscoverLiveInstances", }; // Tests of the app library. constexpr std::array libraryTests = { "ShouldInvokeCommands", "ShouldResolveWatchPaths", "ShouldRecordTimeline", "ShouldEvaluateConditions", "ShouldAnswerRequests", "ShouldPauseAndStep", "ShouldWaitTraceAndFollow", }; std::string ReadFile(const fs::path& path) { std::ifstream in(path, std::ios::binary); return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); } // FNV-1a over the path and bytes of every source, in a fixed order: what // the app and the client compare to tell they came from the same code. std::string SourceId() { std::vector files; for (const std::string_view dir : { "interfaces", "implementations" }) { std::error_code ec; for (const fs::directory_entry& entry : fs::recursive_directory_iterator(Root() / dir, ec)) { if (entry.is_regular_file() && (entry.path().extension() == ".cppm" || entry.path().extension() == ".cpp")) files.push_back(entry.path()); } } std::ranges::sort(files); std::uint64_t hash = 14695981039346656037ULL; const auto mix = [&](std::string_view bytes) { for (const char c : bytes) { hash ^= static_cast(c); hash *= 1099511628211ULL; } }; for (const fs::path& file : files) { mix(file.lexically_relative(Root()).generic_string()); mix(ReadFile(file)); } return std::format("{:016x}", hash); } void WriteIfChanged(const fs::path& file, const std::string& text) { if (fs::exists(file) && ReadFile(file) == text) return; fs::create_directories(file.parent_path()); std::ofstream(file, std::ios::binary | std::ios::trunc) << text; } // Generates the definitions of :BuildInfo for this variant of the core and // returns the source to compile, without its extension. fs::path GenerateBuildInfo(const Configuration& core, const Configuration& client) { const fs::path source = Root() / "build" / "generated" / core.VariantId() / "Crafter.Debug.Core-BuildInfo"; const fs::path clientPath = client.BinDir() / client.outputName; WriteIfChanged(fs::path(source).concat(".cpp"), std::format( "// Generated by Crafter.Debug's project.cpp whenever the sources or the client's\n" "// path change. Not to be edited.\n" "module Crafter.Debug.Core:BuildInfo_impl;\n" "import std;\n" "import :BuildInfo;\n" "\n" "std::string_view Crafter::Debug::Detail::SourceId() {{\n" " return \"{}\";\n" "}}\n" "\n" "std::string_view Crafter::Debug::Detail::ClientPath() {{\n" " return R\"crafter-debug({})crafter-debug\";\n" "}}\n", SourceId(), clientPath.string())); return source; } } extern "C" Configuration CrafterBuildProject(std::span args) { Configuration cfg; cfg.path = "./"; cfg.name = "Crafter.Debug"; cfg.outputName = "Crafter.Debug"; cfg.type = ConfigurationType::LibraryStatic; ApplyStandardArgs(cfg, args); Configuration* network = Network(args); Configuration& core = owned.emplace_back(); core.path = Root(); core.name = "Crafter.Debug.Core"; core.outputName = "Crafter.Debug.Core"; core.type = ConfigurationType::LibraryStatic; ApplyStandardArgs(core, args); // The client, built for the library's target. Not through // ApplyStandardArgs: a consumer's --lib would make it a library. Configuration& client = owned.emplace_back(); client.path = Root(); client.name = "crafter.debug-cli"; client.outputName = "crafter-debug"; client.type = ConfigurationType::Executable; client.target = cfg.target; client.march = cfg.march; client.mtune = cfg.mtune; client.sysroot = cfg.sysroot; client.debug = cfg.debug; // The client's code as a library, so tests can run it in process. Configuration& cli = owned.emplace_back(); cli.path = Root(); cli.name = "Crafter.Debug.Cli"; cli.outputName = "Crafter.Debug.Cli"; cli.type = ConfigurationType::LibraryStatic; cli.target = cfg.target; cli.march = cfg.march; cli.mtune = cfg.mtune; cli.sysroot = cfg.sysroot; cli.debug = cfg.debug; cli.dependencies = { &core, network }; client.dependencies = { &cli }; { std::array interfaces = { "interfaces/Crafter.Debug.Core-Value", "interfaces/Crafter.Debug.Core-Reflect", "interfaces/Crafter.Debug.Core-Codec", "interfaces/Crafter.Debug.Core-Result", "interfaces/Crafter.Debug.Core-Protocol", "interfaces/Crafter.Debug.Core-BuildInfo", "interfaces/Crafter.Debug.Core-Discovery", "interfaces/Crafter.Debug.Core", }; std::array implementations = { "implementations/Crafter.Debug.Core-Value", "implementations/Crafter.Debug.Core-Codec", "implementations/Crafter.Debug.Core-Protocol", "implementations/Crafter.Debug.Core-Discovery", GenerateBuildInfo(core, client), }; core.GetInterfacesAndImplementations(interfaces, implementations); } { std::array interfaces = { "interfaces/Crafter.Debug.Cli-Client", "interfaces/Crafter.Debug.Cli-Quic", "interfaces/Crafter.Debug.Cli" }; std::array implementations = { "implementations/Crafter.Debug.Cli-Client", "implementations/Crafter.Debug.Cli-Quic" }; cli.GetInterfacesAndImplementations(interfaces, implementations); } { std::array interfaces = {}; std::array implementations = { "implementations/main" }; client.GetInterfacesAndImplementations(interfaces, implementations); } cfg.dependencies = { &core, &client, network }; { std::array interfaces = { "interfaces/Crafter.Debug-Registry", "interfaces/Crafter.Debug-Timeline", "interfaces/Crafter.Debug-Condition", "interfaces/Crafter.Debug-Session", "interfaces/Crafter.Debug-Transport", "interfaces/Crafter.Debug", }; std::array implementations = { "implementations/Crafter.Debug-Registry", "implementations/Crafter.Debug-Timeline", "implementations/Crafter.Debug-Condition", "implementations/Crafter.Debug-Session", "implementations/Crafter.Debug-Transport", }; cfg.GetInterfacesAndImplementations(interfaces, implementations); } // Tests only when this is the project being built: a consumer's load of // this file has no use for them. if (IsTopLevelProject()) { for (const std::string_view test : coreTests) cfg.AddTest(test).Dependencies({ &core }); for (const std::string_view test : libraryTests) cfg.AddTest(test).Dependencies({ &cfg }); cfg.AddTest("ShouldRunClientCommands").Dependencies({ &cfg, &cli }); } return cfg; }