// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Build:External; import std; namespace fs = std::filesystem; export namespace Crafter { struct Configuration; struct GitSource { std::string url; std::string branch; std::string commit; }; enum class ExternalBuilder { None, CMake, }; struct ExternalDependency { std::string name; GitSource source; ExternalBuilder builder = ExternalBuilder::None; std::vector options; std::vector includeDirs; // Extra library search paths (each becomes a -L flag), interpreted // relative to the CMake build dir. Currently only honoured for // ExternalBuilder::CMake. The CMake builder always adds its // top-level build dir as a -L; libDirs is for projects (msquic, // others) whose CMakeLists set LIBRARY_OUTPUT_DIRECTORY to a subdir // of the build tree. std::vector libDirs; std::vector libs; }; struct ExternalBuildResult { std::vector compileFlags; std::vector linkFlags; fs::file_time_type latestArtifact = fs::file_time_type::min(); }; // Where this dependency's clone lives in the cache. A pure function of the // declaration and target — BuildExternal derives its own working directory // through this, so a caller that only needs to know where the headers ended // up cannot drift from where they actually are. fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target); // Where BuildExternal leaves this dependency: the CMake build directory, // or the clone itself when it is only fetched. Pure, like ExternalCloneDir. // Build() keys a pass's record of the work on it rather than on the clone, // so a declaration that shares the clone but builds it with CMake is not // handed the outcome of a fetch alone. fs::path ExternalBuildDir(const ExternalDependency& dep, std::string_view target); // The -I flags this dependency contributes, from its declared includeDirs. // Also pure, which is what lets the linter's AST layer parse a source that // includes an external's headers without running a build to find out where // they are. The paths only resolve once something has fetched the clone. std::vector ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target); // Fetches the clone and, for ExternalBuilder::CMake, configures and builds // it. Empty on success, otherwise what failed. A lock on the clone is held // throughout, so another crafter-build, or another build pass in this one, // works on it before or after this call and never during it. Not // cancellable once started: Build() hands the outcome to every // configuration of the pass that declares the dependency, and a run cut // short would leave them a clone nobody fetched. std::string BuildExternal(const ExternalDependency& dep, std::string_view target); // What a configuration that declares `dep` compiles and links with once // BuildExternal has run: the -I flags and, for CMake, the -L, -rpath and -l // flags and the newest library. Per declaration, because two that share a // clone can still differ in includeDirs, libDirs and libs. ExternalBuildResult ExternalOutputs(const ExternalDependency& dep, std::string_view target); // The args the project file now running its CrafterBuildProject was // loaded with: everything that function receives, --target=, --debug and // the other standard flags included. Not cfg.projectArgs, which holds // only what ApplyStandardArgs left uninterpreted: `.args = // cfg.projectArgs` would drop the target and the build type. Empty // everywhere else, because it reads the stack LoadProject keeps per // thread, as IsTopLevelProject does: outside a project file, on any // thread but the one running it, once CrafterBuildProject has returned // (a script runs later), and before it is called, which covers the // file's static initialisers and its `#pragma crafter dependency` // pre-pass. The specs below default their args to it. Defined beside // LoadProject, but declared here so those defaults can see it: importing // :Platform into this partition would make the exported `struct // Configuration;` above a redeclaration of :Platform's unexported one, // which clang rejects. std::vector ProjectArgs(); // Specification for a sibling crafter-build project to fetch and depend on. // GitSource picks the revision: leave branch + commit empty for the // remote's default branch HEAD (sloppy but convenient), set branch to // track a branch tip, or set commit to pin to an immutable SHA. args are // what the dep's CrafterBuildProject receives. They default to this // project's own, read when the spec is made, so --target=, --march=, // --debug and the project's own flags reach every sibling down a chain // without being passed. Set them to forward something else: {} for // nothing, or a list built from this project's args, such as one without // a flag only this project reads. Give such a list to every sibling: // Build() refuses a graph in which two paths load one project with two // different lists, since the program would end up with both builds. struct GitProjectSpec { GitSource source; fs::path projectFile = "project.cpp"; std::vector args = ProjectArgs(); }; // Clones the spec's git URL into the external cache, recursively // LoadProject's the remote's project.cpp, and returns a stable // Configuration* you can drop into cfg.dependencies. The clone is keyed by // url+branch+commit+projectFile alone, so every arg list builds in the // same working tree, each into variant directories of its own. The // Configuration is owned by an internal cache for the lifetime of the // build, keyed by the clone and the args: identical specs share one. Configuration* GitProject(const GitProjectSpec& spec); // Specification for a local sibling crafter-build project (e.g. an // example folder depending on its parent project, or a workspace where // multiple projects live side-by-side without going through git). struct LocalProjectSpec { fs::path projectFile; // relative to cwd or absolute std::vector args = ProjectArgs(); // as GitProjectSpec::args }; // Same as GitProject but for a local on-disk project — no fetch, no // cache copy. Resolves projectFile to its canonical absolute path and // recursively LoadProject's it. Returns a stable Configuration* owned // by the same internal cache as GitProject. Configuration* LocalProject(const LocalProjectSpec& spec); // Clone (or update) the given git source into the shared external // cache and return the path to the cloned working tree. Intended for // example projects that need large asset bundles which shouldn't // live in the parent repo — push the returned path (or specific // files inside it) into `cfg.assets`. Reuses the same cache root as // ExternalDependency / GitProject. Caching key = url + branch + commit; // identical calls reuse the existing clone. Pin `source.commit` to a // SHA for reproducible, offline-after-first-fetch builds. fs::path GitFetch(const GitSource& source); } namespace Crafter { // Where GitProject or LocalProject loaded a Configuration from. The // loader keeps this beside the cache that owns the Configuration, since // the Configuration itself cannot say: only the one they return has its // path rebased, and the configurations a dependency's project file makes // for itself keep a path such as "./", which resolves against the // consumer's directory. struct LoadedProject { fs::path projectFile; // absolute // What a consumer's AGENTS.md calls it: the repository's name for a // git project, with the path below the clone's root for one that is // not at it, and the directory's name for a local one. The same on // every machine and for a --local checkout and its clone alike, // which a path into the cache or a sibling checkout is not, and free // of what a URL can carry, credentials included. std::string name; // Which library this is, whatever the revision: the repository and // the project file in it for git, the project file for a local one. // Two specs that pin one library differently are one library. std::string identity; }; // The LoadedProject behind a Configuration that GitProject or // LocalProject returned; nullopt for any other: the project crafter-build // was pointed at, and every configuration a project file makes itself. std::optional LoadedProjectOf(const Configuration& cfg); }