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

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion .agents/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
---
```

342 records.
343 records.

## By subject

Expand Down Expand Up @@ -69,6 +69,10 @@ Records that declare one. Everything else is listed by date below.
- [生态设计 part 2:openkal 的最小原子与二进制映像、其上的 `mcpplibs.os`,以及 part 1 的收尾与回归记录](2026-10-26-ecosystem-part2-os-library-over-openkal-design.md) — active
- [控制契约、并发与性能、单一 C++ 运行时,以及 IDE 生态(生态级,#838、#839)](2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md) — active

### editors

- [生态设计 part 2:mcpp 插件与 mcppls 的分层、入口、测试、版本感知与分发](2026-10-30-ecosystem-part2-ide-and-language-service-design.md) — active

### heterogeneous

- [A dlopen surface no closure walks, and a process with two unwinders](2026-09-09-dlopen-surface-and-two-unwinders.md) — landed
Expand Down Expand Up @@ -151,6 +155,7 @@ Records that declare one. Everything else is listed by date below.

### 2026-10

- [生态设计 part 2:mcpp 插件与 mcppls 的分层、入口、测试、版本感知与分发](2026-10-30-ecosystem-part2-ide-and-language-service-design.md) — active
- [另一架构的 ok 映像的运行、测试与打包,以及 ok 映像与 Web 的关系(2026.10.29.1)](2026-10-29-openkal-run-test-pack-design.md) — active
- [生态设计:目标集合与腿、打包模型 rev、运行覆盖、不回落、外部构建接入、mcppls 单包与编辑器](2026-10-29-ecosystem-target-sets-packaging-and-editors-design.md) — active
- [平台 part 2:打包与分发——三维模型、通用包、单文件、okb、体积,以及 openkal 0.17.2 轮](2026-10-28-platforms-part2-packaging-and-distribution-design.md) — landed
Expand Down
49 changes: 49 additions & 0 deletions .github/tools/release_canaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
release_canaries.py matrix the GitHub Actions matrix, as `matrix=<json>`
release_canaries.py unpin <checkout> remove the project's own mcpp pin from its checkout
release_canaries.py run <name> run a canary's commands in the current directory
release_canaries.py steps [<dir>] the longest build steps of the builds under <dir>

`run` reads the candidate from `$MCPP`. Each command runs under bash with
`set -eo pipefail`, so a failure is the command's own; a command listed under
Expand All @@ -16,6 +17,11 @@
launcher, because the loader searches the system directory before PATH (the
first release run of these canaries printed "Windows Subsystem for Linux has
no installed distributions" for every command).

After its commands, `run` prints the longest steps (compile or link, and the
output) of every build the canary made, read from each `.ninja_log` beneath the
current directory: a canary's time is mostly its project's build, and the steps
say where (design 2026-10-30 part 2 §11).
"""
from __future__ import annotations

Expand Down Expand Up @@ -79,6 +85,46 @@ def shell_argv(command: str) -> list[str]:
return [shell, "-c", f"set -eo pipefail\n{command}"]


def longest_steps(root: Path, count: int = 20) -> list[tuple[int, str, str]]:
"""(milliseconds, build directory, output) of the longest steps in every
`.ninja_log` beneath `root`. A log holds every build of its directory; the
last line for an output is its latest step."""
steps: list[tuple[int, str, str]] = []
for log in root.rglob(".ninja_log"):
latest: dict[str, int] = {}
try:
lines = log.read_text(encoding="utf-8", errors="replace").splitlines()
except OSError:
continue
for line in lines:
if line.startswith("#"):
continue
fields = line.split("\t")
if len(fields) < 4 or not fields[0].isdigit() or not fields[1].isdigit():
continue
latest[fields[3]] = int(fields[1]) - int(fields[0])
where = str(log.parent.relative_to(root)) if log.parent != root else "."
steps += [(ms, where, out) for out, ms in latest.items()]
steps.sort(key=lambda s: s[0], reverse=True)
return steps[:count]


def print_steps(root: Path) -> None:
steps = longest_steps(root)
if not steps:
print(f"no .ninja_log beneath {root}")
return
print("::group::the longest build steps", flush=True)
for ms, where, out in steps:
print(f"{ms / 1000:9.1f} s {where} {out}")
print("::endgroup::", flush=True)


def cmd_steps(root: str) -> int:
print_steps(Path(root).resolve())
return 0


def cmd_run(name: str) -> int:
mcpp = os.environ.get("MCPP", "")
if not mcpp:
Expand All @@ -103,6 +149,7 @@ def cmd_run(name: str) -> int:
want = expect.get(command)
if want and want not in proc.stdout + proc.stderr:
failed.append(f"`{command}` did not print `{want}`")
print_steps(Path.cwd())
for f in failed:
print(f"::error::canary {name}: {f}")
print(f"canary {name}: {len(c['commands']) - len(failed)} of {len(c['commands'])} command(s) held")
Expand All @@ -119,6 +166,8 @@ def main(argv: list[str]) -> int:
return cmd_unpin(argv[1])
if argv[0] == "run" and len(argv) == 2:
return cmd_run(argv[1])
if argv[0] == "steps" and len(argv) <= 2:
return cmd_steps(argv[1] if len(argv) == 2 else ".")
print(__doc__)
return 2

Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/cross-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,17 @@ jobs:
grep -qxF "PASS: 988 the Windows launcher finds its package from a Windows path" "$log" \
|| { echo "::error::988 did not print its PASS line"; exit 1; }

# A polyglot file (SPEC-014 §5.3) is the Windows program as it is: its
# criterion H runs the file under wine, which only this job has.
- name: "e2e: a polyglot file runs on every system it holds (992)"
run: |
set -o pipefail
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
log="$RUNNER_TEMP/992.log"
bash tests/e2e/992_a_polyglot_file_runs_on_every_system_it_holds.sh 2>&1 | tee "$log"
grep -qxF "ok: the file is a Windows program" "$log" \
|| { echo "::error::992 did not run the file under wine"; exit 1; }

# 257 needs a Linux-hosted MinGW compiler AND wine, which only this job
# has; on the shards it skipped, and it had run on no runner (finding F9
# of the 2026-10-02 CI record).
Expand Down
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,61 @@
> Each `## [<version>]` section is that release's notes. Entries are written in English
> from 2026.9.28.3 on; earlier entries remain as written.

## [2026.10.31.1] - 2026-10-31

### Added

- **`features` in `mcpp --protocol-version`.** A client gates on a feature
rather than on a version number: `target-sets` and `pkg-format-list` name the
meanings 2026.10.30.1 introduced. A name is added when a client gates on it,
and is never renamed or removed (docs/50 §4). Every envelope's `mcpp` field
carries the same list beside the version, so one `self env` gives a client
both, and a job's envelope says when either has changed.
- **`program` in `mcpp self env --format json`**: the file that is running. A
tool that ran `mcpp` through a version manager's shim in a project's directory
reads which mcpp the shim chose for that project. docs/50 states how a tool
runs mcpp for a project: in its root, by the name on `PATH` or a set path,
with `XLINGS_PROJECT_DIR` removed (design 2026-10-30 part 2 §4.0).

- **One file for several systems: `--pkg-format polyglot`** (SPEC-014 §5.3,
#849). An ok image's single files of every leg and system are assembled into
one `<name>-<version>.exe`: the Windows program leads with a POSIX shell
script in its DOS header, the other systems' programs follow unchanged.
Windows starts it as itself (Authenticode signs it); Linux and macOS start it
through `sh`, which copies the system's program to the single-file cache once,
checks its sha256 and executes it. `mcpp run --pkg-format polyglot` starts it
through `sh`; `mcpp emit xpkg` refuses it; it is not in a `--pkg-format` list
(e2e 992).
- **`mcpp run --pkg-format single-file`** runs the single file directly; it was
refused as an unknown format. `tar`, `zip` and `dir` are refused as containers
that are not programs.

- **A tree's POSIX launcher names macOS's quarantine mark.** A package a browser
downloaded carries `com.apple.quarantine` on every file, and macOS refuses to
start an unsigned runner so marked; the launcher says so before it starts the
runner, with `xattr -dr com.apple.quarantine <package>` (SPEC-014 §4.3).

### Fixed

- **A workspace's lock entry is keyed the same way whatever members a command
plans** (#868). Two members spelling one dependency two ways
(`tinyhttps.workspace = true`, `[dependencies.mcpplibs] tinyhttps.workspace =
true`) made `-p` of each write its own key and hash. The key is the
workspace's spelling: its `[dependencies]`, `[workspace.dependencies]`, then
the members in the order `members` lists them (docs/07, e2e 990).
- **A file `[build] ldflags` names by absolute path is a link input.** A
replaced prebuilt archive did not relink the program that names it; now every
link that carries the word depends on the file (docs/04, e2e 991).
- **A partial caret or tilde requirement treats unwritten segments as
wildcards** (#866, NopeCharon): `^0` is `< 1.0.0`, `^0.0` is `< 0.1.0`, `~1` is
`< 2.0.0`. A bare requirement is a caret, so the bare partial spellings `"0"`
and `"0.0"` take the same bounds; before, they matched 0.0.0 alone (docs/05).
- **The TOML reader accepts every escape TOML 1.0 defines** (#867, NopeCharon):
`\b`, `\f`, `\uXXXX` and `\UXXXXXXXX`. The writer escapes every control
character in turn (`\uXXXX` where TOML has no short form), so a normalised
manifest that holds one is still TOML; `\0` is this reader's extension and is
never written.

## [2026.10.30.1] - 2026-10-30

### Added
Expand Down
6 changes: 6 additions & 0 deletions docs/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,12 @@ jobs = "auto" # Concurrent compiles: a positive number, or "
bmi_schedule = "auto" # Module-edge scheduling: auto (= off) | on | off (§ below)
```

A word of `[build] ldflags` that names an existing file by absolute path (a
prebuilt archive, an object) is an input of every link that carries it: a
changed file relinks (mcpp 2026.10.31.1+). A relative word is read by the
linker against the build directory and is not tracked.


#### Compile-flag syntax *(mcpp 2026.9.17.1+)*

An element of `cflags`, `cxxflags` or `asmflags` stands for one or more compiler
Expand Down
7 changes: 7 additions & 0 deletions docs/05-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ baz = "=1.2.3" # Exact match
qux = ">=1.0, <2.0" # Range combination
```

A bare requirement is a caret: `foo = "1.2.3"` is `^1.2.3`, except that a bare
version equal to a published key addresses that key exactly. A bare partial
spelling follows the caret's wildcard rule as well: `"0"` is `>= 0.0.0, < 1.0.0`
and `"0.0"` is `>= 0.0.0, < 0.1.0` (before 2026.10.31.1 both matched 0.0.0
alone). The dependency conflict check and the address check of `--locked` read
requirements the same way.

### `visibility` — whether a dependency's usage crosses this package's own boundary

```toml
Expand Down
6 changes: 5 additions & 1 deletion docs/07-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,11 @@ myproject/
one, covers every member that has been built or configured.
- `mcpp.lock` at the workspace root records the resolution of every member.
`mcpp build --workspace` writes the whole record; `mcpp build -p X` updates
the entries of X's graph.
the entries of X's graph. An entry's key is the spelling under which the
workspace declares the dependency: its own `[dependencies]`, then
`[workspace.dependencies]`, then each member in the order `members` lists
them, whether the command selects it or not. The key and its hash therefore
do not depend on which members a command plans.
- A member's build program writes to `<member>/target/.build-mcpp/`. The
members' build programs are compiled in the workspace's build programs'
graph, `target/.build-mcpp/host/`, and a library several of them import is
Expand Down
46 changes: 41 additions & 5 deletions docs/10-pack-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ A pack is decided by three independent choices and a set of legs (2026.10.27.1+)
|---|---|---|---|
| linkage | `--linkage` | are the libraries inside the program or beside it | `static`, `shared` |
| closure | `--mode` | what the machine that runs the package provides | `system`, `vendored` (default), `self-contained` |
| container | `--pkg-format` | what is delivered | `tar`, `zip`, `dir`, `single-file`, a format a package provides, or a list of containers of one tree (`tar,okb`); unstated: `zip` for a Windows program, `tar` elsewhere |
| container | `--pkg-format` | what is delivered | `tar`, `zip`, `dir`, `single-file`, `polyglot`, a format a package provides, or a list of containers of one tree (`tar,okb`); unstated: `zip` for a Windows program, `tar` elsewhere |
| legs | `--target`, repeatable | which targets the pack covers | targets and target sets (`x86_64-openkal`, `openkal`, `linux-musl`) |
| systems | `--systems` | which systems a self-contained package's runners, or single files, are for | `linux`, `linux-aarch64`, `all` |

Expand Down Expand Up @@ -135,7 +135,9 @@ choices:
| several images of a platform that states no `runner`, not as single files | one pack per target, or `--pkg-format single-file` |
| `--target` outside the platform's targets | a target the platform serves |
| a target set that expands to nothing, or whose rows select different platforms | a target, or another set |
| `dir` or `single-file` in a `--pkg-format` list; a list with `-o` naming one file; a list on `mcpp run` | one format, or `-o <directory>/` |
| `dir`, `single-file` or `polyglot` in a `--pkg-format` list; a list with `-o` naming one file; a list on `mcpp run` | one format, or `-o <directory>/` |
| `--pkg-format polyglot` for a program of one system (a native target) | `--pkg-format single-file`, or `--target openkal` |
| `mcpp run --pkg-format tar`, `zip` or `dir` | `--pkg-format single-file` or `polyglot`, or a format a package provides |
| `--mode self-contained` of an image with `okb` as the only format | `--pkg-format tar,okb`: okb holds the images alone, and the runner would be dropped |
| a tree that runs nowhere (`runs_on` empty) | `--mode self-contained --systems <a system this mcpp builds for>` |
| a dispatched format for a `lib` or `shared` target | the library route, [12](12-binary-distribution.md) |
Expand Down Expand Up @@ -910,6 +912,36 @@ A WebAssembly target has no single file: `--pkg-format single-file` with
`--target wasm32-emscripten` is refused before anything compiles
(2026.10.29.1+), and its page is the `dist-web` provider's format.

### One file for several systems (`polyglot`, mcpp 2026.10.31.1+)

`--pkg-format polyglot` assembles an image's single files of every leg and
system (`--systems all` unless `--systems` says otherwise) into one file,
`target/dist/<name>-<version>.exe`:

```bash
mcpp pack --target openkal --pkg-format polyglot -o dist/
# Runs on linux-x86_64, windows-x86_64, linux-aarch64, macos-aarch64
```

The Windows program leads, with a POSIX shell script in its DOS header; the
other systems' programs follow it unchanged. Windows starts the file as the
program it is, and Authenticode signs it. A Linux or macOS system starts it
through a shell — `sh pg-0.1.0.exe`, or `./pg-0.1.0.exe` typed in a shell, which
runs a file the kernel refuses as a script; a program that calls `execve` on it
directly gets `Exec format error`. The script copies this system's program to
the single-file cache, `<cache>/mcpp/single-file/<sha256 of the program>/`,
with the rules of a native single file (a name of its own, `.mcpp-complete`,
rename without replacing, the user's directories only), checks its sha256 there
(`sha256sum`, `shasum` or `openssl`), and executes it; later runs start it from
the cache. A system the file holds no program for exits with status 126 and
names the systems it holds. Without `windows` among `--systems` the file is a
script (`#!/bin/sh`) the system executes directly, named without `.exe`.

`polyglot` is for a download a person runs. It is not in a `--pkg-format` list,
a native target is refused (its single file is one system's), `mcpp run
--pkg-format polyglot` starts it through `sh`, and `mcpp emit xpkg` refuses it:
an xlings entry installs one system's package, the tree or the single files.

### `mcpp-package.json`

Every staged tree carries `mcpp-package.json`: the package, its platform, its
Expand Down Expand Up @@ -964,7 +996,10 @@ inside an okb are stored so that the loader maps them in place.
`[pack] sign = ["<prog>", …, "{}"]` runs once on each program, image, library
and loader of the staged tree and on the Windows launcher before the container
is written, and on each
single file; `{}` is the file. A failure stops the pack, naming the argv and its
single file; `{}` is the file. A polyglot file's programs are signed one by one
before they are assembled, except the Windows program, and the assembled file
is signed last: Authenticode appends its certificate, which leaves every byte
and offset before it as it was. A failure stops the pack, naming the argv and its
output, and `mcpp-package.json` states the digests of the signed files.

## Configuration
Expand Down Expand Up @@ -1012,8 +1047,9 @@ The `static` mode additionally requires a musl toolchain configured under
- Distribution formats such as `.deb`, `.rpm`, AppImage and `.msi` come from
packages through `--pkg-format <name>`; the engine provides `tar`, `dir`, `zip`
and `single-file`.
- One file that runs on several systems is not produced; a single-file package
serves one system and one architecture.
- A polyglot file holds the systems and architectures this mcpp builds a
single file for (today Linux x86_64 and aarch64, macOS aarch64, Windows
x86_64), and only for ok images; a POSIX system starts it through a shell.
- A WebAssembly package is neither a single file nor an xlings entry; its page is
a provider's format (`dist-web`).
- An ELF program's closure is resolved by running its dynamic linker, so a Linux
Expand Down
Loading
Loading