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
32 changes: 31 additions & 1 deletion .agents/docs/2026-05-19-pack-windows-design.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
# Windows Pack Design

**Date:** 2026-05-19
**Status:** Planned (stub guard in place, implementation not yet started)
**Status:** SUPERSEDED and implemented, 2026-08-17. See
`2026-08-16-windows-toolchain-three-axes-design.md` §4 for the design that
shipped, and `src/pack/binfmt.cppm` / `src/pack/zip.cppm` for the code.

> ## What this document got wrong, and it is worth keeping
>
> Everything below assumes **pack runs on Windows**. That assumption is
> visible in every proposal: `dumpbin /dependents`, `ImageNtHeader` from
> `<windows.h>`, PowerShell's `Compress-Archive`, and an implementation
> "under `#if defined(_WIN32)`".
>
> The assumption came from the guard it was trying to remove. `pack` refused
> Windows, so the problem looked like "pack has no Windows branch". It was
> not: the ELF closure is derived by RUNNING the artifact
> (`LD_TRACE_LOADED_OBJECTS=1 '<binary>'`), so it can cross neither an OS nor
> an ARCHITECTURE — a Linux box cannot trace a PE, and an x86_64 box cannot
> trace an aarch64 ELF either. The `#if defined(_WIN32)` was that limitation
> surfacing at the nearest place a user would hit it.
>
> Reading the import table statically removes both limits at once, and then
> cross-OS packaging is not a feature that had to be added — it is what
> remains when the obstacle is gone. Each Windows-only tool above would have
> reintroduced the obstacle one layer down.
>
> What did survive from here: the `.zip` output, the flat
> DLLs-beside-the-`.exe` layout, no wrapper script, and the skip-list concept
> (with one correction — `vcruntime*.dll` is listed below as a system DLL, and
> it is not: it belongs to the TOOLSET, and whether it travels is
> `cxx_runtime`'s decision).

---

## Current state

Expand Down
6 changes: 6 additions & 0 deletions .agents/docs/2026-08-16-toolchain-architecture-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
> 三处"不可能失败的测试"。下面每一条都指得出具体的 `file:line`,
> 不是风格偏好。

> ⚠️ **§1 / §2 / §3 / §3d 已被方案取代**:
> `2026-08-16-windows-toolchain-three-axes-design.md`。
> 那份追问推翻了这里两处形状 —— `gcc@system` 的推广方向、以及新加
> `windows_sdk` manifest 键 —— 两处都在方案的 §0.2 里写明了为什么错。
> 本文其余各节(§3b / §3c / §4 / §5 / §6 / §7)已落地,保留作记录。

---

## 0. 结论先行
Expand Down
366 changes: 366 additions & 0 deletions .agents/docs/2026-08-16-windows-toolchain-three-axes-design.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions .github/workflows/cross-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,18 @@ jobs:
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
bash tests/e2e/198_windows_resources_cross.sh

# Packaging a Windows program FROM LINUX — and this job is the only
# place that can happen, for the same reason as the two above.
#
# Running it on a Windows runner would prove nothing: the point of
# reading the import table instead of executing the artifact
# (mcpp.pack.binfmt) is precisely that the packaging host need not be
# the target. A same-OS pack cannot tell the two implementations apart.
- name: "e2e: pack a PE from Linux (zip + DLL closure)"
run: |
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
bash tests/e2e/240_pack_pe_zip_cross.sh

# ── windows → linux ───────────────────────────────────────────────────────
# The mirror of mingw-cross-wine. Two jobs because a Windows runner cannot
# execute the ELF it produces; the artefact is handed to a Linux job and
Expand Down
66 changes: 66 additions & 0 deletions docs/02-pack-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,72 @@ own resolution, say — use `--mode vendored` instead. It repoints `PT_INTERP`
at the host loader, at the cost of requiring the host's glibc to be at least
as new as the one the artifact was built against.

### Windows (PE) — a `.zip`, and the DLLs sit beside the `.exe`

A Windows target produces a **`.zip`**, not a `.tar.gz`, and the layout is
flat:

```
target/dist/myapp-0.1.0-x86_64-pc-windows-msvc.zip
└── myapp-0.1.0-x86_64-pc-windows-msvc/
├── myapp.exe
├── vcruntime140.dll ← only under cxx_runtime = "toolchain-coupled"
├── mydep.dll ← third-party dependencies
├── README.md
└── LICENSE
```

There is no `bin/` + `lib/` split and no entry-point wrapper, and neither is a
style choice. The Win32 loader resolves a DLL from **the directory of the
executable**; PE has no `RUNPATH` to point anywhere else, so "next to the
`.exe`" *is* the mechanism that `$ORIGIN/../lib` provides on ELF.

**Windows' own DLLs are never bundled** — `kernel32.dll`, `ntdll.dll`,
`ucrtbase.dll`, the `api-ms-win-*` API sets. Shipping a private copy of an OS
component is a broken program rather than a heavier one (the process ends up
with two of something that must be unique), and Microsoft's redistribution
terms say the same thing from the other side. `[pack.bundle-project]
force_bundle` still overrides this, as it does the ELF skip list.

`vcruntime140.dll` and `msvcp140.dll` are **not** Windows' own: they belong to
the MSVC toolset, exactly as `libstdc++.so` belongs to gcc. Whether they
travel is decided by `cxx_runtime` (see `docs/05-mcpp-toml.md`), not by this
list — and `mcpp pack` refuses a combination that cannot deliver what the
contract promised:

```
$ mcpp pack --mode system # with cxx_runtime = "toolchain-coupled"
error: cxx_runtime = "toolchain-coupled" and --mode system contradict each other.
```

#### Packing a Windows program from Linux or macOS

This works, and it is not a special mode — just build for a Windows target and
pack:

```bash
mcpp pack --target x86_64-windows-gnu # from a Linux host
```

`mcpp pack` used to refuse Windows outright. The reason was not the archiver:
the ELF dependency closure is obtained by **running the artifact** under
`LD_TRACE_LOADED_OBJECTS`, which cannot cross an OS *or* an architecture. A PE
closure is read out of the file's import table instead, so nothing has to be
executed and the packaging host is free. The archive is written by mcpp itself
for the same reason — there is no zip tool present on every host.

Two consequences worth knowing:

- Entries are **stored, not deflated**, so a Windows package is roughly the
size of its contents. Compression is a size optimization, not a correctness
one, and it is not implemented yet.
- The archive is **deterministic**: no timestamps are read, so two packs of
the same tree are byte-identical and a published checksum means something.

The reverse direction — packing a Linux or macOS artifact *from* Windows —
still does not work, and for the original reason: that closure is resolved by
the target's own dynamic linker, which a Windows host has no way to run.

## Configuration

Packaging behavior is configured via the `[pack]` section in `mcpp.toml`. The
Expand Down
49 changes: 42 additions & 7 deletions docs/03-toolchains.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ questions. `msvc@system` asks *"use what this developer already has"*;
`msvc@14.44.35207` asks *"build this project with exactly this compiler"*.
Pinned toolsets coexist with each other and with a system Visual Studio.

> **`@system` is an MSVC-only spelling.** There is no `gcc@system` or
> `llvm@system`, and that is deliberate rather than an omission: mcpp is built
> on xlings, a user-space OS, and the design drives host dependencies to a
> minimum — a toolchain comes from a payload the manifest names, so every
> machine builds with the same compiler. Windows is the one place where
> refusing to use what is already installed would cost more than it buys:
> Visual Studio is very often present and cannot always be redistributed.
> `<family>@system` for any other family is an error that names both things
> you might have meant. (The family-less `[toolchain] … = "system"` — the PATH
> compiler — is a separate and deliberate escape hatch, and is unaffected.)

### `msvc@system` — the machine's own Visual Studio

mcpp locates and identifies an installed Visual Studio / Build Tools; it never
Expand Down Expand Up @@ -304,27 +315,51 @@ environment from the VC tools + Windows SDK (no `vcvarsall` involved), stages
`/interface /TP /ifcOutput`, scans with `/scanDependencies`, and links with
`link.exe`/`lib.exe` through response files.

The Windows SDK is located in this order: **`WindowsSdkDir`** (+
`WindowsSdkVersion`) if declared, then the `xim:windows-sdk` payload beside a
pinned toolset in mcpp's store, then `C:\Program Files (x86)\Windows Kits\10`.
A missing SDK fails the build with guidance (`mcpp self doctor` reports SDK
status).
**The Windows SDK follows the origin**, because the two origins answer
different questions and so must the SDK:

| origin | how the SDK is chosen |
|---|---|
| `msvc@<toolset>` | the `xim:windows-sdk` payload installed **with that toolset**, in mcpp's own store. `WindowsSdkDir` / `WindowsSdkVersion` in the environment are **ignored**, and mcpp prints a `note:` saying so. |
| `msvc@system` | **`WindowsSdkDir`** (+ `WindowsSdkVersion`) if declared, then `C:\Program Files (x86)\Windows Kits\10`. |

The asymmetry is the point. A pinned toolset is a promise that two machines
compile the same source against the same headers; an environment variable that
can quietly redirect it turns the pin into a preference. A machine's own SDK,
on the other hand, can only be found by looking, and there a declared answer
outranks a scan — the same precedence `VSINSTALLDIR` has over `vswhere`.

If a pinned toolset has no SDK payload beside it (an older install, say), mcpp
falls back to the machine's SDK rather than failing — and says so, because that
build is no longer reproducible and nothing else would record it.

A root only counts as an SDK when it has **both** halves — `Include\<v>\ucrt\
corecrt.h` *and* `Lib\<v>\um\<arch>\kernel32.lib`. A root with headers and no
import libraries is skipped rather than selected, so a partially unpacked
payload cannot outrank the machine's complete SDK and turn into
`LNK1104: cannot open file 'kernel32.lib'` at the very end of a build.

The resolved SDK version is part of the build's **runtime identity**
(`ucrt@10.0.26100.0`) and therefore of the fingerprint that keys the build
cache: changing SDK changes the cache key, exactly as changing compiler does.
It is a **compatibility floor declaration**, not a payload binding like
`glibc@2.39` on Linux — `ucrtbase.dll` is a Windows component and mcpp neither
ships nor substitutes it.

**CRT model.** `/MD` (host-coupled) by default; `/MT` when either

```toml
[target.x86_64-windows-msvc]
linkage = "static" # the libc axis — TARGET section, or `--static`

[build]
linkage = "static" # the libc axis
cxx_runtime = "self-contained" # the C++ runtime axis
```

is written down. On the MSVC ABI these are one physical switch — `/MT` links
is written down. Note which section each one lives in: `linkage` is
exact-triple only and **there is no `[build] linkage` key** — writing one gets
an "unsupported key (ignored)" warning and no static CRT. (This page said
exactly that a few sections up, and then showed the wrong form here.) On the MSVC ABI these are one physical switch — `/MT` links
the C and C++ runtimes out of the same library — so both spellings select it
and mean the same thing. It is a **whole-project** property: one `std` module
is built per project and cl bakes `_MSVC_MT`/`_MSVC_MD` into it, so a
Expand Down
37 changes: 33 additions & 4 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,39 @@ default applies only when nobody said anything.
`self-contained`, `false` means `host-coupled`. An explicit `cxx_runtime` wins.

**A contract that cannot be honored is reported, never silently downgraded.** If a
toolchain ships no `libc++.a`, or a contract has no mechanism on that platform
(`self-contained` under the MSVC runtime would need `/MT`, which mcpp does not emit
yet), the build prints what it fell back to instead of quietly producing a
different artifact than the manifest asked for.
toolchain ships no `libc++.a`, or a contract has no mechanism on that platform,
the build prints what it fell back to instead of quietly producing a different
artifact than the manifest asked for.

#### On the MSVC runtime

The CRT model is the mechanism here, and it is a **whole-project** switch: cl
bakes `_MSVC_MT`/`_MSVC_MD` into the one `std` module a project builds, so a
per-role contract that disagrees with the project's cannot be honoured and is
reported rather than ignored.

| value | what it is on MSVC |
|---|---|
| `self-contained` | `/MT` — the static CRT. `linkage = "static"` selects the same thing from the libc axis. |
| `host-coupled` (default under `/MD`) | the target provides `vcruntime140.dll` / `msvcp140.dll` — i.e. Visual Studio or the redistributable is installed there. |
| `toolchain-coupled` | the toolset's **own** copy of those DLLs travels with the artifact. |

`toolchain-coupled` is worth spelling out, because the obvious reading is
wrong. `ucrtbase.dll` *is* a Windows component (since Windows 10) and mcpp
never ships it. `vcruntime140.dll` and `msvcp140.dll` are **not**: every MSVC
toolset carries them under `VC\Redist\MSVC\<version>\<arch>\`, exactly the
way a gcc payload carries `libstdc++.so`. Under this contract mcpp stages them
beside the artifact — which is what makes a default `/MD` build runnable on a
machine that has only the pinned toolset and no Visual Studio at all.

The debug CRT (`vcruntime140d.dll` and friends, under `debug_nonredist\`) is
never staged: it may not be redistributed.

Combining it with `/MT` is a contradiction rather than a missing feature — a
static CRT leaves no DLL to couple to — so it is reported and resolved to
`self-contained`. `mcpp pack` enforces the other half: a mode that bundles
nothing (`--mode system`, `--mode static`) cannot deliver `toolchain-coupled`
and refuses.

**Scope.** The contract governs the C++ runtime only. Static **libc** is a separate
axis (`linkage = "static"` / `--static`, e.g. a musl target), and the deployment
Expand Down
59 changes: 59 additions & 0 deletions docs/zh/02-pack-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,65 @@ if (!base) {
它把 `PT_INTERP` 重指到宿主 loader,`/proc/self/exe` 正常,代价是要求宿主
glibc 不低于构建时所用的那份。

### Windows(PE)—— 产物是 `.zip`,DLL 就放在 `.exe` 旁边

Windows 目标产出的是 **`.zip`** 而不是 `.tar.gz`,并且是扁平布局:

```
target/dist/myapp-0.1.0-x86_64-pc-windows-msvc.zip
└── myapp-0.1.0-x86_64-pc-windows-msvc/
├── myapp.exe
├── vcruntime140.dll ← 仅在 cxx_runtime = "toolchain-coupled" 时
├── mydep.dll ← 第三方依赖
├── README.md
└── LICENSE
```

没有 `bin/` + `lib/` 的分层,也没有入口 wrapper —— 这两点都不是风格选择。
Win32 loader 解析 DLL 的第一顺位就是**可执行文件所在目录**,而 PE 没有
`RUNPATH` 可以指向别处,所以"放在 `.exe` 旁边"**就是** ELF 上
`$ORIGIN/../lib` 所提供的那个机制。

**Windows 自己的 DLL 永远不会被打进包里** —— `kernel32.dll`、`ntdll.dll`、
`ucrtbase.dll`、`api-ms-win-*` API set。带一份系统组件的私有拷贝不是"包变大
了",而是**程序坏了**(进程里出现了两份本应唯一的东西),而 Microsoft 的再分发
条款也从另一侧说了同一件事。`[pack.bundle-project] force_bundle` 仍然可以覆盖
这条,和它覆盖 ELF 跳过表一样。

`vcruntime140.dll` / `msvcp140.dll` **不是** Windows 自己的:它们属于 MSVC
toolset,就像 `libstdc++.so` 属于 gcc。它们要不要跟着产物走,由 `cxx_runtime`
决定(见 `docs/zh/05-mcpp-toml.md`),不由这张表决定 —— 而 `mcpp pack` 会拒绝
那些无法兑现契约的组合:

```
$ mcpp pack --mode system # 且 cxx_runtime = "toolchain-coupled"
error: cxx_runtime = "toolchain-coupled" and --mode system contradict each other.
```

#### 在 Linux / macOS 上给 Windows 打包

这是可以的,而且不是什么特殊模式 —— 指定 Windows 目标构建,然后打包即可:

```bash
mcpp pack --target x86_64-windows-gnu # 在 Linux 宿主上
```

`mcpp pack` 以前直接拒绝 Windows。真正的原因不是打包工具:ELF 的依赖闭包是靠
**把产物跑起来**(`LD_TRACE_LOADED_OBJECTS`)求出来的,所以它既跨不了 OS 也跨
不了架构。PE 的闭包改为从文件的导入表里**读**出来,于是不需要执行任何东西,
打包宿主也就自由了。压缩包本身也由 mcpp 自己写,理由相同 —— 没有哪个 zip 工具
在每个宿主上都存在。

有两点值得知道:

- 条目是 **stored(不压缩)** 的,所以 Windows 包的体积约等于其内容之和。压缩
是体积优化而不是正确性问题,目前尚未实现。
- 压缩包是**确定性**的:不读取任何时间戳,同一棵树打两次字节一致,公布的校验和
才有意义。

反方向 —— 在 Windows 上给 Linux / macOS 产物打包 —— 仍然不支持,原因还是最初
那个:那条闭包要由目标自己的动态链接器解析,而 Windows 宿主没有办法运行它。

## 配置项

打包行为通过 `mcpp.toml` 中的 `[pack]` 节配置,常用字段如下:
Expand Down
Loading
Loading