feat: project.cpp declares its own dependencies with #pragma crafter dependency

A project.cpp could only import std and Crafter.Build, so anything shared
between build descriptions had to be a header every consumer copies. Now

    #pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]

names a sibling crafter-build project that project.cpp itself imports a
module from. LoadProject scans for the directive, resolves it through
GitProject (a URL) or LocalProject (a path relative to the project file),
forces the result into host-tooling mode (the host triple LoadProject's
own cache is keyed on, native march, no debug, -fPIC so the archive links
into a shared object), builds it, and puts its BMIs and archive on the
project.so command line. The dependency's own project.cpp goes through
the same loader, so its directives resolve recursively; a cycle between
project files is an error instead of unbounded recursion.

The dependency is built on every load, fresh or not: clang crashes rather
than erroring when it imports a BMI whose own imported PCM was rebuilt
underneath it, and the module depfile — which Module::Check already reads —
is what notices the host PCM cache was regenerated. Its artifact mtime is
an input to project.so's staleness check, so editing the dependency
recompiles project.cpp without touching it. The Linux link gains
-fuse-ld=lld because release archives hold ThinLTO bitcode.

ProjectDependency(name) hands the same Configuration* back from inside
CrafterBuildProject, by configuration name or exported module name, so a
test that links the module needn't spell the source twice. It reads a
thread_local frame stack that an RAII guard keeps for the whole of
LoadProject, which also provides the cycle check.

ExpectedOutput() is the link gate's artifact-path lambda hoisted into the
:Clang partition, so the staleness check and the -r runner share the one
rule. Windows branches are wired identically but unverified.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-02 20:03:22 +02:00
commit b94139b67c
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
15 changed files with 618 additions and 45 deletions

View file

@ -102,6 +102,38 @@ Clones live at `~/.cache/crafter.build/external/<name>-<hash>/`, keyed by `(url,
**Header-only git deps** — same as cmake deps but `builder = ExternalBuilder::None`. Just clones and propagates `-I` flags.
**Sibling crafter-build projects** — `GitProject` / `LocalProject` load another project's `project.cpp` in-process and return a `Configuration*` for `cfg.dependencies`:
```cpp
Configuration* math = GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" }, // .branch or .commit to pin
.args = depArgs, // forwarded to its CrafterBuildProject
});
Configuration* sibling = LocalProject({ .projectFile = "../Crafter.Math/project.cpp", .args = depArgs });
cfg.dependencies = { math };
```
Clones share the external cache and identical specs share one `Configuration`. An unpinned source is `git pull`ed on every invocation; set `.commit` for reproducible, offline-after-first-fetch builds.
### Dependencies of project.cpp itself
`project.cpp` can import modules from another crafter-build project. The file has to compile before it can say anything, so the dependency is a directive next to the `import` it enables:
```cpp
import std;
import Crafter.Build;
#pragma crafter dependency "https://forgejo.catcrafts.net/Catcrafts/Crafter.LintRules.git"
import Crafter.LintRules;
```
```
#pragma crafter dependency "<source>" [branch "<name>" | commit "<sha>"] [project "<file>"]
```
A source containing `://` (or starting with `git@`) is cloned like `GitProject`; anything else is a path relative to the project file, like `LocalProject` (a directory means its `project.cpp`; `project` names another file in a repo). Before `project.cpp` is compiled, each dependency is fetched, built for the host, and put on the module path and link line. Its own `project.cpp` goes through the same loader, so its directives resolve recursively, and a rebuilt dependency recompiles `project.cpp` without anyone touching it. Inside `CrafterBuildProject`, `ProjectDependency("<module or configuration name>")` returns the same `Configuration*`, for a test that links the module too.
Pin with `commit` for the same reason as above. `crafter-build clean` leaves the dependency's build alone (it lives with the clone). The directive is read textually, so one inside a block comment still counts. A cycle between project files is an error. Windows hosts are wired the same way but unverified.
## Install layout
```