Skip to content

Commit 551d747

Browse files
committed
2026.9.27.1: a build.mcpp can declare a runtime library search directory, joining [runtime] library_dirs
`mcpp::runtime_library_dir(dir)` (`mcpp:runtime-library-dir=`, protocol 12) is the build-program form of `[runtime] library_dirs`: a launch-time search directory for a dependency a build.mcpp discovers (a vcpkg prefix's `bin/`, a Qt SDK's `bin/`) rather than one an author can write into a fixed TOML array. It joins the SAME field the manifest key populates (`RuntimeConfig::libraryDirs`), so every existing consumer of that field sees a directive-declared entry without any of them changing: `mcpp run`'s loader path, `mcpp pack`'s closure search, and the ELF/Mach-O `-Wl,-rpath` rendering. Persisted like `deploy`/`warning`/`pack-format` (non-empty cache tag, `kCacheEpoch` not bumped, same reasoning), and the root package's own build.mcpp needed the same manifest-snapshot mirror `deploy` already has in prepare.cppm, or `resolve_runtime_contract` never sees a directive-sourced entry. Adds unit coverage in test_build_directives.cpp and an e2e script (779) that links an executable against a prebuilt shared library only the directive's RUNPATH makes loadable, checks the build.mcpp cache-hit replay, and checks `mcpp pack --format dir`'s closure staging. Docs: docs/30-build-mcpp.md and docs/zh/30-build-mcpp.md gain the directive, the typed API entry and a worked section; docs/04-mcpp-toml.md and its zh counterpart point `[runtime] library_dirs` at the build-program form.
1 parent 3a6ac7f commit 551d747

12 files changed

Lines changed: 374 additions & 8 deletions

‎docs/04-mcpp-toml.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,11 @@ For one compatibility train, `library_dirs` maps only to runtime search,
14731473
`capabilities` maps to required run-phase capability requirements. None of
14741474
these legacy fields creates a provider.
14751475

1476+
`library_dirs` has a build-program form for a directory only a build.mcpp can
1477+
locate (a vcpkg prefix's `bin/`, a Qt SDK's `bin/`): `mcpp::runtime_library_dir(dir)`
1478+
(2026.9.27.1+, protocol 12; [30 — Build Programs](30-build-mcpp.md)), which
1479+
joins this same field.
1480+
14761481
`target/<triple>/<fp>/resolution.json` schema 2 stores the RuntimeBinding,
14771482
canonical requirements/providers/artifacts, LinkIntent, platform search
14781483
mechanism, and post-link verdict. `mcpp why runtime` is a pure interpreter of

‎docs/30-build-mcpp.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ is ignored, so diagnostics may be logged freely.
6767
| `mcpp:windows-subsystem=<target>:<value>` *(2026.9.12.2+)* | set the PE subsystem (`console` or `windows`) of the executable `<target>` of **this** package, the same field as `[targets.<target>] windows_subsystem` (docs/04). Reaches that target's link and no other, never a consumer, and renders nothing on a target that is not PE. A target the package does not declare with `kind = "bin"`, a value outside the set, and a value that contradicts mcpp.toml are each refused before any directive is applied |
6868
| `mcpp:windows-entry=<target>:<value>` *(2026.9.12.2+)* | set the entry function (`main`, `wmain`, `WinMain` or `wWinMain`) of the executable `<target>`, the same field as `windows_entry`; the scope and the refusals are those of `windows-subsystem` |
6969
| `mcpp:deploy=<from>\t<to>` *(2026.9.12.3+, protocol 11)* | place a file this program produced or selected beside the artifact, at `<to>`, relative to the executable's directory — the build-program form of `[runtime] deploy` (docs/04 §2.11). `<from>` may be absolute (an action's own declared output) or resolved against the package root; TAB-separated, because an absolute Windows `<from>` contains a colon. **Reaches the consumer**, joining the same `LinkIntent` `link-lib`/`link-search`/`link-flag` feed — see below |
70+
| `mcpp:runtime-library-dir=<dir>` *(2026.9.27.1+, protocol 12)* | add `<dir>` to the launch-time search path — the build-program form of `[runtime] library_dirs` (docs/04 §2.11). Relative resolves against the package root. **Reaches the consumer**, joining the same `LinkIntent` field the manifest key populates: RUNPATH/rpath on ELF and Mach-O, never `-L`, and `mcpp pack`'s closure search — see below |
7071
| `mcpp:link-script=<path>` *(2026.8.19+)* | link with this **linker script** (`-T`; relative resolves against the package root, and the emitted path is absolute because the link runs in the build directory). Reaches the **consumer**, unlike `include-dir` — a board's memory layout is the one thing a consumer cannot write for itself |
7172
| `mcpp:warning=<text>` *(2026.8.21.2+)* | say something to the user and **keep going**. The one directive that changes no compile line, no link line and no source set. Survives the build cache — see below |
7273
| `mcpp:fact=<name>=<version>` *(2026.9.5.2+)* | state something the program **established about the machine** (`cuda.driver=12.4`). Compared against floors before anything is compiled; see below |
@@ -134,6 +135,7 @@ int main() {
134135
| `mcpp::link_flag(s)` *(2026.9.6.5+)* | `mcpp:link-flag=` |
135136
| `mcpp::windows_subsystem(target, value)` / `mcpp::windows_entry(target, value)` *(2026.9.12.2+)* | `mcpp:windows-subsystem=` / `mcpp:windows-entry=` |
136137
| `mcpp::deploy(from, to)` *(2026.9.12.3+, protocol 11)* | `mcpp:deploy=<from>\t<to>` — see below |
138+
| `mcpp::runtime_library_dir(dir)` *(2026.9.27.1+, protocol 12)* | `mcpp:runtime-library-dir=<dir>` — see below |
137139
| `mcpp::link_script(p)` *(2026.8.19+)* | `mcpp:link-script=` |
138140
| `mcpp::runner(tok)` *(2026.8.19.2+)* | `mcpp:runner=` — see below |
139141
| `mcpp::xpkg_dir(ns, name)` / `mcpp::xpkg_dir(name)` *(2026.8.19+)* | the payload directory of a package declared in `[xlings.workspace]` — by this manifest, or by a dependency compiled into this build program *(2026.9.6.6+)*; `""` when it was not declared or is not installed (see below) |
@@ -656,6 +658,38 @@ int main() {
656658
| `.apk` | `assets/myapp.resources/` |
657659
| web | the same relative path in the static directory, served beside `<name>.js`. A project that wants the files inside the `.data` preload instead links with `--preload-file <dir>@/<to>`, an ordinary link flag |
658660

661+
### A launch-time search directory: `runtime_library_dir` (2026.9.27.1+, protocol 12)
662+
663+
`[runtime] library_dirs` (docs/04 §2.11) names a directory to search when the
664+
artifact runs. It is a fixed TOML array, so it cannot name a directory a
665+
build.mcpp only discovers — a vcpkg prefix's `bin/`, a Qt SDK's `bin/`, or any
666+
other prebuilt-dependency layout a build-time probe locates. `mcpp::runtime_library_dir`
667+
is that same declaration, reached from a build program:
668+
669+
```cpp
670+
import mcpp;
671+
#include <string>
672+
673+
int main() {
674+
const std::string qtBin = locate_qt_prefix() + "/bin"; // however this
675+
// package finds it
676+
mcpp::link_search(qtBin.c_str());
677+
mcpp::runtime_library_dir(qtBin.c_str());
678+
}
679+
```
680+
681+
- **Joins the same field the manifest key populates.** A directive-declared
682+
directory reaches every consumer of `[runtime] library_dirs` exactly as one
683+
written in `mcpp.toml` would: `mcpp run`'s loader path, `mcpp pack`'s closure
684+
search, and RUNPATH/rpath on ELF and Mach-O (never `-L` — a launch-time
685+
search directory is not a link-library search path).
686+
- **`dir` may be absolute or package-relative.** A relative value resolves
687+
against the package root, like every other `AbsPath` directive
688+
(`include-dir`, `deploy`'s `from`).
689+
- **Replayed on a cache hit.** A `runtime-library-dir` directive is persisted
690+
in the build cache like `deploy` and `warning`; a cached run restores it
691+
exactly as a fresh run would.
692+
659693
### Producing a distributable: `pack_format` / `stage_dir` (2026.9.11.1+)
660694

661695
An `.msi`, a `.deb`, an AppImage and a signed `.app` are none of the four roles'

‎docs/zh/04-mcpp-toml.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,6 +1405,11 @@ Link intent 把各个发现阶段分开处理:
14051405
映射到必需的运行期 soname 要求,`capabilities` 映射到必需的运行期
14061406
能力要求。这些遗留字段都不创建提供者。
14071407

1408+
`library_dirs` 有一个构建程序形态,用于一个只有 build.mcpp 才能定位的目录
1409+
(一个 vcpkg 前缀的 `bin/`、一个 Qt SDK 的 `bin/`):`mcpp::runtime_library_dir(dir)`
1410+
(2026.9.27.1+,protocol 12;[30 —— 构建程序](30-build-mcpp.md)),并入的是
1411+
同一个字段。
1412+
14081413
`target/<triple>/<fp>/resolution.json` schema 2 存储 RuntimeBinding、
14091414
规范化后的要求/提供者/产物、LinkIntent、平台发现机制与链接后判定。
14101415
`mcpp why runtime` 是对最新存储文件的一个纯粹解读器:它既不会重新

‎docs/zh/30-build-mcpp.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ mcpp build # compiles + runs build.mcpp, then builds the project
6464
| `mcpp:windows-subsystem=<target>:<value>` *(2026.9.12.2+)* | 设置**本包**可执行目标 `<target>` 的 PE 子系统(`console` 或 `windows`),与 `[targets.<target>] windows_subsystem`(docs/04)是同一字段。只到达该目标的链接,不到达其他目标或消费者,在非 PE 目标上不产生任何标志。本包未以 `kind = "bin"` 声明该目标、取值不在集合内、取值与 mcpp.toml 的声明矛盾,这三种情形都在应用任何指令之前被拒绝 |
6565
| `mcpp:windows-entry=<target>:<value>` *(2026.9.12.2+)* | 设置可执行目标 `<target>` 的入口函数(`main`、`wmain`、`WinMain` 或 `wWinMain`),与 `windows_entry` 是同一字段;作用域与拒绝条件同 `windows-subsystem` |
6666
| `mcpp:deploy=<from>\t<to>` *(2026.9.12.3+,protocol 11)* | 把本程序生成或选中的一个文件放到产物旁边的 `<to>`(相对可执行文件所在目录)——`[runtime] deploy`(docs/04 §2.11)的构建程序形态。`<from>` 可以是绝对路径(某个 action 自己声明的输出),也可以按包根解析;用 TAB 分隔,因为一个绝对的 Windows `<from>` 本身含冒号。**到达消费者**,并入同一个被 `link-lib`/`link-search`/`link-flag` 喂入的 `LinkIntent`——见下 |
67+
| `mcpp:runtime-library-dir=<dir>` *(2026.9.27.1+,protocol 12)* | 把 `<dir>` 加入启动期搜索路径——`[runtime] library_dirs`(docs/04 §2.11)的构建程序形态。相对路径按包根解析。**到达消费者**,并入清单键所填的同一个 `LinkIntent` 字段:在 ELF 与 Mach-O 上是 RUNPATH/rpath,绝不是 `-L`,并进入 `mcpp pack` 的闭包搜索——见下 |
6768
| `mcpp:link-script=<path>` *(2026.8.19+)* | 用这个**链接脚本**链接(`-T`;相对路径按包根解析,发出的是绝对路径,因为链接是在构建目录里跑的)。与 `include-dir` 不同,它**到达消费者** —— 板子的内存布局恰恰是消费者写不出来的那一项 |
6869
| `mcpp:warning=<text>` *(2026.8.21.2+)* | 对用户说一句话并**继续**。唯一一条不改变编译行、链接行与源码集的指令。它**穿过构建缓存** —— 见下 |
6970
| `mcpp:fact=<name>=<version>` *(2026.9.5.2+)* | 陈述程序**测得的机器事实**(`cuda.driver=12.4`)。在编译任何东西之前与 floor 比较;见下 |
@@ -123,6 +124,7 @@ int main() {
123124
| `mcpp::link_flag(s)` *(2026.9.6.5+)* | `mcpp:link-flag=` |
124125
| `mcpp::windows_subsystem(target, value)` / `mcpp::windows_entry(target, value)` *(2026.9.12.2+)* | `mcpp:windows-subsystem=` / `mcpp:windows-entry=` |
125126
| `mcpp::deploy(from, to)` *(2026.9.12.3+,protocol 11)* | `mcpp:deploy=<from>\t<to>` —— 见下 |
127+
| `mcpp::runtime_library_dir(dir)` *(2026.9.27.1+,protocol 12)* | `mcpp:runtime-library-dir=<dir>` —— 见下 |
126128
| `mcpp::link_script(p)` *(2026.8.19+)* | `mcpp:link-script=` |
127129
| `mcpp::runner(tok)` *(2026.8.19.2+)* | `mcpp:runner=` —— 见下 |
128130
| `mcpp::xpkg_dir(ns, name)` / `mcpp::xpkg_dir(name)` *(2026.8.19+)* | `[xlings.workspace]` 里声明的包的载荷目录 —— 本 manifest 声明的,或编进本构建程序的某个依赖声明的(2026.9.6.6+);没声明或没安装时返回 `""`(见下) |
@@ -559,6 +561,35 @@ int main() {
559561
| `.apk` | `assets/myapp.resources/` |
560562
| web | 静态目录里同一个相对路径,与 `<name>.js` 放在一起。想把文件放进 `.data` 预加载的项目改用链接标志 `--preload-file <dir>@/<to>`,那是一条普通的链接标志 |
561563

564+
### 一个启动期搜索目录:`runtime_library_dir`(2026.9.27.1+,protocol 12)
565+
566+
`[runtime] library_dirs`(docs/04 §2.11)声明产物运行时要搜索的一个目录。
567+
它是一个固定的 TOML 数组,因此点不了一个只有 build.mcpp 才能发现的目录——
568+
一个 vcpkg 前缀的 `bin/`、一个 Qt SDK 的 `bin/`,或任何其它由构建期探测
569+
才能定位的预编译依赖布局。`mcpp::runtime_library_dir` 就是同一次声明,从
570+
构建程序里发出:
571+
572+
```cpp
573+
import mcpp;
574+
#include <string>
575+
576+
int main() {
577+
const std::string qtBin = locate_qt_prefix() + "/bin"; // 本包自己
578+
// 怎么找到它
579+
mcpp::link_search(qtBin.c_str());
580+
mcpp::runtime_library_dir(qtBin.c_str());
581+
}
582+
```
583+
584+
- **并入清单键所填的同一个字段。** 一个由指令声明的目录,到达
585+
`[runtime] library_dirs` 的每一个消费者,与写在 `mcpp.toml` 里的完全一样:
586+
`mcpp run` 的加载器路径、`mcpp pack` 的闭包搜索,以及 ELF 与 Mach-O 上的
587+
RUNPATH/rpath(绝不是 `-L`——一个启动期搜索目录不是一个链接库搜索路径)。
588+
- **`dir` 可以是绝对路径或按包根解析的相对路径。** 相对路径按包根解析,
589+
与其它每一个 `AbsPath` 指令(`include-dir`、`deploy` 的 `from`)一样。
590+
- **在缓存命中时被重放。** `runtime-library-dir` 指令与 `deploy`、`warning`
591+
一样进入构建缓存;一次缓存命中的重跑会像真正跑过一样把它恢复回来。
592+
562593
### 产出可分发物:`pack_format` 与 `stage_dir`(2026.9.11.1+)
563594

564595
一个 `.msi`、一个 `.deb`、一个 AppImage、一个签过名的 `.app`,都不是那四个 role 的

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.9.26.1"
3+
version = "2026.9.27.1"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

‎modules/buildmcpp/src/directives.cppm‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@ enum class Slot : std::size_t {
164164
// splitting it into two slots would let a build with N deploy directives
165165
// pair them up wrong the moment N > 1.
166166
Deploy,
167+
// A LAUNCH-TIME SEARCH DIRECTORY, THE BUILD-PROGRAM FORM OF `[runtime]
168+
// library_dirs` (docs/04 §2.11). A dependency that brings a prebuilt
169+
// shared library (a vcpkg prefix's `bin/`, a Qt SDK's `bin/`) knows where
170+
// it lives only at build-program time, and the manifest key cannot be
171+
// computed -- it is a fixed TOML array. `Transform::AbsPath`, not a new
172+
// path shape: this is the SAME field the manifest key feeds
173+
// (`RuntimeConfig::libraryDirs`), so every consumer of it -- `mcpp run`'s
174+
// loader path, `mcpp pack`'s closure search, and the ELF/Mach-O
175+
// `-Wl,-rpath` rendering -- sees a directive-declared entry exactly as it
176+
// sees a TOML one, through the one merge in `plan.cppm` that already
177+
// exists for the manifest key. No new downstream code path.
178+
RuntimeLibraryDir,
167179
Count
168180
};
169181
inline constexpr std::size_t kSlotCount = static_cast<std::size_t>(Slot::Count);
@@ -262,7 +274,7 @@ struct Def {
262274
int sinceProtocol;
263275
};
264276

265-
inline constexpr std::array<Def, 26> kTable{{
277+
inline constexpr std::array<Def, 27> kTable{{
266278
// wire tag slot scope transform must missingPrefix missingSuffix since
267279
{"cxxflag", "cxxflag", Slot::CxxFlags, Scope::PackagePrivate, Transform::Verbatim, false, "", "", 1},
268280
{"cflag", "cflag", Slot::CFlags, Scope::PackagePrivate, Transform::Verbatim, false, "", "", 1},
@@ -413,6 +425,29 @@ inline constexpr std::array<Def, 26> kTable{{
413425
// before `apply` on both the run path and the cache-hit path, so a cached
414426
// replay refuses exactly what a fresh run would.
415427
{"deploy", "deploy", Slot::Deploy, Scope::LinkGlobal, Transform::Deploy, false, "", "", 11},
428+
// v12. The build-program form of `[runtime] library_dirs` (docs/04
429+
// §2.11): a launch-time search directory, for a dependency (a vcpkg
430+
// prefix's `bin/`, a Qt SDK's `bin/`) whose location a build.mcpp learns
431+
// rather than one an author can write into TOML. Scope::LinkGlobal,
432+
// exactly as `deploy` above, because `apply` folds it into
433+
// `RuntimeConfig::libraryDirs` -- the SAME field the manifest key
434+
// populates -- so `resolve_runtime_contract`'s per-package merge
435+
// (plan.cppm) carries it into a consumer's `linkIntent.runtimeSearchDirs`
436+
// through the one path that already exists for the manifest key, rather
437+
// than a second one this table would have to keep in sync. `mustExistAfterRun`
438+
// is FALSE: a directory is not the declared-output contract's shape
439+
// (`generated=`/`source=` name a file), and the directory need not even
440+
// exist yet -- a payload not installed on this machine is the manifest
441+
// key's own behaviour (`xpkg_dir()` returning empty configures no runner,
442+
// silently and correctly).
443+
//
444+
// kCacheEpoch is NOT bumped, the same reasoning `warning`/`pack-format`
445+
// state explicitly: an entry written before this row carries no
446+
// `d runtime-library-dir` line, and the program that wrote it could not
447+
// emit one -- so replaying it yields exactly what that program said, and
448+
// the entry is still correct. An older engine reading a newer entry
449+
// already discards the whole record through the unknown-tag path.
450+
{"runtime-library-dir", "runtime-library-dir", Slot::RuntimeLibraryDir, Scope::LinkGlobal, Transform::AbsPath, false, "", "", 12},
416451
}};
417452

418453
// ── Collected output of one run ────────────────────────────────────────────
@@ -945,6 +980,14 @@ void apply(mcpp::manifest::Manifest& m, const Directives& d) {
945980
for (auto const& p : d.at(Slot::IncludeDirsAfter))
946981
bc.includeDirsAfter.emplace_back(p);
947982

983+
// `runtime-library-dir`: already absolute from the AbsPath
984+
// transform, like IncludeDirs above. Joins `RuntimeConfig::libraryDirs`,
985+
// the exact field `[runtime] library_dirs` populates from TOML -- so a
986+
// consumer sees it exactly as it sees the manifest key, folded in by the
987+
// SAME merge (plan.cppm's `resolve_runtime_contract`), never a second one.
988+
for (auto const& p : d.at(Slot::RuntimeLibraryDir))
989+
m.runtimeConfig.libraryDirs.emplace_back(p);
990+
948991
// Claims join the runtime declarations the manifest could have carried
949992
// itself, so the version-floor check in prepare reads ONE list and never
950993
// learns which spelling a claim arrived in.

‎modules/buildmcpp/src/program_protocol.cppm‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,14 @@ export namespace mcpp::build::program_protocol {
8181
// at a path relative to the executable. Same cost as v5's: a package calling
8282
// `mcpp::deploy()` fails on an older engine at the build.mcpp COMPILE, because
8383
// that engine's bundled module has no such function.
84-
inline constexpr int kProtocolVersion = 11;
84+
// v12: adds `runtime-library-dir` -- the build-program form of `[runtime]
85+
// library_dirs`: a launch-time search directory for a dependency (a vcpkg
86+
// prefix's `bin/`, a Qt SDK's `bin/`) whose location a build.mcpp learns
87+
// rather than one an author can write into TOML. Same cost as v5's: a
88+
// package calling `mcpp::runtime_library_dir()` fails on an older engine at
89+
// the build.mcpp COMPILE, because that engine's bundled module has no such
90+
// function.
91+
inline constexpr int kProtocolVersion = 12;
8592

8693
// ── Cache-format epoch ─────────────────────────────────────────────────────
8794
//

‎modules/versioning/src/version.cppm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ import std;
3131

3232
export namespace mcpp {
3333

34-
inline constexpr std::string_view MCPP_VERSION = "2026.9.26.1";
34+
inline constexpr std::string_view MCPP_VERSION = "2026.9.27.1";
3535

3636
} // namespace mcpp

‎src/build/hostprogram.cppm‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,17 @@ inline void windows_entry(const char* target, const char* value) {
183183
inline void deploy(const char* from, const char* to) {
184184
std::printf("mcpp:deploy=%s\t%s\n", from, to);
185185
}
186+
// The build-program form of `[runtime] library_dirs` (docs/04 §2.11): a
187+
// directory to search at LAUNCH time, for a dependency (a vcpkg prefix's
188+
// `bin/`, a Qt SDK's `bin/`) whose location this program learns rather than
189+
// one an author can write into `mcpp.toml`. Reaches the consumer, joining the
190+
// same `LinkIntent` `link_lib`/`link_search`/`deploy` feed, and gets the same
191+
// treatment the manifest key does: RUNPATH/rpath on ELF and Mach-O, never
192+
// `-L`, and `mcpp pack`'s closure search. Relative paths resolve against this
193+
// package's root, like every other AbsPath directive.
194+
inline void runtime_library_dir(const char* dir) {
195+
std::printf("mcpp:runtime-library-dir=%s\n", dir);
196+
}
186197
// ── Build-graph nodes (mcpp 2026.8.5.1+) ────────────────────────────────
187198
// Declare WORK instead of doing it. A build program is a good place to decide
188199
// what the build looks like and a bad place to perform it: work done here is

0 commit comments

Comments
 (0)