Crafter.Build/interfaces/Crafter.Build-External.cppm
Jorijn van der Graaf 060ab4380f
fix: give a project file its args only for the CrafterBuildProject call
ProjectArgs() read args that LoadProject recorded when it pushed the
frame, so they were there for everything that ran on that frame, not
only for CrafterBuildProject. Two things went wrong with that.

The pragma pre-pass runs on the frame too, so both of its specs needed
a hand-written `.args = {}` to keep the consumer's args from reaching
host tooling, and anything else built there would have needed one as
well. And dlopen runs a project file's static initialisers only on the
first load of that file: a later load gets the same mapping back. A
spec made at namespace scope therefore kept the first load's args for
every later load of the file, a silently wrong variant.

The frame now gets the args just before CrafterBuildProject is called.
The pre-pass and the static initialisers see none, on every load, so
the two opt-outs go. The dependencies the pre-pass resolved move the
other way, to just before dlopen: they are the same on every load, and
ProjectDependency() called from a static initialiser used to find none
and terminate the process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:11:41 +01:00

136 lines
7.2 KiB
C++

// 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<std::string> options;
std::vector<fs::path> 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<fs::path> libDirs;
std::vector<std::string> libs;
};
struct ExternalBuildResult {
std::vector<std::string> compileFlags;
std::vector<std::string> 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<std::string> 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<std::string> 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, so --target=, --march=, --debug and flags of its own
// such as --local reach a whole chain of sibling projects without being
// passed. Set them to forward something else: a copy without one of this
// project's flags, one with an extra --target=, or {} for nothing.
struct GitProjectSpec {
GitSource source;
fs::path projectFile = "project.cpp";
std::vector<std::string> args = ProjectArgs();
};
// Clones the spec's git URL into the per-project external cache (keyed by
// url+branch+commit+args+projectFile), recursively LoadProject's the
// remote's project.cpp, and returns a stable Configuration* you can drop
// into cfg.dependencies. The Configuration is owned by an internal cache
// for the lifetime of the build; identical specs share one Configuration.
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<std::string> 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);
}