Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ bench-child.log
# to be expanded (`> $binDir`) and was not. Listed so the same slip is caught
# next time rather than reviewed again.
binDir
examples/*/target/
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,50 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [2026.8.24.3] — 2026-08-24

### 修复

- **⚠️ 三元组是请求,而解析把「未指定」抹掉了。**

```
$ mcpp build --target x86_64-linux # 我写的是「不指定 C 库」
Target x86_64-linux-gnu → x86_64-unknown-linux-gnu ← 被改写
c-abi musl (openkal-musl@0.3.3, graph) ← 名字自相矛盾
```

三元组同时充当**身份**(输出目录、缓存键、`cfg()` 的主语)与**请求**。
身份必须是全的,请求必须能说「没指定」;`parse` 用自动填充让身份变全,
代价是请求消失 —— 两态在下游不可区分。

⚠️ 修法是窄的:保留填充,另记 `Triple::envExplicit`。而请求**必须在规范化
之前捕获** —— `str()` 渲染的是填好的身份,之后再 parse 就分不出来了。

未指定 ⇒ 报告显示工程写的那个拼写;写了且与图矛盾 ⇒ **拒绝**。

- **⚠️ 目标行的约定在图之前就被应用,而它要回答的问题在图之后才有答案。**

`x86_64-linux-musl → gcc@16.1.0` 说的不是「偏好 gcc」,是「musl-gcc 载荷
供给这个目标的 C 库」。工程的 C 库若来自依赖图,该载荷根本不被使用。

⚠️ 早决定被**双向实测**否掉:无条件应用会替换用户用 `mcpp toolchain default`
设下的工具链;不应用会让一个零依赖的交叉构建从可用变为不可用。

⭐ 判据换成它本来就该是的那个:**图供给 `kernel-abi` 或 `c-abi` 时,约定不适用。**
工具链解析因此移到依赖解析之后。

⚠️ 代码不搬,只搬执行时机 —— 原地包成 lambda,在图已知处调用。
先前记录的「39 处读写挡着」是**没测就写下的**:实测依赖解析段读 `tc` 仅 1 处,
而那一处要的是三元组不是编译器。

实测三格:

| 场景 | 结果 |
|---|---|
| openkal 工程 + 全局 llvm 默认 + `--target x86_64-linux-musl` | `Resolved llvm@22.1.8` ✅ |
| 无依赖工程 + 同一目标 | `Resolved gcc@16.1.0`(约定生效)✅ |
| 无依赖工程 + `x86_64-windows-gnu` | `Resolved gcc@16.1.0` ✅ |

## [2026.8.24.2] — 2026-08-24

### 新增
Expand Down
11 changes: 9 additions & 2 deletions docs/14-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ interface. The environment field states a request for a C library; it is a
request rather than the answer, and the resolved value is reported by the
build.

Omitting the field declines to state one: `x86_64-linux` asks for whatever
supplies that layer, and `x86_64-linux-musl` asks for musl. When the dependency
graph supplies a different one the graph decides, and the build reports that the
name is inaccurate together with the spelling to use instead. The request is
ignored rather than violated, so the artifact is the same either way.

### The Toolchain

`mcpp toolchain default <family>@<version>`, `[toolchain]` in the manifest, or
Expand All @@ -128,8 +134,9 @@ layer, which is the one layer no package may supply.

A target row may carry a convention — a toolchain whose payload supplies that
target's C library. The convention applies when the manifest states nothing for
that target. When it replaces a default set with `mcpp toolchain default`, the
status line reports the substitution and names the one-line override.
that target AND nothing in the dependency graph supplies the target's system.
The second condition is knowable only after resolution, so the toolchain is
resolved there rather than before it.

### Dependencies

Expand Down
250 changes: 250 additions & 0 deletions docs/15-openkal-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# Cross-Compilation Over openkal

Conventional cross-compilation is served by a payload. A toolchain is built for
one target, its driver has exactly one answer, and reaching a second target
means obtaining a second toolchain. The number of payloads a distribution must
publish is therefore the number of host-target pairs it supports.

openkal changes what is being crossed. The target side — the platform interface,
the C library, the compiler runtime and the C++ runtime — becomes a set of
packages resolved from the dependency graph and compiled from source by whichever
compiler is running. What remains for the compiler is code generation, and one
Clang binary emits every object format it was built with.

This document states the model, what a project writes, what the ecosystem
supplies, and the limits that have been measured.

## The Claim

An ecosystem of N platforms and M architectures requires N implementations of
one interface rather than N×M toolchains. The count follows from where the
target side lives: a package built from source is built for whatever target the
compiler is asked to emit, so a platform implementation is written once and
reaches every architecture the compiler supports.

The claim is verified by a matrix of three hosts and three targets, each cell
building one source and running the result.

## What A Project Writes

```toml
[dependencies]
openkal-llvm-runtime = "0.1.1"

[toolchain]
default = "llvm@22.1.8"
```

Two lines. The first selects three layers of the target side; the second names
a compiler and says nothing about where anything else comes from.

Targets are given on the command line:

```bash
mcpp build --target x86_64-linux
mcpp build --target aarch64-macos
mcpp build --target x86_64-windows-gnu
```

No `[target.<triple>]` section is required for a hosted target, and no
preprocessor directive is required in the source. A worked example is
[examples/06-openkal-cross](../examples/06-openkal-cross).

## What The Ecosystem Supplies

| Package | Layer | Content |
|---|---|---|
| `openkal` | — | the specification, and the C++ modules that declare it |
| `openkal-linux` | `kernel-abi` | the reference implementation, on Linux system calls |
| `openkal-macos` | `kernel-abi` | on the macOS system-call surface |
| `openkal-windows` | `kernel-abi` | on Win32 and the object manager, using no C runtime symbol |
| `openkal-opensbi` | `kernel-abi` | on the RISC-V Supervisor Binary Interface, no operating system |
| `openkal-uefi` | `kernel-abi` | on UEFI Boot Services, before an operating system exists |
| `openkal-musl` | `c-abi` | musl redirected onto openkal, ported once |
| `openkal-llvm-runtime` | `compiler-runtime`, `c++-abi` | compiler-rt builtins, libunwind, libc++abi and libc++, configured for openkal-musl |

A project names the last of these. The others follow from its dependencies.

## Why The Compiler Must Be LLVM

`openkal-llvm-runtime` declares the requirement rather than leaving it to be
discovered:

```toml
requires = ["mcpp:compiler=llvm"]
```

Its sources are libc++'s, and its `std` module source in particular is compiled
by Clang. Handing that source to GCC fails inside libc++'s own headers, in a
message naming a file the reader has never opened:

```
fatal error: __config: No such file or directory
```

With the requirement declared, the build refuses the combination before it
compiles anything, and names the command that selects a compiler which satisfies
it.

## How The Target Is Chosen

The target row of mcpp's own vocabulary may carry a toolchain convention. That
convention names the payload which supplies **that target's C library**, and it
applies only when two conditions hold: the manifest states nothing for the
target, and nothing in the dependency graph supplies the target's system.

The second condition is knowable only after the graph is resolved. A project
whose C library comes from `openkal-musl` therefore keeps the compiler it asked
for, while a project with no dependencies still receives the payload the row
names. Both behaviours were measured; deciding either way in advance was wrong
for the other.

## The Environment Segment

On Linux the third segment of a target triple names the C library. Under openkal
the C library comes from the graph, so a triple that names one states a request
the graph may not honour:

```
mcpp build --target x86_64-linux-gnu # asks for glibc
c-abi musl (openkal-musl@0.3.3, graph)
```

The graph decides. Omitting the segment states no request and produces the same
artifact:

```
mcpp build --target x86_64-linux
```

The build reports the mismatch when the segment is present and disagrees. It is
a report rather than a refusal, because the segment is ignored rather than
violated. Measured on one host, `x86_64-linux` against `x86_64-linux-musl`: the
two executables differ, and after stripping they are byte-identical. What
differs is the debug information, which records the output directory, and the
directory is named after the triple. The code is the same code.

On Windows the same segment names the object ABI instead — `gnu` for PE with the
GNU ABI, `msvc` for PE with Microsoft's — and both are compatible with more than
one C library. The mismatch report is therefore scoped to platforms where the
segment names a C library.

## Bare Metal

A target with no operating system is the same model with the platform layer
supplied by firmware rather than by a kernel. `riscv64-none-elf` over OpenSBI
runs the same source as a hosted target, including `import std`, because the
standard library it uses is the one the graph supplied rather than the
compiler's own.

Two things must be declared, both properties of the board rather than defaults:

```toml
[target.riscv64-none-elf]
sysroot = ""
runner = ["qemu-system-riscv64", "-machine", "virt", "-nographic",
"-no-reboot", "-bios", "default", "-kernel"]
```

`sysroot = ""` selects the zero-libc tier. Which machine model and which
firmware mode to use are board facts, and an engine that guesses one is an
engine a different board has to fight.

### The Source Is The Same, The Program Is Not

"The same source" is a claim about the toolchain and the standard library, and
it holds: `import std` works, the C++ runtime is the one the graph supplied, and
no `#if` distinguishes the targets. It is not a claim that any given program
builds for any given target, and the specification is explicit about why.

A bare-metal backend provides some interfaces and not others. `openkal-opensbi`
provides `abort`, `stream`, `memory`, `env` and `time`; it provides no
filesystem and no tasks, because the machine has none. Clause 6.1 makes that
absence a link-time fact:

> An interface that an implementation does not provide is absent as a link-time
> definition, and a consumer that uses it fails to link.

A capability word therefore answers a narrower question than it first appears
to. It says how an implementation behaves *within an interface it provides* —
whether names are compared case-sensitively, what the granularity of a clock is.
Whether the interface exists at all is answered before that, by the dependency
graph, and failing that by the linker.

The distinction is easy to lose, because the query is an inline function over a
data object, so a program that merely asks whether a filesystem exists takes the
address of `kal_fs_props` and fails to link with no filesystem call anywhere in
it. Defining that word as zero in the backend removes the error and is the one
remedy the clause forbids: the program then proceeds past the point the linker
existed to stop it at. It was tried, published as `openkal-opensbi@0.1.3`, and
retracted.

### Two Routes To A Bare x86_64 Machine

An x86_64 machine with no operating system is reached in two different ways, and
the difference is what loads the program.

| Route | Target | Platform layer | Entry |
|---|---|---|---|
| UEFI application | `x86_64-windows-gnu` | `openkal-uefi` | firmware, with Boot Services available |
| Kernel, or raw bare metal | `x86_64-none-elf` | none, or `openarch` | the reset vector, with nothing beneath |

A UEFI application is PE/COFF entered through the Microsoft x64 calling
convention. Both are properties the LLVM toolchain already has, so its target is
the same triple as a Windows program and firmware function pointers are called
directly. What distinguishes it from a Windows build is which implementation of
the platform interface the graph resolved, together with three link flags that
select `IMAGE_SUBSYSTEM_EFI_APPLICATION`.

A kernel has no firmware services to call. Its target is `x86_64-none-elf`, the
zero-libc tier: no C library on the compile line, no library directory on the
link, and `#include <stdio.h>` does not resolve. The program is entered at its
own `_start` and reaches hardware directly.

`openarch` is the layer such a program builds on. It is not a platform interface
and does not answer to `mcpp:kernel-abi`; it is the architecture mechanism —
execution contexts, traps, per-CPU state and address spaces — presented as one
interface over several instruction sets, with a backend package per instruction
set. A kernel depends on it and supplies its own platform layer, or none.

### Why x86_64 Bare Metal Required Engine Work

`riscv64-none-elf` and `aarch64-none-elf` are rows in a table and nothing more:
Clang has a BareMetal toolchain for both, drives their links itself and reaches
`ld.lld`. It has none for x86_64, so that triple falls through to the generic
GCC toolchain, whose linker is the host's `g++`:

```
g++: error: unrecognized command-line option '-fuse-ld=…/ld.lld'
```

Measured for every spelling of a bare x86_64 triple, and not correctable by any
flag. The row therefore carries a linker emulation and mcpp invokes `ld.lld`
itself, which is also why the host toolchain must be shown not to participate in
such a link.

## Measured Limits

Three, recorded because each was found by building rather than by reading.

**A backend must define every capability word.** The specification's queries are
inline functions over property objects, so a program that merely asks whether a
filesystem exists takes the address of `kal_fs_props`. A backend that omits the
words for layers it lacks makes the question fail to link on exactly the class of
machine the question exists for.

**Two suppliers of one layer is an error rather than a choice.** A C library, a
platform interface and a C++ runtime are mutually exclusive. Selecting the wrong
one does not fail the link; it produces a program that runs and intermittently
does not.

**A payload's C++ runtime cannot sit above a foreign C library.** Its
`__config_site` records the configuration it was built with. The resolver's
structure prevents the combination on the default path, and a diagnostic covers
the paths where a project overrides the contract explicitly.

## Reference

[docs/14 — The Target Side](14-target-side.md) for the five layers, the four
origins and the rules. [SPEC-002](spec/target-side.md) for the normative
statement of the capability grammar.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [12 - Distributing a Prebuilt Library](12-binary-distribution.md)
- [13 - Bare-Metal and Freestanding Targets](13-baremetal.md)
- [14 - The Target Side](14-target-side.md)
- [15 - Cross-Compilation Over openkal](15-openkal-cross.md)

## Specifications

Expand Down
26 changes: 26 additions & 0 deletions docs/spec/target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,32 @@ mcpp:<层名>[=<实现名>]
⚠️ 当前实现覆盖「两层均来自图」与「两层均来自载荷」。
「一层预制、一层来自图」的接线尚不完整。

### 3.4 规则四:三元组是请求 ✅ 已实现

三元组的 env 段**必须**被当作对 `c-abi` 的请求,而非其答案。

- 段缺席 ⇒ 未作陈述,任何供给者都不与之矛盾;
- 段存在且与解析出的 `c-abi` 不同,且后者来自图 ⇒ 引擎**必须**报出该不一致,
并**必须**给出不含该段的目标拼写;**禁止**据此使构建失败。

⚠️ 拒绝曾被实现并被实测否掉:它打破了每一个把宿主目标拼作 `x86_64-linux-gnu`
的工程与 CI 配置 —— 而那正是 `mcpp toolchain list` 打印的拼写。
判据是该请求**不改变任何东西**:图两种写法下都供给同一个 C 库,
因此该段是被忽略而非被违反。

⚠️ 规范化会把 `x86_64-linux` 写成 `x86_64-linux-gnu`,因为身份必须是全的。
请求**必须**在规范化之前捕获;报告**应当**显示工程书写的拼写。

### 3.5 目标表的约定何时生效 ✅ 已实现

目标行的 `pin` 命名的是**供给该目标 C 库的载荷**,不是偏好的编译器。
它**必须**仅在两个条件同时成立时生效:清单对该目标未作陈述,
且依赖图中无人供给 `kernel-abi` 或 `c-abi`。

⚠️ 第二个条件在依赖解析之后才可知,因此工具链**必须**在其之后解析。
过早决定被双向实测否掉:无条件应用会替换用户用 `mcpp toolchain default`
设下的工具链;不应用会让一个零依赖的交叉构建从可用变为不可用。

---

## 4. 报告
Expand Down
10 changes: 8 additions & 2 deletions docs/zh/14-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,21 @@ C 库、平台接口与 C++ 运行时是互斥的选择,而非可叠加的贡献
`--target <三元组>`,或 `[build] target`。OS 段选择平台接口。
env 段陈述一条对 C 库的请求;它是请求而非答案,解析出的值由构建报告。

省略该段即为不陈述:`x86_64-linux` 请求「供给该层的任何实现」,
`x86_64-linux-musl` 请求 musl。依赖图供给了另一个时以图为准,
构建会报出该名字不准确并给出应当使用的拼写。该请求是被忽略而非被违反,
因此产物两种写法下相同。

### 工具链

`mcpp toolchain default <族>@<版本>`、清单中的 `[toolchain]`,
或针对单一目标的 `[target.<三元组>].toolchain`。它选择 `compiler` 层 ——
唯一一个任何包都不能供给的层。

目标表的行可以携带一条约定,即其载荷供给该目标 C 库的工具链。
该约定在清单对该目标未作陈述时生效。当它替换了由 `mcpp toolchain default`
设定的默认时,状态行报出该替换并给出一行覆盖写法。
该约定在两个条件同时成立时生效:清单对该目标未作陈述,**且**依赖图中没有任何
东西供给该目标的系统。第二个条件只有在解析之后才可知,
因此工具链在那之后解析,而不在那之前。

### 依赖

Expand Down
Loading
Loading