diff --git a/.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md b/.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md new file mode 100644 index 00000000..94114976 --- /dev/null +++ b/.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md @@ -0,0 +1,271 @@ +# MSVC System-Toolchain Detection — Design + +Date: 2026-07-13 +Status: approved for implementation (target release: 0.0.88) + +## 1. Problem & scope + +mcpp on Windows today builds exclusively with MSVC-ABI Clang (`llvm@20.1.7`), +which *silently borrows* the MSVC STL (`std.ixx`) and Windows SDK from an +installed Visual Studio via `mcpp.toolchain.msvc` discovery. But MSVC itself +is not a selectable toolchain: `mcpp toolchain default msvc` falls into the +xim-package path and fails with a confusing index error, and a machine +*without* VS fails deep inside the Clang std-module build with no actionable +message. + +**Goal of this change (detection-first):** make MSVC a first-class *system* +toolchain that a developer can switch to / configure, where mcpp + +1. checks whether the system has a usable MSVC installation, +2. auto-locates it (vswhere → env vars → well-known paths) and identifies its + version (VS product, VC tools version, `cl.exe` compiler version), +3. and, when MSVC is absent, prints clear installation guidance — + **mcpp never installs MSVC itself.** + +**Explicit non-goal (phase 2, separate PR):** building with `cl.exe` +(`/std:c++23`, `.ifc` modules, `link.exe`/`lib.exe`, `/scanDependencies`). +Selecting MSVC and then running `mcpp build` must fail with a precise +"native cl.exe builds not yet supported" message that names the detected +version and points at `llvm@20.1.7` as the working alternative — never with +an incidental downstream error. + +## 2. Existing scaffold (what we build on) + +| Layer | Symbol | Status | +|---|---|---| +| model | `CompilerId::MSVC`, `is_msvc_target()` (`src/toolchain/model.cppm`) | exists | +| discovery | `mcpp.toolchain.msvc`: `find_vs_install_path()` (vswhere/env/paths), `find_msvc_tools_dir()`, `find_cl()`, `find_std_module_source()` (`src/toolchain/msvc.cppm`) | exists, used only by clang.cppm for std.ixx | +| capabilities | `capabilities_for()` MSVC case: `msvc-stl` / `lib.exe` (`src/toolchain/provider.cppm`) | stub exists | +| abi | msvcrt / msvc-stl / msvc mappings (`src/toolchain/abi.cppm`) | exists | +| spec | `frontend_candidates_for("msvc") → {"cl.exe"}` (`src/toolchain/registry.cppm`) | exists | +| manifest | `[toolchain] windows = "msvc@system"` documented in `src/manifest/types.cppm` comment | schema ready, unimplemented | +| escape hatch | `tcSpec == "system"` skips xim resolution (`src/build/prepare.cppm:685`) | precedent for system toolchains | + +Missing: `detect()` cannot classify `cl.exe`; no cl version identification; +no lifecycle (`default`/`list`/`install`/`remove`) branches; no +prepare-time resolution of `msvc@system`; no doctor reporting; no +absent-MSVC guidance anywhere. + +## 3. Design + +### 3.1 The `msvc@system` spec + +MSVC is a **system toolchain**: mcpp locates it, never installs/removes it. +Canonical spec string is `msvc@system`. Accepted user inputs (CLI and +manifest) and their normalization: + +- `msvc`, `msvc@system` → `msvc@system` +- `msvc@` (e.g. `msvc@19.44`) → detect, then require the detected + `cl.exe` version to start with ``; mismatch is an error that + prints the detected version. (Pin-verify, not select-among-many: we always + use the newest VC tools of the newest VS, same policy as vswhere `-latest`.) + +The persisted global default (`~/.mcpp/config.toml [toolchain] default`) and +the manifest value are always the *stable* form `msvc@system` — never a +concrete version — so config survives VS updates. Concrete versions are +displayed, not stored. + +Registry additions (`registry.cppm`): + +- `bool is_system_toolchain(const ToolchainSpec&)` — true for `msvc` today + (the concept is deliberately general; `system` (PATH compiler) stays as-is). +- `parse_toolchain_spec` unchanged; msvc branching happens in callers via + `is_system_toolchain` (keeps parse pure). + +### 3.2 Discovery + identification (`msvc.cppm` extension) + +New exported surface: + +```cpp +struct MsvcInstallation { + std::filesystem::path vsRoot; // C:\Program Files\Microsoft Visual Studio\2022\BuildTools + std::string vsProduct; // "2022 BuildTools" (derived from path segments) + std::string toolsVersion; // "14.44.35207" (VC\Tools\MSVC\) + std::filesystem::path clPath; // ...\bin\Hostx64\x64\cl.exe + std::string clVersion; // "19.44.35211" (banner; falls back to toolsVersion-derived) + std::string arch; // "x64" | "x86" | "arm64" (host-native target dir) + bool hasStdModules; // modules\std.ixx present +}; + +// Locate the best (newest) usable installation. nullopt = MSVC absent. +std::optional detect_installation(); + +// Pure, cross-platform, unit-testable: +// parse "Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35211 for x64" +// (also localized banners: match a 19.xx.xxxxx token + arch token anywhere). +std::optional> parse_cl_banner(std::string_view); + +// Multi-line installation guidance used everywhere MSVC is required but absent. +std::string install_guidance(); +``` + +Implementation notes: + +- `detect_installation()` composes the existing `find_vs_install_path()` + + `find_latest_msvc_tools()`; adds cl banner capture. `cl.exe` is executed + bare with stderr merged; the banner prints regardless of the "usage" exit + status, so **ignore the exit code** and parse the output + (`platform::process::capture`, not probe's `run_capture` which errors on + non-zero exit). If execution or parsing fails, fall back to + `clVersion = ""` and report `toolsVersion` — never fail detection just + because the banner didn't parse. +- `parse_cl_banner` must not assume the English banner: scan for a + `\d+\.\d+\.\d+`-shaped token (first match) and an arch token among + `x64|x86|arm64|ARM64` (last match). Lives outside `#ifdef _WIN32` so Linux + unit tests cover it. +- `install_guidance()` (also outside `#ifdef`) says, in this order: what was + searched (vswhere / VSINSTALLDIR / standard paths), that mcpp does not + install MSVC, and how to get it: + - Visual Studio Installer → workload **Desktop development with C++** + (component `Microsoft.VisualStudio.Component.VC.Tools.x86.x64`) + - or Build Tools only: `winget install Microsoft.VisualStudio.2022.BuildTools` + then add the C++ workload in the installer + - then re-run `mcpp toolchain default msvc`. +- `arch`: host-native pair only (`Hostx64\x64` on x64, `Hostarm64\arm64` on + ARM64 hosts, falling back across the two). Cross target dirs are phase 2. + +### 3.3 Lifecycle commands (`lifecycle.cppm`) + +All four subcommands gain an msvc branch **before** the xim-package path, +gated on `is_system_toolchain(spec)`: + +- `mcpp toolchain default msvc[@…]` + - non-Windows → error: `the msvc toolchain is only available on Windows hosts`. + - Windows, detected → verify optional version pin, then + `write_default_toolchain(cfg, "msvc@system")` and print e.g. + ``` + Detected msvc 19.44.35211 (VS 2022 BuildTools, VC tools 14.44.35207) + cl: C:\...\bin\Hostx64\x64\cl.exe + import std: available (std.ixx) + Default set to msvc@system (was: llvm@20.1.7) + note: `mcpp build` with native MSVC (cl.exe) is not yet supported — coming in a later release. + ``` + - Windows, absent → error + `install_guidance()`, exit 1. +- `mcpp toolchain install msvc` — never installs. If detected: print the + detection summary + hint `mcpp toolchain default msvc`, exit 0. If absent: + `install_guidance()`, exit 1. Non-Windows: same error as `default`. +- `mcpp toolchain list` — on Windows, after the xim "Installed" rows, run + detection and append a `System:` section: + ``` + System: + msvc 19.44.35211 (VS 2022 BuildTools) C:\...\cl.exe + ``` + with `*` when the effective default is `msvc@system` + (`matches_default_toolchain` gains: `configuredDefault == "msvc@system"` + matches compiler `msvc`, any version). Absent MSVC adds one hint line: + `(msvc: not detected — run 'mcpp toolchain default msvc' for setup guidance)`. + Non-Windows: section omitted entirely. +- `mcpp toolchain remove msvc` — error: system toolchain, remove via the + Visual Studio Installer. + +### 3.4 Build-time resolution (`prepare.cppm`) + +In the toolchain-resolution chain (before the xim `resolve_xpkg_path` +branch): if `tcSpec` parses to an msvc system spec → + +- non-Windows: error `toolchain 'msvc@system' is only available on Windows`. +- Windows: `msvc::detect_installation()`; absent → error + guidance; + found → `explicit_compiler = clPath` (no xim resolution, no post-install + fixup), `ui::info("Resolved", "msvc@system → ")`. + +After `detect()` returns (ctx.tc populated), a single gate: +`tc.compiler == CompilerId::MSVC` → return error: + +``` +native MSVC (cl.exe) builds are not yet supported by mcpp. + detected: msvc 19.44.35211 at C:\...\cl.exe (selection & detection work today) + for building on Windows use the MSVC-ABI Clang toolchain instead: + mcpp toolchain default llvm@20.1.7 +``` + +This keeps every command that only *resolves/inspects* the toolchain +(`toolchain …`, `doctor`, `self env`) fully working while `build`/`run`/ +`test` fail early with one owned message. + +### 3.5 Detection classification (`detect.cppm`) + +`detect()` classifies by driver **filename first** for MSVC: stem `cl` +(case-insensitive) short-circuits before the `--version` probe (cl.exe has no +`--version`; running it with GNU flags produces garbage). The MSVC path: + +- run bare `cl.exe` (stderr merged), `parse_cl_banner` → `tc.version`, arch. +- `tc.compiler = CompilerId::MSVC`; `tc.targetTriple` = `x86_64-pc-windows-msvc` + / `i686-…` / `aarch64-…` from the banner arch (fallback: path segment + `Hostx64\x64`, then host arch). +- `tc.driverIdent` = normalized banner (fingerprint input — MSVC patch + updates change the banner, invalidating BMI caches correctly). +- `tc.stdModuleSource` = `msvc::find_std_module_source()`; + `tc.hasImportStd` accordingly. Skip `-dumpmachine` / `-print-sysroot` / + payload probing (meaningless for cl.exe). +- `bmi_traits()` gains the MSVC branch (`ifc.cache` / `.ifc` / + `needsExplicitModuleOutput=true`) so phase 2 doesn't silently reuse GCC + defaults; unreachable in builds this release because of the 3.4 gate. + +### 3.6 Doctor (`doctor.cppm`) + +Windows-only section "Checking msvc (system)": detected → print product / +tools version / cl version / std.ixx presence as ok-lines; absent → a +warning (not a failure) with the one-line hint to run +`mcpp toolchain default msvc` for guidance. When the *effective default* is +`msvc@system`, doctor's existing toolchain check must treat the missing +std-module BMI as expected (build support pending), not an error. + +### 3.7 User docs + +`docs/03-toolchains.md`: new "MSVC (system toolchain, Windows)" section — +what detection does, the not-installed guidance, the build-support status, +`[toolchain] windows = "msvc@system"` manifest example (finally matching the +types.cppm comment). + +## 4. Testing + +Unit (`tests/unit/test_toolchain_msvc.cpp`, cross-platform): + +- `parse_cl_banner`: English banner, localized banner (arch token + version + token only), garbage → nullopt, arm64 variant. +- `install_guidance()` non-empty, mentions `winget` and does not claim mcpp + installs MSVC. +- spec normalization: `msvc` / `msvc@system` / `msvc@19.44` → + `is_system_toolchain` true; `gcc@16.1.0` → false. +- `matches_default_toolchain("msvc@system", "msvc", )` true. +- `bmi_traits` MSVC branch. + +E2E: + +- `95_msvc_system_toolchain.sh` — `# requires: msvc` (new capability in + `run_all.sh`, set on Windows when vswhere+VC tools present): + `toolchain default msvc` succeeds & prints `Detected` + `msvc@system`; + `toolchain list` shows the System section with `*`; `mcpp build` on a + scratch project fails with `not yet supported` + detected version; + `toolchain remove msvc` fails with the system-toolchain message; + `toolchain install msvc` exits 0 with the already-installed summary. +- `96_msvc_unavailable_nonwindows.sh` — `# requires: unix-shell`: + `toolchain default msvc` fails with `only available on Windows`; + `toolchain list` shows no System section. + +CI: `ci-windows.yml` gains one step "Toolchain: MSVC — detection & selection" +running the e2e positive flow against `$MCPP_SELF` (windows-latest ships VS +2022 Enterprise, so detection must succeed; treat failure as regression). + +## 5. Rollout plan + +1. Temp branch `tmp/msvc-windows-ci`: implementation + **all workflows except + `ci-windows.yml` deleted** (CI-time economy while iterating), draft PR, + iterate to green. +2. Real branch `feat/msvc-system-toolchain` from the green tree with + workflows restored, version bumped to **0.0.88** + (`src/toolchain/fingerprint.cppm` `MCPP_VERSION` + `mcpp.toml` version + + `CHANGELOG.md`), real PR, full CI green, merge. +3. Tag `v0.0.88` → `release.yml` → mirror assets to xlings-res (gh + gtc + both ends) → xim-pkgindex mcpp package update (gitee auto-mirrors) → + `xlings install mcpp` verification → bump bootstrap pin. + +## 6. Risks + +- **cl.exe bare-run exit code/output shape varies by VS version** — mitigated: + ignore exit code, tolerate parse failure (fall back to tools-dir version). +- **windows-latest runner image changes VS layout** — mitigated: detection has + three strategies; CI step asserts detection so a silent regression surfaces. +- **Non-English banners** — mitigated: token-based parsing, unit-tested. +- **Users interpret `msvc` selection as build support** — mitigated: the + explicit note at selection time + the owned build-time error. diff --git a/.agents/docs/2026-07-13-msvc-system-toolchain-implementation-plan.md b/.agents/docs/2026-07-13-msvc-system-toolchain-implementation-plan.md new file mode 100644 index 00000000..22c8c9de --- /dev/null +++ b/.agents/docs/2026-07-13-msvc-system-toolchain-implementation-plan.md @@ -0,0 +1,78 @@ +# MSVC System-Toolchain Detection — Implementation Plan + +Companion to `2026-07-13-msvc-system-toolchain-detection-design.md`. +Target release: 0.0.88. Each step compiles + `mcpp test` passes on its own. + +## Step 1 — discovery & identification core (`src/toolchain/msvc.cppm`) + +- Add `MsvcInstallation`, `detect_installation()`, `parse_cl_banner()`, + `install_guidance()` per design §3.2. +- `parse_cl_banner` + `install_guidance` live outside `#ifdef _WIN32`. +- `vsProduct` derived from the two path segments after + `Microsoft Visual Studio` (`"2022 BuildTools"`); empty if layout unusual. +- cl banner capture: `platform::process::capture` on bare cl.exe, merged + stderr, exit code ignored. + +## Step 2 — spec layer (`src/toolchain/registry.cppm`) + +- `is_system_toolchain(const ToolchainSpec&)` (compiler == "msvc"). +- `matches_default_toolchain`: `"msvc@system"` matches (`"msvc"`, any ver). +- `display_label("msvc", …)` unchanged (generic path already fine). + +## Step 3 — model traits (`src/toolchain/model.cppm`) + +- `bmi_traits`: MSVC branch → `{ifc.cache, .ifc, ifc, true, false, false}`. + +## Step 4 — detection (`src/toolchain/detect.cppm`) + +- Filename short-circuit (stem `cl`, case-insensitive) before `--version` + probe → `enrich` MSVC path per design §3.5 (banner → version/arch/triple, + driverIdent, std.ixx, skip dumpmachine/sysroot/payloads). + +## Step 5 — lifecycle (`src/toolchain/lifecycle.cppm`) + +- `toolchain_set_default` / `toolchain_install` / `toolchain_remove` / + `toolchain_list`: msvc branches per design §3.3 (non-Windows error; + detect-report-persist; guidance on absence; System section in list). + +## Step 6 — build-time resolution + gate (`src/build/prepare.cppm`) + +- msvc spec branch before xim resolution (design §3.4). +- post-detect gate on `CompilerId::MSVC` with the owned + "not yet supported" error. + +## Step 7 — doctor (`src/doctor.cppm`) + +- Windows "Checking msvc (system)" section; msvc-default-aware std-BMI check. + +## Step 8 — tests + +- `tests/unit/test_toolchain_msvc.cpp` (banner/guidance/spec/traits, §4). +- `tests/e2e/95_msvc_system_toolchain.sh` (`# requires: msvc`), + `tests/e2e/96_msvc_unavailable_nonwindows.sh` (`# requires: unix-shell`). +- `tests/e2e/run_all.sh`: `msvc` capability (Windows + vswhere + VC tools). + +## Step 9 — docs + +- `docs/03-toolchains.md` MSVC section; CHANGELOG entry. + +## Step 10 — CI + +- `ci-windows.yml`: step "Toolchain: MSVC — detection & selection" driving + the 95 e2e flow via `$MCPP_SELF`. + +## Rollout (design §5) + +1. `tmp/msvc-windows-ci` branch: steps 1-10 + delete all workflows except + `ci-windows.yml`; draft PR; iterate until green. +2. `feat/msvc-system-toolchain`: green tree + workflows restored + version + bump (fingerprint.cppm, mcpp.toml, CHANGELOG) → real PR → full CI → merge. +3. Release v0.0.88 → xlings-res mirror (gh + gtc) → xim-pkgindex → + `xlings install mcpp` verify → bootstrap pin bump. + +## Verification per step + +- Local: `mcpp build && mcpp test` (Linux) after every step; unit tests for + the pure functions run on Linux. +- Windows behavior only verifiable in CI — keep Windows-only logic thin and + push the parseable/testable parts into pure functions. diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml deleted file mode 100644 index 661cb893..00000000 --- a/.github/workflows/aur-publish.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: aur-publish - -# Publish the `mcpp-bin` and `mcpp` AUR packages after a release. -# -# Triggers on COMPLETION of the `release` workflow (not on `release: -# published`): release.yml creates the GitHub Release in its first job but -# uploads the aarch64 / macOS / Windows assets in LATER jobs, so the aarch64 -# .sha256 that mcpp-bin needs only exists once the whole workflow finishes. -# -# Requires one repository secret: -# AUR_SSH_PRIVATE_KEY — private key whose public half is registered on the -# AUR account that owns mcpp / mcpp-bin. -# See scripts/aur/README.md → "Automated publishing" for the full setup. -on: - workflow_run: - workflows: [release] - types: [completed] - workflow_dispatch: - inputs: - version: - description: "Version to publish (default: [package].version in mcpp.toml)" - required: false - -concurrency: - group: aur-publish - cancel-in-progress: false - -jobs: - publish: - runs-on: ubuntu-latest - # On the workflow_run trigger, only proceed if the release actually - # succeeded (skip failed/cancelled release runs). - if: >- - github.event_name == 'workflow_dispatch' || - github.event.workflow_run.conclusion == 'success' - steps: - - name: Checkout released commit - uses: actions/checkout@v4 - with: - # workflow_run: the exact commit the release was built from. - # workflow_dispatch: default ref (HEAD of the branch). - ref: ${{ github.event.workflow_run.head_sha || github.ref }} - - - name: Refresh both PKGBUILDs to the release version - id: refresh - env: - # CI runs as root; force update.sh's template .SRCINFO path. - MCPP_AUR_NO_MAKEPKG: "1" - run: | - VER="${{ github.event.inputs.version }}" - if [ -z "$VER" ]; then - # mcpp.toml at the released commit carries the right version. - VER=$(grep -m1 -E '^\s*version\s*=' mcpp.toml | sed -E 's/.*"([^"]+)".*/\1/') - fi - echo "version=$VER" >> "$GITHUB_OUTPUT" - ./scripts/aur/update.sh "$VER" - - - name: Configure AUR SSH - run: | - install -dm700 ~/.ssh - printf '%s\n' "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/aur - chmod 600 ~/.ssh/aur - ssh-keyscan -t rsa,ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null - cat > ~/.ssh/config <<'EOF' - Host aur.archlinux.org - User aur - IdentityFile ~/.ssh/aur - IdentitiesOnly yes - EOF - - - name: Push to the AUR - env: - VER: ${{ steps.refresh.outputs.version }} - run: | - set -eu - git config --global user.name "mcpp-ci" - git config --global user.email "x.d2learn.org@gmail.com" - - publish() { # $1 = package name (= dir under scripts/aur/) - pkg="$1"; src="scripts/aur/${pkg}"; work="/tmp/aur-${pkg}" - # Clone the existing AUR repo; if the package doesn't exist yet - # (first publish), start an empty repo — AUR creates it on push. - if git clone "ssh://aur@aur.archlinux.org/${pkg}.git" "$work" 2>/dev/null \ - && [ -e "$work/.git" ]; then :; else - rm -rf "$work"; mkdir -p "$work" - git -C "$work" init -q - git -C "$work" remote add origin "ssh://aur@aur.archlinux.org/${pkg}.git" - fi - # AUR repos contain only PKGBUILD, .SRCINFO and local sources. - cp "$src/PKGBUILD" "$src/.SRCINFO" "$src/mcpp.sh" "$work/" - git -C "$work" add -A - if git -C "$work" diff --cached --quiet; then - echo ":: ${pkg}: no changes, skipping" - return 0 - fi - git -C "$work" commit -q -m "${pkg} ${VER}" - git -C "$work" push origin HEAD:master - echo ":: ${pkg}: published ${VER}" - } - - publish mcpp-bin - publish mcpp-m diff --git a/.github/workflows/bootstrap-macos.yml b/.github/workflows/bootstrap-macos.yml deleted file mode 100644 index 33fffc4d..00000000 --- a/.github/workflows/bootstrap-macos.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: bootstrap-macos - -# One-shot workflow to produce the first macOS mcpp binary. -# Uses xmake + xlings LLVM to compile mcpp from source. -# Once a macOS binary exists, mcpp can self-host for future releases. - -on: - workflow_dispatch: - -jobs: - bootstrap: - name: Bootstrap mcpp (macOS ARM64) - runs-on: macos-15 - timeout-minutes: 30 - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - - - name: Install xlings - run: | - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - curl -fsSL -o "${WORK}/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install - echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Install LLVM + xmake - run: | - xlings install llvm -y || xlings install llvm@20.1.7 -y - brew install xmake - LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname) - echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV" - "$LLVM_ROOT/bin/clang++" --version - xmake --version - - - name: Build mcpp with xmake - run: | - # Generate xmake.lua if not present - if [ ! -f xmake.lua ]; then - cat > xmake.lua << 'EOF' - add_rules("mode.release") - set_languages("c++23") - - package("cmdline") - set_homepage("https://github.com/mcpplibs/cmdline") - set_description("Modern C++ command-line parsing library") - set_license("Apache-2.0") - add_urls("https://github.com/mcpplibs/cmdline/archive/refs/tags/$(version).tar.gz") - add_versions("0.0.1", "3fb2f5495c1a144485b3cbb2e43e27059151633460f702af0f3851cbff387ef0") - on_install(function (package) - import("package.tools.xmake").install(package) - end) - package_end() - - add_requires("cmdline 0.0.1") - - target("mcpp") - set_kind("binary") - add_files("src/main.cpp") - add_files("src/**.cppm") - add_packages("cmdline") - add_includedirs("src/libs/json") - set_policy("build.c++.modules", true) - -- Static link libc++ for minimal runtime dependencies - add_ldflags("-static-libstdc++", {force = true}) - add_cxxflags("-stdlib=libc++", {force = true}) - add_ldflags("-stdlib=libc++", {force = true}) - EOF - fi - - # Configure with xlings LLVM - xmake f -y -m release --toolchain=llvm --sdk="$LLVM_ROOT" - # Build - xmake build -y mcpp - - - name: Verify built binary - run: | - MCPP=$(find build -name mcpp -type f -perm +111 | head -1) - test -x "$MCPP" - echo "=== file ===" - file "$MCPP" - echo "=== otool -L (dynamic deps) ===" - otool -L "$MCPP" - echo "=== version ===" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: Package - id: package - run: | - VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) - TARBALL="mcpp-${VERSION}-macosx-arm64.tar.gz" - WRAPPER="mcpp-${VERSION}-macosx-arm64" - - mkdir -p "dist/$WRAPPER/bin" - cp "$MCPP" "dist/$WRAPPER/bin/mcpp" - strip "dist/$WRAPPER/bin/mcpp" 2>/dev/null || true - cp LICENSE "dist/$WRAPPER/" 2>/dev/null || true - cp README.md "dist/$WRAPPER/" 2>/dev/null || true - - cat > "dist/$WRAPPER/mcpp" << 'LAUNCHER' - #!/bin/sh - exec "$(dirname "$0")/bin/mcpp" "$@" - LAUNCHER - chmod +x "dist/$WRAPPER/mcpp" - - # Bundle xlings - XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" - if [ -x "$XLINGS_BIN" ]; then - mkdir -p "dist/$WRAPPER/registry/bin" - cp "$XLINGS_BIN" "dist/$WRAPPER/registry/bin/xlings" - fi - - (cd dist && tar -czf "$TARBALL" "$WRAPPER") - (cd dist && shasum -a 256 "$TARBALL" > "$TARBALL.sha256") - - echo "tarball=dist/$TARBALL" >> "$GITHUB_OUTPUT" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - ls -la dist/ - - - name: Smoke test - run: | - SMOKE=$(mktemp -d) - tar -xzf "${{ steps.package.outputs.tarball }}" -C "$SMOKE" - VERSION="${{ steps.package.outputs.version }}" - "$SMOKE/mcpp-${VERSION}-macosx-arm64/bin/mcpp" --version - "$SMOKE/mcpp-${VERSION}-macosx-arm64/mcpp" --version - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcpp-macosx-arm64 - path: | - dist/mcpp-*-macosx-arm64.tar.gz - dist/mcpp-*-macosx-arm64.tar.gz.sha256 diff --git a/.github/workflows/ci-aarch64-fresh-install.yml b/.github/workflows/ci-aarch64-fresh-install.yml deleted file mode 100644 index 5f6b6e7c..00000000 --- a/.github/workflows/ci-aarch64-fresh-install.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: ci-aarch64-fresh-install - -# End-to-end "fresh install" of the whole ecosystem on a NATIVE aarch64 host, -# exactly as a new aarch64-Linux / Termux(-proot) user would: -# -# curl quick_install.sh | bash -> installs aarch64 xlings (static musl) -# xlings install mcpp -> installs aarch64 mcpp (static musl) -# mcpp new / build / run -> NATIVE aarch64 build (pulls the native -# musl-gcc toolchain from the ecosystem) -# -# Validates that every published aarch64 asset (xlings, mcpp, musl-gcc) lines -# up and that mcpp can build & run a real `import std` program natively on -# aarch64 — no cross, no qemu. Runs on GitHub's native ARM64 runner. - -on: - workflow_dispatch: - schedule: - - cron: '0 6 * * 1' # weekly Mon 06:00 UTC - push: - branches: [ main ] - paths: - - '.github/workflows/ci-aarch64-fresh-install.yml' - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - fresh-install: - name: fresh install + native build (aarch64 / glibc) - runs-on: ubuntu-24.04-arm - timeout-minutes: 60 - env: - # Verbose every mcpp invocation — cold bootstrap path (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - name: System info - run: | - uname -a - echo "arch: $(uname -m)" # aarch64 on this runner - - - name: Fresh-install xlings (curl | bash) - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" - - - name: Verify xlings + GLOBAL mirror - run: | - xlings --version - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - - - name: Fresh-install mcpp via xlings - run: | - xlings install mcpp -y - mcpp --version - mcpp self config --mirror GLOBAL 2>/dev/null || true - - - name: Refresh mcpp package index (force latest xim-pkgindex) - run: | - # mcpp seeds a baseline index with a freshness TTL marker, so a plain - # `index update` can no-op within the window. Force the latest index - # so this run validates the native build against current packages. - mcpp index update || true - idx="$HOME/.mcpp/registry/data/xim-pkgindex" - rm -rf "$idx" - git clone --depth 1 https://github.com/openxlings/xim-pkgindex "$idx" - grep -n "skipping relocation\|os.isfile(path.join(bindir" "$idx/pkgs/m/musl-gcc.lua" | head -2 || true - - - name: Native build + run an `import std` program - run: | - work=$(mktemp -d); cd "$work" - mcpp new hello - cd hello - # default src uses import std (C++23) - mcpp build - out=$(mcpp run 2>/dev/null || true) - echo "program output: $out" - bin=$(find target -type f -name hello | head -1) - file "$bin" - file "$bin" | grep -q "ARM aarch64" || { echo "expected aarch64 ELF"; exit 1; } - - - name: Self-host — build mcpp + xlings from source natively - run: | - # mcpp/xlings manifests pin a glibc default toolchain; on aarch64 the - # musl-static target is the published path, so build with --target. - git clone --depth 1 https://github.com/mcpp-community/mcpp /tmp/mcpp-src - cd /tmp/mcpp-src - mcpp self config --mirror GLOBAL 2>/dev/null || true - mcpp build --target aarch64-linux-musl - m=$(find target/aarch64-linux-musl -type f -name mcpp | head -1) - file "$m" | grep -q "ARM aarch64" || { echo "expected aarch64 mcpp"; exit 1; } - "$m" --version - git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - mcpp build --target aarch64-linux-musl - x=$(find target/aarch64-linux-musl -type f -name xlings | head -1) - file "$x" | grep -q "ARM aarch64" || { echo "expected aarch64 xlings"; exit 1; } - "$x" --version diff --git a/.github/workflows/ci-fresh-install.yml b/.github/workflows/ci-fresh-install.yml deleted file mode 100644 index bd5649c8..00000000 --- a/.github/workflows/ci-fresh-install.yml +++ /dev/null @@ -1,379 +0,0 @@ -name: ci-fresh-install - -# Fresh install CI — validates the released mcpp binary via xlings. -# Simulates a real first-time user on a clean machine (no caches). -# -# For each platform, tests every supported toolchain: -# 1. mcpp new hello → mcpp run (basic project) -# 2. mcpp build (build mcpp itself from source) -# -# This workflow tests released mcpp, not PR code. -# It runs on release publish, manual trigger, and daily schedule. - -on: - # NOTE: `release: published` never fires from the release pipeline — the - # release is created by release.yml with GITHUB_TOKEN, and GitHub - # suppresses workflow triggers from GITHUB_TOKEN-generated events. Kept - # only for releases created manually outside the pipeline. The reliable - # post-release hook is `workflow_run` below: a platform-generated event, - # exempt from that suppression, and requiring no cross-repo PAT. - release: - types: [ published ] - workflow_run: - workflows: [ release ] - types: [ completed ] - workflow_dispatch: - schedule: - # Run daily at 06:00 UTC to catch issues from xlings/runner updates - - cron: '0 6 * * *' - -concurrency: - group: ci-fresh-install - cancel-in-progress: false # use false to test in PRs, true to only test released mcpp - -jobs: - # ────────────────────────────────────────────────────────────────── - # Linux: gcc@16.1.0, musl-gcc@16.1.0, llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - # A4: post-release runs race the xim-pkgindex bump (a PR a maintainer - # merges asynchronously) — the 0.0.85 fresh-install failed with - # "version not found: available 0.0.84" 55s after the bump merged. - # Convert the race into a bounded wait: poll the index's mcpp.lua until - # it carries the released version (<=15 min), then let every job run. - # Non-workflow_run triggers (manual, cron, PR) skip the wait. - wait-index: - runs-on: ubuntu-latest - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - timeout-minutes: 20 - steps: - - name: Wait for xim-pkgindex to track the released mcpp - if: ${{ github.event_name == 'workflow_run' }} - run: | - VER=$(curl -fsSL "https://api.github.com/repos/mcpp-community/mcpp/releases/latest" | python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'].lstrip('v'))") - echo "released: $VER — waiting for index..." - for i in $(seq 1 30); do - if curl -fsSL "https://raw.githubusercontent.com/openxlings/xim-pkgindex/main/pkgs/m/mcpp.lua" | grep -q "\"$VER\""; then - echo "index tracks $VER (after $((i*30))s)"; exit 0 - fi - sleep 30 - done - echo "::error::index never tracked $VER within 15min — merge the bump PR (openxlings/xim-pkgindex) and re-run" - exit 1 - - name: No wait needed (manual/cron trigger) - if: ${{ github.event_name != 'workflow_run' }} - run: echo "not a post-release run; skipping index wait" - - linux-fresh: - needs: [wait-index] - name: Linux fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - # Verbose every mcpp invocation — fresh-install is the cold index/sandbox - # bootstrap path, exactly where extra diagnostics matter (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Install xlings + mcpp - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.38 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - run: | - # The release tarball bundles a pkgindex snapshot frozen at - # build time; refresh it so the workspace pin in .xlings.json - # (latest mcpp) resolves. - xlings update - xlings install mcpp -y -g # install to global - mcpp --version - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "Default: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_gcc - cd hello_gcc - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new ... --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on hosts without a sha256sum binary). - - name: "Template: mcpp new --template imgui (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template imgui - test -f abc1/mcpp.toml - - - name: "Default: build mcpp" - run: | - mcpp clean - mcpp run - - - name: "musl-gcc: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0-musl - mcpp toolchain default gcc@16.1.0-musl - cd "$(mktemp -d)" - mcpp new hello_musl - cd hello_musl - mcpp run - - - name: "musl-gcc: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0-musl - mcpp clean - mcpp run - - - name: "gcc 16: mcpp new → run" - run: | - mcpp toolchain install gcc 16.1.0 - mcpp toolchain default gcc@16.1.0 - cd "$(mktemp -d)" - mcpp new hello_gcc16 - cd hello_gcc16 - mcpp run - - - name: "gcc 16: build mcpp" - run: | - mcpp toolchain default gcc@16.1.0 - mcpp clean - mcpp run - - - name: "LLVM: mcpp new → run" - run: | - mcpp toolchain install llvm 20.1.7 - mcpp toolchain default llvm@20.1.7 - cd "$(mktemp -d)" - mcpp new hello_llvm - cd hello_llvm - mcpp run - - - name: "LLVM: build mcpp" - run: | - mcpp toolchain default llvm@20.1.7 - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Newer/rolling-glibc distros — reproduction surface for the - # bundled-glibc-vs-host-libtinfo `sh:` crash (host glibc > bundled). - # Plus older-glibc legs (the safe reverse direction) proving the - # musl-static mcpp + self-contained toolchain run end-to-end on old - # hosts. Runs the released mcpp inside distro containers. - # ────────────────────────────────────────────────────────────────── - linux-distro-matrix: - needs: [wait-index] - name: Linux distro (${{ matrix.distro }}) - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-24.04 - container: - image: ${{ matrix.image }} - timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - # findutils (find) is mandatory on every leg: quick_install.sh - # locates the extracted xlings dir with `find`, so a missing find - # fails the install with exit 127 *before* mcpp ever runs. Minimal - # images (opensuse/tumbleweed) ship without it; arch's base merely - # bundles it by luck. List it explicitly everywhere — don't rely on - # the base image. - include: - - distro: fedora-latest - image: fedora:latest - setup: dnf -y install curl bash tar gzip xz git findutils binutils file glibc-langpack-en - - distro: arch - image: archlinux:latest - setup: pacman -Sy --noconfirm curl bash tar gzip xz git findutils binutils file - - distro: tumbleweed - image: opensuse/tumbleweed:latest - setup: zypper -n install curl bash tar gzip xz git findutils binutils file - - distro: debian-testing - image: debian:testing - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: ubuntu-2004 - image: ubuntu:20.04 - setup: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - - distro: debian-11 - image: debian:11 - setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file - env: - XLINGS_NON_INTERACTIVE: '1' - HOME: /root - steps: - - uses: actions/checkout@v4 - - - name: Install prerequisites (${{ matrix.distro }}) - run: ${{ matrix.setup }} - - - name: Install xlings + mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.38 - # Deliberately NOT writing to $GITHUB_PATH here. On container - # images that declare no PATH in their config (opensuse/ - # tumbleweed), appending a single dir to GITHUB_PATH makes the - # runner exec later steps' `sh` with only that dir on PATH — - # `sh` (in /usr/bin) vanishes and the next step dies with - # `exec: "sh": ... not found` / exit 127. Each step below already - # exports PATH itself, so the append is redundant anyway. - - - name: Configure mcpp - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - xlings update - xlings install mcpp -y -g - mcpp --version - mcpp self config --mirror GLOBAL - - - name: "Regression: new → run (loader env must not crash /bin/sh)" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" - mcpp new hello_distro - cd hello_distro - mcpp run - - - name: "Self-containment: produced binary uses bundled loader" - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - cd "$(mktemp -d)" && mcpp new hc && cd hc && mcpp build - bin="$(find target -type f -name hc | head -1)" - interp="$(file "$bin" | grep -o 'interpreter [^,]*' | awk '{print $2}')" - echo "interp=$interp" - case "$interp" in - */.mcpp/*|*/registry/*|*xpkgs*) echo "OK bundled loader" ;; - *) echo "FAIL host loader: $interp"; exit 1 ;; - esac - - # ────────────────────────────────────────────────────────────────── - # macOS: llvm@20.1.7 - # ────────────────────────────────────────────────────────────────── - macos-fresh: - needs: [wait-index] - name: macOS fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - # macos-14: the support floor (mcpp ≥0.0.50 / xlings ≥0.4.50 ship - # minos=14.0 static-libc++ binaries). A fresh install passing here - # is the continuous proof of the macOS 14 floor — and of host-tool - # independence (this image has no sha256sum; macos-15 does). - runs-on: macos-14 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - # v0.4.50+: first xlings release whose macosx binary runs on - # macOS 14 (older ones carry minos=15 and refuse to start). - # v0.4.51: in-process sha256 — required on this image, which - # has no sha256sum binary (pinned fetches failed before). - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.51 - echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" - - - name: Install mcpp and config mirror - run: | - # Refresh the bundled pkgindex snapshot so the workspace pin - # in .xlings.json (latest mcpp) resolves. - xlings update - xlings install mcpp -y -g # install to global - mcpp --version - mcpp self config --mirror GLOBAL - - echo "mcpp debug info:" - which mcpp - cat $HOME/.xlings/.xlings.json - - - name: "LLVM: mcpp new → run" - run: | - cd "$(mktemp -d)" - mcpp new hello_mac - cd hello_mac - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path — this is what broke on hosts without a sha256sum binary - # (stock macOS / bare Windows) before xlings 0.4.51 hashed - # in-process. - - name: "Template: mcpp new --template imgui (fetch path)" - run: | - cd "$(mktemp -d)" - mcpp new abc1 --template imgui - test -f abc1/mcpp.toml - - - name: "LLVM: build mcpp" - run: | - mcpp clean - mcpp run - - # ────────────────────────────────────────────────────────────────── - # Windows: llvm@20.1.7 + MSVC STL - # ────────────────────────────────────────────────────────────────── - windows-fresh: - needs: [wait-index] - name: Windows fresh install - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: windows-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - name: Install xlings - shell: pwsh - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex - - $xlingsbin = "$env:USERPROFILE\.xlings\subos\current\bin" - $env:PATH = "$xlingsbin;$env:PATH" - $xlingsbin | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 - - - name: Install mcpp and config mirror - shell: pwsh - run: | - # Refresh the bundled pkgindex snapshot so the workspace pin - # in .xlings.json (latest mcpp) resolves. - xlings update - xlings install mcpp -y -g --verbose - - cat "$env:USERPROFILE\.xlings\.xlings.json" - mcpp --version - mcpp self config --mirror GLOBAL - - - name: "LLVM: mcpp new → run" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new hello_win - Set-Location hello_win - mcpp run - - # Template packages exercise the sha256-pinned mcpp-index fetch - # path (user report: `mcpp new abc1 --template imgui` failed with - # fetch 'imgui@0.0.6' exit 1 on bare Windows — no sha256sum binary - # outside git-bash; fixed by xlings 0.4.51 in-process hashing). - - name: "Template: mcpp new --template imgui (fetch path)" - shell: pwsh - run: | - $tmp = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_ } - Set-Location $tmp - mcpp new abc1 --template imgui - if (!(Test-Path abc1/mcpp.toml)) { exit 1 } - - - name: "LLVM: build mcpp" - shell: pwsh - run: | - mcpp clean - mcpp run diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml deleted file mode 100644 index 7e823b74..00000000 --- a/.github/workflows/ci-linux-e2e.yml +++ /dev/null @@ -1,192 +0,0 @@ -name: ci-linux-e2e - -# The e2e suite (tests/e2e/run_all.sh, ~18 min) split out of ci-linux.yml so it -# runs in PARALLEL with the build/unit/toolchain-matrix job instead of tacked on -# after it. Both workflows share the same cache lineage (mcpp sandbox + xlings + -# target/), so this job restores a warm build and the only added wall-clock vs. -# the inline version is one extra warm `mcpp build`. Net per-PR critical path -# drops from "build + … + e2e" to max(build+matrix, build+e2e). -# -# Paired workflows: ci-linux.yml (build + unit + toolchain matrix + integration), -# ci-macos.yml, ci-windows.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - e2e: - name: e2e suite (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 45 - env: - MCPP_HOME: /home/runner/.mcpp - # NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that - # assert mcpp's DEFAULT (quiet) output — e.g. 48_build_error_output and - # 53_namespaced_cache_label — which forced verbose would break. Verbose is - # set only in the fresh-install workflows (cold bootstrap, no such asserts). - # A specific test that needs verbose passes `--verbose` itself. - steps: - - uses: actions/checkout@v4 - - # Same cache lineage as ci-linux.yml so this job lands on a warm - # toolchain/sandbox instead of re-installing it. Both workflows read - # (and may save) the same keys; actions/cache tolerates concurrent - # "already exists" saves. - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-ci-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-ci- - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-v2- - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Cache target/ (build artifacts + BMIs) - uses: actions/cache@v4 - with: - path: target - key: mcpp-target-${{ runner.os }}-${{ hashFiles('src/**', 'tests/**', 'mcpp.toml', 'mcpp.lock') }} - restore-keys: | - mcpp-target-${{ runner.os }}- - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: E2E suite - # Per-test 600s timeout lives in tests/e2e/run_all.sh and identifies - # WHICH test hung; this caps the whole suite so a hang fails fast. - timeout-minutes: 25 - run: | - # Point the e2e runner at the freshly-built binary, not the - # bootstrap one. Tests cd into mktemp -d, so $MCPP must be - # absolute or the relative path breaks under the temp cwd. - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - export MCPP - # Tests that set MCPP_HOME to a fresh tmpdir need an xlings to - # bootstrap from; surface the xlings binary installed above. - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - # GitHub-hosted runners are outside CN; keep CI toolchain downloads on - # the global mirror while mcpp's default remains CN for fresh local - # sandboxes. E2E tests with their own MCPP_HOME read this variable. - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # Pin the global default so test 28 (default-toolchain path) gets a - # deterministic GNU answer instead of an auto-install pick. - "$MCPP" toolchain default gcc@16.1.0 - # Warm musl once so fresh-home e2e tests inherit the payload. - "$MCPP" toolchain install gcc 16.1.0-musl - bash tests/e2e/run_all.sh - - # ────────────────────────────────────────────────────────────────── - # Hermetic (no host toolchain): the ONLY environment class that - # faithfully reproduces issue #195. Standard runners ship gcc + - # libc6-dev, so a sandbox toolchain that leaks to the host's CRT - # still links "green" there; this container has no compiler and no - # host Scrt1.o, so any leak fails loudly. Builds PR code with the - # bootstrap mcpp, then runs the llvm flow end-to-end. - # ────────────────────────────────────────────────────────────────── - hermetic: - name: hermetic e2e (no host toolchain, container) - runs-on: ubuntu-24.04 - container: debian:stable-slim - timeout-minutes: 60 - env: - XLINGS_NON_INTERACTIVE: '1' - steps: - - name: Install base utilities (NO compiler) - run: | - apt-get update -qq - apt-get install -y -qq curl ca-certificates git xz-utils unzip - # The whole point of this job: no host toolchain, no host CRT. - ! command -v gcc - ! command -v cc - test ! -e /usr/lib/x86_64-linux-gnu/Scrt1.o - test ! -e /usr/lib/gcc - - - uses: actions/checkout@v4 - - # Payload cache (downloads only — the container still has no host - # toolchain, which is the property under test). - - name: Cache mcpp sandbox payloads - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-hermetic-${{ hashFiles('mcpp.toml') }} - restore-keys: | - mcpp-hermetic- - - - name: Bootstrap xlings + released mcpp - run: | - curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.62 - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - xlings update - xlings install mcpp -y -g - MCPP_BOOT="$HOME/.xlings/subos/current/bin/mcpp" - "$MCPP_BOOT" --version - "$MCPP_BOOT" self config --mirror GLOBAL - echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV" - - - name: Build PR mcpp from source (sandbox gcc only) - run: | - "$MCPP_BOOT" build - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: "issue #195 reproduction: manifest llvm toolchain, fresh" - run: | - cd "$(mktemp -d)" - "$MCPP" new hello195 - cd hello195 - printf '\n[toolchain]\nlinux = "llvm@22.1.8"\n' >> mcpp.toml - printf 'import std;\nint main() { std::println("hello {}", 195); return 0; }\n' > src/main.cpp - "$MCPP" run - - - name: Hermetic llvm e2e subset - run: | - export PATH="$HOME/.xlings/subos/current/bin:$PATH" - export MCPP - bash tests/e2e/86_llvm_hermetic_link.sh - bash tests/e2e/37_llvm_import_std.sh diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml deleted file mode 100644 index fc3a3640..00000000 --- a/.github/workflows/ci-linux.yml +++ /dev/null @@ -1,168 +0,0 @@ -name: ci-linux - -# Self-host CI on Linux: mcpp builds mcpp. The bootstrap mcpp comes from -# `xlings install mcpp` (xim:mcpp in the xlings package index), so this -# workflow no longer depends on a previous-release tarball — the -# chicken-and-egg now lives upstream in the xlings index. -# -# This job covers build + unit/integration tests + the toolchain matrix + -# the xlings integration build. The ~18 min e2e suite is a SEPARATE workflow -# (ci-linux-e2e.yml) that runs in parallel on the same warm caches, so the -# per-PR critical path is max(this, e2e) instead of their sum. -# -# Paired workflows: ci-linux-e2e.yml, ci-macos.yml, ci-windows.yml. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-and-test: - name: build + test (linux x86_64, self-host) - runs-on: ubuntu-24.04 - timeout-minutes: 60 - env: - # MCPP_HOME pinned so the cache key below restores into the - # same path mcpp resolves at runtime. - MCPP_HOME: /home/runner/.mcpp - # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - # Cache mcpp's sandbox: the toolchain (musl-gcc + binutils + - # glibc + linux-headers + patchelf + ninja) takes minutes to - # install on a cold runner. Key on the workspace manifest so a - # toolchain change in mcpp.toml refreshes the cache; restore-keys - # provide a layered fallback so near-misses still skip the slow - # toolchain installs. - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - # NOTE: the "-ci-" segment keeps this lineage disjoint from - # release.yml's "-release-" caches. A bare "mcpp-sandbox--" - # restore prefix used to match the release sandbox too, silently - # swapping in a differently-populated registry (issue #120). - key: mcpp-sandbox-${{ runner.os }}-ci-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-ci- - - # Cache xlings + its locally installed packages (xim:mcpp etc.). - # Saves the xlings bootstrap roundtrip + the mcpp xpkg download - # on hot runs. - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-v2- - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - run: | - # Always install the pinned version — cache may hold an older - # xlings whose sysroot/packages are incompatible. - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - # xim:mcpp — `xlings install` is idempotent so cache hits skip - # the download. - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - # Cache the build directory: precise key on src/ + manifest - # so a no-source-change run lands on a full hit. Layered - # restore-keys let mid-run partial hits keep BMI/dyndep state - # for proper incremental builds. - - name: Cache target/ (build artifacts + BMIs) - uses: actions/cache@v4 - with: - path: target - key: mcpp-target-${{ runner.os }}-${{ hashFiles('src/**', 'tests/**', 'mcpp.toml', 'mcpp.lock') }} - restore-keys: | - mcpp-target-${{ runner.os }}- - - - name: Configure mirror + Build mcpp from source (self-host) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # Set GLOBAL mirror via xlings directly (bootstrap mcpp may lack --mirror flag) - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build - - - name: Unit + integration tests via `mcpp test` - run: | - # Use freshly-built mcpp for test (it has --mirror support) - MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - "$MCPP_FRESH" self config --mirror GLOBAL - "$MCPP_FRESH" test - - # NOTE: the e2e suite (tests/e2e/run_all.sh) moved to ci-linux-e2e.yml - # so it runs in parallel with this job. The toolchain matrix below no - # longer relies on e2e's toolchain warm-ups: the GCC build uses the - # mcpp.toml-pinned default (gcc@16.1.0, already in the sandbox from the - # self-host build above), and the musl `--target` build auto-installs - # gcc@16.1.0-musl on demand (cached across runs). - - - name: Save freshly-built mcpp for toolchain tests - run: | - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - cp "$MCPP" /tmp/mcpp-fresh - echo "MCPP=/tmp/mcpp-fresh" >> "$GITHUB_ENV" - - - name: "Toolchain: GCC — build mcpp + test" - run: | - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0" build.log - "$MCPP" test - - - name: "Toolchain: musl-gcc — build mcpp (--target)" - run: | - "$MCPP" clean - "$MCPP" build --target x86_64-linux-musl 2>&1 | tee build.log; grep -q "Resolved gcc@16.1.0-musl" build.log - - - name: "Toolchain: LLVM — build mcpp" - run: | - "$MCPP" toolchain install llvm 20.1.7 - # Override project toolchain to use LLVM for this build - sed -i 's/^default = "gcc@16.1.0"/default = "llvm@20.1.7"/' mcpp.toml - "$MCPP" clean - "$MCPP" build 2>&1 | tee build.log; grep -q "Resolved llvm@20.1.7" build.log - # Restore - sed -i 's/^default = "llvm@20.1.7"/default = "gcc@16.1.0"/' mcpp.toml - - # Integration: the mcpp built from THIS PR's source (the self-host binary, - # $MCPP = /tmp/mcpp-fresh) builds & runs a real external C++ project — - # xlings (openxlings/xlings ships its own mcpp.toml). MCPP_VENDORED_XLINGS - # only supplies the xlings package backend that mcpp resolves deps through. - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - # $MCPP is the freshly self-hosted binary built from this PR. - git clone --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml deleted file mode 100644 index f1d15f41..00000000 --- a/.github/workflows/ci-macos.yml +++ /dev/null @@ -1,421 +0,0 @@ -name: ci-macos - -# macOS CI for mcpp — validates LLVM/Clang as the default macOS toolchain. -# Tests the full xlings → LLVM → C++23 import std pipeline on macOS ARM64. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-macos-${{ github.ref }} - cancel-in-progress: true - -jobs: - macos-xlings-llvm: - name: macOS ARM64 — xlings LLVM end-to-end - runs-on: macos-15 - timeout-minutes: 30 - # NOTE: no MCPP_VERBOSE here — this job runs the e2e suite, which includes - # tests asserting mcpp's default quiet output (48/53). Verbose is forced only - # in the fresh-install workflows. - steps: - - uses: actions/checkout@v4 - - - name: System info - run: | - uname -a - sw_vers - xcrun --show-sdk-path - echo "SDK: $(xcrun --show-sdk-version)" - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-macos15-arm64-v3-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-macos15-arm64-v3- - - - name: Bootstrap xlings - env: - XLINGS_NON_INTERACTIVE: '1' - XLINGS_VERSION: '0.4.30' - run: | - WORK=$(mktemp -d) - tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" - curl -fsSL -o "${WORK}/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "${WORK}/${tarball}" -C "${WORK}" - XLINGS_DIR="${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64" - "$XLINGS_DIR/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - echo "PATH=$HOME/.xlings/subos/default/bin:$PATH" >> "$GITHUB_ENV" - - - name: Install LLVM via xlings - run: | - # latest-first: test binaries now link the toolchain's own libc++ - # (A1 root fix in flags.cppm), so new llvm releases are - # self-consistent — this job is the proof for each new default. - xlings install llvm -y || xlings install llvm@20.1.7 -y - # Verify clang++ is available - LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname) - echo "LLVM_ROOT=$LLVM_ROOT" - ls "$LLVM_ROOT/bin/clang++" - "$LLVM_ROOT/bin/clang++" --version - echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV" - echo "CXX=$LLVM_ROOT/bin/clang++" >> "$GITHUB_ENV" - - - name: Inspect LLVM package structure - run: | - echo "=== bin/ ===" - ls "$LLVM_ROOT/bin/" | grep -E "^(clang|llvm|lld|ld)" | head -20 - echo "=== lib/ ===" - ls "$LLVM_ROOT/lib/" 2>/dev/null | head -10 - echo "=== share/libc++/ ===" - find "$LLVM_ROOT" -name "std.cppm" -o -name "std.compat.cppm" 2>/dev/null - echo "=== clang++.cfg ===" - cat "$LLVM_ROOT/bin/clang++.cfg" 2>/dev/null || echo "(no cfg file)" - echo "=== Target triple ===" - "$CXX" -dumpmachine - echo "=== Module manifest ===" - "$CXX" -print-library-module-manifest-path 2>/dev/null || echo "(not available)" - - - name: Test — non-module C++23 compilation - run: | - WORK=$(mktemp -d) - cd "$WORK" - cat > main.cpp << 'EOF' - #include - #include - int main() { - std::cout << std::format("Hello from LLVM on macOS! clang {}", __clang_version__) << std::endl; - return 0; - } - EOF - "$CXX" -std=c++23 -o hello main.cpp - ./hello - - - name: Test — import std (two-stage module compilation) - run: | - WORK=$(mktemp -d) - cd "$WORK" - - # Find std.cppm - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - if [ -z "$STD_CPPM" ]; then - echo "::error::std.cppm not found in LLVM package" - find "$LLVM_ROOT" -name "*.cppm" 2>/dev/null - exit 1 - fi - echo "std.cppm at: $STD_CPPM" - - echo "=== Step 1: Precompile std module ===" - mkdir -p pcm.cache - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - - echo "=== Step 2: Compile std.pcm → std.o ===" - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - echo "=== Step 3: Compile main.cpp with import std ===" - cat > main.cpp << 'EOF' - import std; - int main() { - std::println("C++23 import std works on macOS via xlings LLVM!"); - return 0; - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm -c main.cpp -o main.o - - echo "=== Step 4: Link ===" - "$CXX" main.o std.o -o hello_modules - echo "=== Step 5: Run ===" - ./hello_modules - - - name: Test — import std.compat - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - STD_COMPAT_CPPM=$(find "$LLVM_ROOT" -name "std.compat.cppm" -path "*/libc++/*" | head -1) - - if [ -z "$STD_COMPAT_CPPM" ]; then - echo "::warning::std.compat.cppm not found, skipping" - exit 0 - fi - echo "std.compat.cppm at: $STD_COMPAT_CPPM" - - mkdir -p pcm.cache - # Build std first - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # Build std.compat (depends on std) - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - --precompile "$STD_COMPAT_CPPM" -o pcm.cache/std.compat.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/std.compat.pcm -c -o std.compat.o - - cat > main.cpp << 'EOF' - import std.compat; - #include - int main() { - printf("std.compat works on macOS! %s\n", "success"); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=std.compat=pcm.cache/std.compat.pcm \ - -c main.cpp -o main.o - "$CXX" main.o std.o std.compat.o -o compat_test - ./compat_test - - - name: Test — multi-module project - run: | - WORK=$(mktemp -d) - cd "$WORK" - - STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1) - mkdir -p pcm.cache - - # Build std - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - --precompile "$STD_CPPM" -o pcm.cache/std.pcm - "$CXX" -std=c++23 -Wno-reserved-module-identifier \ - pcm.cache/std.pcm -c -o std.o - - # User module: greeter - cat > greeter.cppm << 'EOF' - export module greeter; - import std; - export namespace greeter { - std::string hello(std::string_view name) { - return std::format("Hello, {}! (from macOS module)", name); - } - } - EOF - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - --precompile greeter.cppm -o pcm.cache/greeter.pcm - "$CXX" -std=c++23 -fmodule-file=std=pcm.cache/std.pcm \ - pcm.cache/greeter.pcm -c -o greeter.o - - # Main - cat > main.cpp << 'EOF' - import std; - import greeter; - int main() { - std::println("{}", greeter::hello("mcpp")); - return 0; - } - EOF - "$CXX" -std=c++23 \ - -fmodule-file=std=pcm.cache/std.pcm \ - -fmodule-file=greeter=pcm.cache/greeter.pcm \ - -c main.cpp -o main.o - "$CXX" main.o greeter.o std.o -o multimod - ./multimod - - - name: Validate mcpp probe logic expectations - run: | - echo "=== Verifying mcpp's assumptions ===" - echo "1. -print-sysroot returns empty (mcpp falls back to xcrun):" - result=$("$CXX" -print-sysroot 2>/dev/null || true) - if [ -z "$result" ]; then - echo " PASS: empty (xcrun fallback needed)" - else - echo " INFO: $result" - fi - - echo "2. xcrun --show-sdk-path works:" - xcrun --show-sdk-path && echo " PASS" - - echo "3. -dumpmachine returns darwin triple:" - triple=$("$CXX" -dumpmachine) - echo " $triple" - echo "$triple" | grep -q "darwin" && echo " PASS: contains 'darwin'" - - echo "4. libc++ module manifest discoverable:" - manifest=$("$CXX" -print-library-module-manifest-path 2>/dev/null || true) - if [ -n "$manifest" ] && [ -f "$manifest" ]; then - echo " PASS: $manifest" - echo " Content:" - cat "$manifest" | head -20 - else - echo " INFO: manifest not via flag, using fallback path" - find "$LLVM_ROOT/share/libc++" -name "*.cppm" 2>/dev/null && echo " PASS: fallback exists" - fi - - echo "5. llvm-ar available:" - ls "$LLVM_ROOT/bin/llvm-ar" && echo " PASS" - - echo "6. clang-scan-deps available:" - ls "$LLVM_ROOT/bin/clang-scan-deps" && echo " PASS" || echo " WARN: not found" - - - name: Validate install.sh platform detection - run: | - uname_s=$(uname -s) - uname_m=$(uname -m) - echo "Platform: ${uname_s}-${uname_m}" - case "${uname_s}-${uname_m}" in - Darwin-arm64) echo "PASS: would select darwin-arm64" ;; - Darwin-x86_64) echo "PASS: would select darwin-x86_64" ;; - *) echo "FAIL: unexpected platform"; exit 1 ;; - esac - - - name: Bootstrap mcpp via xlings - run: | - # Same pattern as Linux CI: xlings install mcpp - xlings install mcpp -y - MCPP="$HOME/.xlings/subos/default/bin/mcpp" - test -x "$MCPP" - "$MCPP" --version - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - - name: Configure dev mcpp sandbox to reuse xlings LLVM - run: | - # Use EXACTLY the LLVM the install step resolved (env LLVM_ROOT) — - # a version-glob pick chose a stale cached 20.1.7 next to the - # freshly installed 22.1.8 and silently tested the wrong toolchain. - LLVM_PKG="$LLVM_ROOT" - test -d "$LLVM_PKG" - LLVM_VER=$(basename "$LLVM_PKG") - echo "MCPP_LLVM_VER=$LLVM_VER" >> "$GITHUB_ENV" - MCPP_LLVM_LINK="$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm/$LLVM_VER" - printf '1\n' > "$LLVM_PKG/.mcpp_ok" - - mkdir -p "$HOME/.mcpp/registry/data/xpkgs/xim-x-llvm" - rm -rf "$MCPP_LLVM_LINK" - ln -s "$LLVM_PKG" "$MCPP_LLVM_LINK" - - mkdir -p "$HOME/.mcpp" - cat > "$HOME/.mcpp/config.toml" </dev/null | grep -cE "T __ZNSt3__1" || echo "0 (good: no libc++ code in binary)" - echo "--- direct run ---" - set +e - "$BIN" > run.out 2>&1 - echo "exit=$?" - head -20 run.out - sleep 5 - echo "--- newest crash report (termination) ---" - CR=$(ls -t "$HOME/Library/Logs/DiagnosticReports"/*.ips 2>/dev/null | head -1) - if [ -n "$CR" ]; then - python3 - "$CR" <<'PY' - import json, sys - lines = open(sys.argv[1]).read().splitlines() - meta = json.loads(lines[0]); body = json.loads("\n".join(lines[1:])) - print("proc:", meta.get("app_name"), "| exc:", body.get("exception", {})) - print("termination:", body.get("termination", {})) - t = [th for th in body.get("threads", []) if th.get("triggered")] - for fr in (t[0].get("frames", [])[:12] if t else []): - print(" ", fr.get("imageIndex"), fr.get("symbol", fr.get("imageOffset"))) - imgs = body.get("usedImages", []) - for i, im in enumerate(imgs[:12]): - print("img", i, im.get("path")) - PY - else - echo "none" - fi - - - name: E2E suite - # See ci-linux.yml — fail-fast on hung tests instead of burning the - # whole job budget. Per-test 600s timeout lives in run_all.sh. - timeout-minutes: 25 - run: | - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - export MCPP - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - test -x "$MCPP_VENDORED_XLINGS" - export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL - "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" - "$MCPP" self config - # macOS default toolchain is LLVM - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - bash tests/e2e/run_all.sh - - - name: "Toolchain: LLVM — build mcpp (self-host)" - run: | - MCPP=$(find target -path "*/bin/mcpp" | head -1) - MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") - test -x "$MCPP" - cp "$MCPP" /tmp/mcpp-fresh - MCPP=/tmp/mcpp-fresh - "$MCPP" toolchain default "llvm@${MCPP_LLVM_VER}" - "$MCPP" clean - "$MCPP" build - "$MCPP" --version - - # Integration: the mcpp built from THIS PR's source (the self-host binary, - # $MCPP = /tmp/mcpp-fresh) builds & runs a real external C++ project — - # xlings (openxlings/xlings ships its own mcpp.toml). - - name: "Integration: mcpp builds & runs xlings (openxlings/xlings)" - env: - XLINGS_NON_INTERACTIVE: '1' - run: | - MCPP=/tmp/mcpp-fresh # the freshly self-hosted binary built from this PR - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - git clone --depth 1 --recurse-submodules \ - https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL - "$MCPP" build - "$MCPP" run diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 5318e5a7..2325f70a 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -220,6 +220,43 @@ jobs: cd hello_win "$MCPP_SELF" run + # windows-latest ships VS 2022 Enterprise with the VC workload, so + # msvc@system detection MUST succeed here — a failure is a regression + # in the discovery/identification chain (vswhere → env → paths). + # Runs BEFORE the LLVM self-host rebuild: that step cleans + rebuilds + # target/, invalidating this job's $MCPP_SELF fingerprint path. + - name: "Toolchain: MSVC — detection & selection (msvc@system)" + shell: bash + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + + # Neutral cwd: the repo root's mcpp.toml [toolchain] would shadow + # the global default in `toolchain list` / doctor output. + TMP=$(mktemp -d); cd "$TMP" + + out=$("$MCPP_SELF" toolchain default msvc); echo "$out" + grep -q "Detected" <<<"$out" + grep -q "msvc@system" <<<"$out" + + "$MCPP_SELF" toolchain list | tee tc-list.txt + grep -E '\*\s*msvc' tc-list.txt + + "$MCPP_SELF" self doctor 2>&1 | tee doctor.txt || true + grep -qi "msvc" doctor.txt + + # native cl.exe builds are gated with one owned message + "$MCPP_SELF" new hello_msvc && cd hello_msvc + set +e + bout=$("$MCPP_SELF" build 2>&1); brc=$? + set -e + echo "$bout" + test $brc -ne 0 + grep -q "not yet supported" <<<"$bout" + + # restore the LLVM default for the remaining steps + cd "$GITHUB_WORKSPACE" + "$MCPP_SELF" toolchain default llvm@20.1.7 + - name: "Toolchain: LLVM — build mcpp (self-host)" shell: bash run: | diff --git a/.github/workflows/cross-build-test.yml b/.github/workflows/cross-build-test.yml deleted file mode 100644 index 50a1d4f6..00000000 --- a/.github/workflows/cross-build-test.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: cross-build-test - -# mcpp cross-build test — the single source of truth for "which CROSS-build -# target combinations mcpp supports", verified end-to-end. -# -# Cross = host arch ≠ target arch. Verification targets are mcpp ITSELF and -# xlings (real, self-hosting C++23 module projects), cross-built from source for -# each target triple, arch-checked, and smoke-run under qemu-user. -# -# ── Supported cross matrix (built + verified below) ──────────────────────── -# target | toolchain | host→target | run -# ----------------------|---------------------------------|---------------|----- -# aarch64-linux-musl | aarch64-linux-musl-gcc@16.1.0 | x86_64→arm64 | qemu -# -# mcpp resolves a cross `--target -musl` build to the triple-named cross -# gcc musl toolchain from the xlings ecosystem (xim:-gcc, see -# src/build/prepare.cppm). Output is a fully static musl ELF (no PT_INTERP), -# which also makes the aarch64 artefact runnable natively in Termux/Android — -# qemu-aarch64 is the CI proxy for "does this cross artefact actually execute". -# -# ── NOT here ─────────────────────────────────────────────────────────────── -# * Same-arch builds (host arch == target arch) are NOT cross. The native musl -# static build `--target x86_64-linux-musl` (x86_64 host) is exercised by -# ci-linux.yml's "Toolchain: musl-gcc" step, and release.yml for the static -# release artefact. Keep them there; this file is cross-arch only. -# -# ── Planned cross rows (documented; NOT yet wired in mcpp — keep as comments) ─ -# * llvm/clang cross : clang is inherently a cross-compiler, but mcpp does not -# yet inject `-target ` + a cross sysroot for a -# clang toolchain; cross `--target` resolves to gcc musl -# only. Wire the clang cross path first, then add a row. -# * riscv64-linux-musl: add once xim:riscv64-linux-musl-gcc ships to -# xlings-res + xim-pkgindex. - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - workflow_dispatch: - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - cross-build: - name: cross-build ${{ matrix.target }} (mcpp + xlings) - runs-on: ubuntu-24.04 - timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - include: - - target: aarch64-linux-musl - file_arch: "ARM aarch64" - qemu_bin: qemu-aarch64-static - env: - MCPP_HOME: /home/runner/.mcpp - # Verbose every mcpp invocation for richer CI diagnostics (src/cli.cppm). - MCPP_VERBOSE: "1" - steps: - - uses: actions/checkout@v4 - - - name: Cache mcpp sandbox - uses: actions/cache@v4 - with: - path: ~/.mcpp - key: mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} - restore-keys: | - mcpp-sandbox-${{ runner.os }}-cross-${{ matrix.target }}- - - - name: Cache xlings - uses: actions/cache@v4 - with: - path: ~/.xlings - key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} - restore-keys: | - xlings-${{ runner.os }}-v2- - - - name: Install qemu-user-static - run: | - sudo apt-get update -qq - sudo apt-get install -y qemu-user-static - ${{ matrix.qemu_bin }} --version | head -1 - - - name: Bootstrap mcpp via xlings - env: - XLINGS_NON_INTERACTIVE: '1' - # 0.4.62 (current) — kept in lock-step with the xlings the release - # is built/bundled against (release.yml). A past 0.4.61 "download 404 - # for mcpp@" was NOT a version bug — the xlings-res/mcpp GitHub - # release assets were uploaded in a broken state (records present, - # blobs missing → 404 on GET); re-uploaded clean. The stale-INDEX - # half is handled by the marker-clear below. - XLINGS_VERSION: '0.4.62' - run: | - tarball="xlings-${XLINGS_VERSION}-linux-x86_64.tar.gz" - curl -fsSL -o "/tmp/${tarball}" \ - "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" - tar -xzf "/tmp/${tarball}" -C /tmp - "/tmp/xlings-${XLINGS_VERSION}-linux-x86_64/subos/default/bin/xlings" self install - export PATH="$HOME/.xlings/subos/default/bin:$PATH" - xlings --version - # Force a real index re-sync even on a warm cache: drop the TTL refresh - # markers so `xlings update` actually pulls the latest index (sees the - # current bootstrap pin) while the toolchain payloads stay cached. - find "$HOME/.xlings" -name '.xlings-index-cache.json' -delete 2>/dev/null || true - xlings config --mirror GLOBAL 2>/dev/null || true - xlings update -y 2>/dev/null || xlings update 2>/dev/null || true - xlings install mcpp -y - echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - echo "MCPP_BOOT=$HOME/.xlings/subos/default/bin/mcpp" >> "$GITHUB_ENV" - - - name: Self-host build (bootstrap mcpp -> fresh host mcpp) - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true - "$MCPP_BOOT" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP_BOOT" build - MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") - test -x "$MCPP" - "$MCPP" self config --mirror GLOBAL - echo "MCPP=$MCPP" >> "$GITHUB_ENV" - - - name: "Cross-build mcpp -> ${{ matrix.target }}" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - "$MCPP" build --target ${{ matrix.target }} - bin=$(find target/${{ matrix.target }} -type f -name mcpp | head -1) - [ -n "$bin" ] || { echo "no mcpp artefact for ${{ matrix.target }}"; exit 1; } - echo "== file =="; file "$bin" - file "$bin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } - file "$bin" | grep -q "statically linked" || { echo "expected static"; exit 1; } - echo "MCPP_XBIN=$bin" >> "$GITHUB_ENV" - - - name: "Cross-build xlings -> ${{ matrix.target }}" - run: | - export MCPP_VENDORED_XLINGS="$XLINGS_BIN" - git clone --depth 1 https://github.com/openxlings/xlings /tmp/xlings-src - cd /tmp/xlings-src - "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build --target ${{ matrix.target }} - xbin=$(find target/${{ matrix.target }} -type f -name xlings | head -1) - [ -n "$xbin" ] || { echo "no xlings artefact for ${{ matrix.target }}"; exit 1; } - echo "== file =="; file "$xbin" - file "$xbin" | grep -q "${{ matrix.file_arch }}" || { echo "expected ${{ matrix.file_arch }}"; exit 1; } - file "$xbin" | grep -q "statically linked" || { echo "expected static"; exit 1; } - echo "XLINGS_XBIN=$xbin" >> "$GITHUB_ENV" - - - name: "Smoke-run cross artefacts under qemu" - run: | - RUN="${{ matrix.qemu_bin }}" - # mcpp is self-contained, so --version runs cleanly under bare qemu — - # this is the hard execution proof for the cross artefact. - echo "== mcpp --version ==" - mver=$($RUN "$MCPP_XBIN" --version) - echo "$mver"; echo "$mver" | grep -q "mcpp" || { echo "mcpp --version failed"; exit 1; } - # xlings expects a real runtime environment (sandbox/config) and may - # exit non-zero on a bare `--version` under qemu; its ELF arch + static - # linkage were already asserted in the build step, so treat execution - # here as best-effort rather than gating. - echo "== xlings --version (best-effort under qemu) ==" - xver=$($RUN "$XLINGS_XBIN" --version 2>&1 || true) - echo "$xver" diff --git a/CHANGELOG.md b/CHANGELOG.md index e7dc1692..d45949bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.88] — 2026-07-13 + +### 新增 + +- **MSVC 系统工具链支持(`msvc@system`,detection-first)**。MSVC 作为首个 + "系统工具链"接入:mcpp 负责**定位与识别**(vswhere → `VSINSTALLDIR`/ + `VS*COMNTOOLS` → 标准安装路径三级发现;cl.exe banner 解析出编译器版本/架构, + 容错本地化 banner),**从不安装/卸载** MSVC 本体。 + - `mcpp toolchain default msvc`:检测系统 MSVC,打印 VS 产品/VC tools/ + cl 版本与 `std.ixx`(import std)可用性,持久化稳定 spec `msvc@system` + (不落具体版本,VS 升级后配置依然有效);未安装时输出安装指引 + (VS Installer C++ 工作负载 / `winget install …BuildTools`)并退出非零。 + `msvc@19.44` 形式为 pin 校验(仍取最新 VC tools,前缀不符则报错)。 + - `mcpp toolchain list`:Windows 上新增 `System:` 段展示检测到的 MSVC; + `install msvc` 报告已装现状或给指引;`remove msvc` 明确拒绝(系统组件)。 + - `mcpp self doctor`:Windows 上新增 "msvc (system)" 检查段。 + - manifest 支持 `[toolchain] windows = "msvc@system"`(types.cppm 注释中的 + 既有 schema 首次落地);非 Windows 主机使用 msvc spec 时给出明确报错。 + - `mcpp build`:原生 cl.exe 构建(.ifc 管线)**本版暂不支持**,在工具链解析 + 后以单一 owned 错误信息拦截,并提示可用的 `llvm@20.1.7`(MSVC-ABI Clang)。 + - detect() 新增 cl.exe 分类路径(文件名短路,banner → 版本/triple), + `bmi_traits` 预置 MSVC `.ifc` 分支;e2e 95/96 + 单测覆盖。 + - 设计文档:`.agents/docs/2026-07-13-msvc-system-toolchain-detection-design.md`。 + ## [0.0.87] — 2026-07-09 ### 修复 diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index b7e1422b..1ed8ef9a 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -69,6 +69,52 @@ Available (run `mcpp toolchain install `): The entry marked with `*` is the current default toolchain. `@mcpp/...` is shorthand for `~/.mcpp/...`, used to keep the output narrower. +## MSVC (System Toolchain, Windows) + +MSVC is different from every other toolchain mcpp manages: it is a **system +toolchain**. mcpp locates and identifies an installed Visual Studio / Build +Tools — it never installs, updates, or removes MSVC itself. + +```bash +mcpp toolchain default msvc +``` + +On a machine with MSVC installed, mcpp auto-locates it (via `vswhere.exe`, +then `VSINSTALLDIR`/`VS*COMNTOOLS`, then the standard install paths), +identifies the versions involved, and persists the stable spec `msvc@system`: + +``` +Detected msvc 19.44.35211 (VS 2022 BuildTools) (VC tools 14.44.35207) + cl: C:\Program Files\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.44.35207\bin\Hostx64\x64\cl.exe + import std: available (std.ixx) +Default set to msvc@system (was: llvm@20.1.7) +``` + +If MSVC is **not** installed, mcpp prints installation guidance instead +(Visual Studio Installer with the *Desktop development with C++* workload, or +`winget install Microsoft.VisualStudio.2022.BuildTools`) and exits non-zero — +install it yourself, then re-run the command. + +`mcpp toolchain list` shows the detected MSVC in a separate `System:` section, +and `mcpp self doctor` reports its status on Windows. In a manifest you can +pin it per-platform: + +```toml +[toolchain] +windows = "msvc@system" +``` + +`msvc@` (e.g. `msvc@19.44`) acts as a pin-verify: mcpp still uses the +newest installed VC tools, but errors if the detected version doesn't match +the prefix. + +> [!NOTE] +> Selection and detection are supported today; **building with native MSVC +> (cl.exe) is not yet supported** — `mcpp build` fails with a clear message +> naming the detected version. For building on Windows use the MSVC-ABI Clang +> toolchain: `mcpp toolchain default llvm@20.1.7` (it borrows the MSVC STL +> from the same detected installation). + ## Project-Level Version Pinning If a project needs to pin a specific version rather than rely on the global default, declare it in the project's `mcpp.toml`: diff --git a/mcpp.toml b/mcpp.toml index 130318bd..e75db21d 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.87" +version = "0.0.88" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 02216dd1..3157fe33 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -18,6 +18,7 @@ import mcpp.modgraph.validate; import mcpp.toolchain.clang; import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; +import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.stdmod; import mcpp.toolchain.post_install; @@ -682,7 +683,28 @@ prepare_build(bool print_fingerprint, } } - if (tcSpec.has_value() && *tcSpec != "system") { + // msvc@system: a *system* toolchain — located on the machine, never + // resolved through xim packages. mcpp does not install MSVC. + bool tcSpecIsMsvc = false; + if (tcSpec.has_value()) { + if (auto s = mcpp::toolchain::parse_toolchain_spec(*tcSpec); + s && mcpp::toolchain::is_system_toolchain(*s)) + tcSpecIsMsvc = true; + } + if (tcSpecIsMsvc) { + if (!mcpp::platform::is_windows) { + return std::unexpected(std::format( + "toolchain '{}' is only available on Windows hosts", *tcSpec)); + } + auto inst = mcpp::toolchain::msvc::detect_installation(); + if (!inst) { + return std::unexpected(mcpp::toolchain::msvc::install_guidance()); + } + explicit_compiler = inst->clPath; + mcpp::ui::info("Resolved", std::format( + "msvc@system → msvc {} ({})", + inst->display_version(), inst->clPath.string())); + } else if (tcSpec.has_value() && *tcSpec != "system") { auto spec = mcpp::toolchain::parse_toolchain_spec(*tcSpec); if (!spec || spec->version.empty()) { return std::unexpected(std::format( @@ -841,6 +863,19 @@ prepare_build(bool print_fingerprint, auto tc = mcpp::toolchain::detect(explicit_compiler); if (!tc) return std::unexpected(tc.error().message); + // Native MSVC builds are gated off until the cl.exe/.ifc pipeline lands. + // Selection & detection (`mcpp toolchain default msvc`, `list`, `doctor`) + // work; the build must fail here with one owned message rather than an + // incidental error deep in the GCC/Clang-shaped flag machinery. + if (tc->compiler == mcpp::toolchain::CompilerId::MSVC) { + return std::unexpected(std::format( + "native MSVC (cl.exe) builds are not yet supported by mcpp.\n" + " detected: msvc {} at {} (selection & detection work today)\n" + " for building on Windows use the MSVC-ABI Clang toolchain instead:\n" + " mcpp toolchain default llvm@20.1.7", + tc->version, tc->binaryPath.string())); + } + // For musl-gcc the toolchain is fully self-contained // (`/x86_64-linux-musl/{include,lib}` is its own sysroot). // musl-gcc's `-dumpmachine` reports `x86_64-linux-musl`. diff --git a/src/doctor.cppm b/src/doctor.cppm index 1c67e8e6..60e168aa 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -16,8 +16,10 @@ import mcpp.build.plan; import mcpp.config; import mcpp.fallback.install_integrity; import mcpp.fetcher.progress; +import mcpp.platform; import mcpp.platform.process; import mcpp.toolchain.detect; +import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.stdmod; import mcpp.toolchain.abi; @@ -96,6 +98,30 @@ export int doctor_report() { ok(std::format("{} at {}", tc->label(), tc->binaryPath.string())); } + // Windows: report the system MSVC (msvc@system). Absence is a warning, + // not an error — mcpp works with LLVM/Clang without it, and mcpp never + // installs MSVC itself. + if (mcpp::platform::is_windows) { + mcpp::ui::status("Checking", "msvc (system)"); + if (auto inst = mcpp::toolchain::msvc::detect_installation()) { + ok(std::format("msvc {}{} (VC tools {})", + inst->display_version(), + inst->vsProduct.empty() + ? std::string{} + : std::format(" (VS {})", inst->vsProduct), + inst->toolsVersion)); + ok(std::format("cl at {}", inst->clPath.string())); + if (inst->hasStdModules) { + ok("import std: std.ixx available"); + } else { + warn("MSVC STL std.ixx missing (VC tools too old for import std?)"); + } + } else { + warn("msvc not detected — run `mcpp toolchain default msvc` for " + "setup guidance (mcpp does not install MSVC)"); + } + } + mcpp::ui::status("Checking", "std module"); if (tc) { auto cacheRoot = mcpp::toolchain::default_cache_root(); diff --git a/src/toolchain/detect.cppm b/src/toolchain/detect.cppm index f0d9c478..c3079e01 100644 --- a/src/toolchain/detect.cppm +++ b/src/toolchain/detect.cppm @@ -8,6 +8,7 @@ export import mcpp.toolchain.probe; import std; import mcpp.toolchain.clang; import mcpp.toolchain.gcc; +import mcpp.toolchain.msvc; import mcpp.xlings; export namespace mcpp::toolchain { @@ -38,6 +39,17 @@ detect(const std::filesystem::path& explicit_compiler) { Toolchain tc; tc.binaryPath = *bin_r; + + // MSVC cl.exe has no --version / -dumpmachine / -print-sysroot; classify + // it by filename and take a dedicated enrich path (banner → version, + // arch → triple, std.ixx lookup). No runtime dirs / sysroot / payloads — + // those concepts are GCC/Clang-shaped. + if (auto stem = lower_copy(tc.binaryPath.stem().string()); stem == "cl") { + if (auto r = mcpp::toolchain::msvc::enrich_toolchain_from_cl(tc); !r) + return std::unexpected(r.error()); + return tc; + } + tc.compilerRuntimeDirs = discover_compiler_runtime_dirs(tc.binaryPath); auto envPrefix = compiler_env_prefix(tc); diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 7b30e0f8..cb12886f 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.87"; +inline constexpr std::string_view MCPP_VERSION = "0.0.88"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 83405fbb..220ab1dc 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -15,6 +15,7 @@ import mcpp.fetcher.progress; import mcpp.manifest; import mcpp.platform; import mcpp.toolchain.detect; +import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.post_install; import mcpp.ui; @@ -164,6 +165,27 @@ struct EffectiveDefault { bool fromProject = false; }; +// ─── msvc@system: system-toolchain helpers ────────────────────────────── +// +// MSVC is located and identified, never installed/removed by mcpp. All four +// subcommands branch here before the xim-package path. + +int msvc_wrong_host() { + mcpp::ui::error("the msvc toolchain is only available on Windows hosts"); + return 1; +} + +void msvc_print_detected(const mcpp::toolchain::msvc::MsvcInstallation& inst) { + mcpp::ui::status("Detected", std::format( + "msvc {}{} (VC tools {})", + inst.display_version(), + inst.vsProduct.empty() ? "" : std::format(" (VS {})", inst.vsProduct), + inst.toolsVersion)); + std::println(" cl: {}", inst.clPath.string()); + std::println(" import std: {}", + inst.hasStdModules ? "available (std.ixx)" : "not available"); +} + EffectiveDefault effective_default_toolchain(const mcpp::config::GlobalConfig& cfg) { std::error_code ec; auto mpath = std::filesystem::current_path(ec) / "mcpp.toml"; @@ -234,6 +256,26 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { } } + // ─── System section (Windows: detected MSVC) ──────────────────── + // MSVC is never in xpkgs — it's located on the machine. Show it so + // `toolchain list` reflects everything `toolchain default` accepts. + if (mcpp::platform::is_windows) { + if (auto inst = mcpp::toolchain::msvc::detect_installation()) { + bool isDefault = mcpp::toolchain::matches_default_toolchain( + effective.spec, "msvc", inst->display_version()); + std::println(""); + std::println("System:"); + std::println(" {:<3}{:<22} {}", + isDefault ? "*" : "", + mcpp::toolchain::display_label("msvc", inst->display_version()), + inst->clPath.string()); + } else { + std::println(""); + std::println(" (msvc: not detected — run `mcpp toolchain default msvc` " + "for setup guidance)"); + } + } + // ─── Available section ────────────────────────────────────────── // List xim:gcc + xim:musl-gcc versions known to the local index // that aren't already installed. Helpful to discover what users @@ -280,6 +322,34 @@ export int toolchain_list(const mcpp::config::GlobalConfig& cfg) { // `mcpp toolchain install ` — install + post-install fixups. export int toolchain_install(const mcpp::config::GlobalConfig& cfg, const std::string& pos0, const std::string& pos1) { + // Accept three input shapes — they all collapse to (compiler, version): + // mcpp toolchain install gcc 16.1.0 → ("gcc", "16.1.0") + // mcpp toolchain install gcc@16.1.0 → ("gcc", "16.1.0") + // mcpp toolchain install gcc 15 → ("gcc", "15") partial + // (parsed before the bootstrap check: system toolchains need no + // bootstrap, and an invalid spec should not report a bootstrap error) + auto spec = mcpp::toolchain::parse_toolchain_spec( + pos0, pos1); + if (!spec || spec->compiler.empty()) { + mcpp::ui::error("missing compiler name; e.g. `mcpp toolchain install gcc 16.1.0`"); + return 2; + } + + // msvc@system: mcpp never installs MSVC — report what's there, or + // print installation guidance. + if (mcpp::toolchain::is_system_toolchain(*spec)) { + if (!mcpp::platform::is_windows) return msvc_wrong_host(); + if (auto inst = mcpp::toolchain::msvc::detect_installation()) { + msvc_print_detected(*inst); + std::println(""); + std::println("MSVC is already installed — mcpp does not manage it."); + std::println("Tip: `mcpp toolchain default msvc` to make it the default."); + return 0; + } + mcpp::ui::error(mcpp::toolchain::msvc::install_guidance()); + return 1; + } + // Toolchain install needs patchelf (ELF fixup) and ninja (build). // Fail early if bootstrap is incomplete rather than producing a // broken toolchain with missing fixups. @@ -291,16 +361,6 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, return 1; } - // Accept three input shapes — they all collapse to (compiler, version): - // mcpp toolchain install gcc 16.1.0 → ("gcc", "16.1.0") - // mcpp toolchain install gcc@16.1.0 → ("gcc", "16.1.0") - // mcpp toolchain install gcc 15 → ("gcc", "15") partial - auto spec = mcpp::toolchain::parse_toolchain_spec( - pos0, pos1); - if (!spec || spec->compiler.empty()) { - mcpp::ui::error("missing compiler name; e.g. `mcpp toolchain install gcc 16.1.0`"); - return 2; - } auto pkg = mcpp::toolchain::to_xim_package(*spec); // Partial-version resolution: `gcc 15` → highest available 15.x.y in @@ -385,6 +445,39 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, mcpp::ui::error("missing spec; e.g. `mcpp toolchain default gcc@16.1.0`"); return 2; } + + // msvc@system: locate + identify the system MSVC, persist the stable + // spec (never a concrete version — config survives VS updates). + if (mcpp::toolchain::is_system_toolchain(*spec)) { + if (!mcpp::platform::is_windows) return msvc_wrong_host(); + auto inst = mcpp::toolchain::msvc::detect_installation(); + if (!inst) { + mcpp::ui::error(mcpp::toolchain::msvc::install_guidance()); + return 1; + } + // `msvc@19.44` is a pin-verify against the detected install, not + // a selection among many — mcpp always uses the newest VC tools. + if (!spec->version.empty() && spec->version != "system" + && !inst->display_version().starts_with(spec->version)) { + mcpp::ui::error(std::format( + "msvc@{} requested, but the system MSVC is {} (VC tools {})", + spec->version, inst->display_version(), inst->toolsVersion)); + return 1; + } + msvc_print_detected(*inst); + auto wr = mcpp::config::write_default_toolchain(cfg, "msvc@system"); + if (!wr) { + mcpp::ui::error(wr.error().message); + return 1; + } + mcpp::ui::status("Default", std::format( + "set to msvc@system (was: {})", + cfg.defaultToolchain.empty() ? "" : cfg.defaultToolchain)); + std::println("note: `mcpp build` with native MSVC (cl.exe) is not yet " + "supported — coming in a later release."); + return 0; + } + auto pkg = mcpp::toolchain::to_xim_package(*spec); // Partial-version resolution against installed payloads. @@ -421,6 +514,11 @@ export int toolchain_remove(const mcpp::config::GlobalConfig& cfg, const std::string& pos0) { auto xlEnv = mcpp::config::make_xlings_env(cfg); auto parsedSpec = mcpp::toolchain::parse_toolchain_spec(pos0); + if (parsedSpec && mcpp::toolchain::is_system_toolchain(*parsedSpec)) { + mcpp::ui::error("msvc is a system toolchain managed by the Visual " + "Studio Installer — mcpp cannot remove it"); + return 1; + } if (!parsedSpec || parsedSpec->version.empty()) { mcpp::ui::error(std::format("invalid spec '{}'", pos0)); return 2; diff --git a/src/toolchain/model.cppm b/src/toolchain/model.cppm index 58ac2083..288c8dcc 100644 --- a/src/toolchain/model.cppm +++ b/src/toolchain/model.cppm @@ -85,6 +85,18 @@ bool is_msvc_target(const Toolchain& tc) { } BmiTraits bmi_traits(const Toolchain& tc) { + if (tc.compiler == CompilerId::MSVC) { + // Native cl.exe builds are gated off until the .ifc pipeline lands; + // these traits exist so nothing silently reuses the GCC defaults. + return { + .bmiDir = "ifc.cache", + .bmiExt = ".ifc", + .manifestPrefix = "ifc", + .needsExplicitModuleOutput = true, + .needsPrebuiltModulePath = false, + .scanNeedsFModules = false, + }; + } if (is_clang(tc)) { return { .bmiDir = "pcm.cache", diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index ffe786c9..b2109aab 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -18,6 +18,8 @@ export module mcpp.toolchain.msvc; import std; import mcpp.platform; +import mcpp.toolchain.model; +import mcpp.toolchain.probe; export namespace mcpp::toolchain::msvc { @@ -33,6 +35,46 @@ std::optional find_std_module_source(); // Find cl.exe (for future MSVC toolchain support). std::optional find_cl(); +// ─── System-toolchain detection (msvc@system) ──────────────────────────── +// +// mcpp treats MSVC as a *system* toolchain: it locates and identifies an +// installed Visual Studio / Build Tools, but never installs or removes one. + +struct MsvcInstallation { + std::filesystem::path vsRoot; // …\Microsoft Visual Studio\2022\BuildTools + std::string vsProduct; // "2022 BuildTools" (path-derived; may be empty) + std::string toolsVersion; // "14.44.35207" (VC\Tools\MSVC\) + std::filesystem::path clPath; // …\bin\Hostx64\x64\cl.exe + std::string clVersion; // "19.44.35211" (banner; empty if unparseable) + std::string arch; // "x64" | "x86" | "arm64" + bool hasStdModules = false; // modules\std.ixx present + + // Preferred user-facing version: compiler version, else tools version. + std::string display_version() const { + return clVersion.empty() ? toolsVersion : clVersion; + } +}; + +// Locate the best (newest) usable installation. nullopt = MSVC absent. +std::optional detect_installation(); + +// Parse a cl.exe banner into (version, arch). Token-based so localized +// banners work: first "d.d.d[.d]" run is the version, arch is the arm64/x64/ +// x86 token. Pure and cross-platform for unit testing. +std::optional> +parse_cl_banner(std::string_view banner); + +// Map a cl banner arch token to the canonical windows-msvc triple. +std::string triple_for_arch(std::string_view arch); + +// Multi-line guidance shown wherever MSVC is required but absent. +// States what was searched and how to install (mcpp does not install MSVC). +std::string install_guidance(); + +// Classify + enrich an already-probed cl.exe binary for detect(): +// version/arch from the banner, targetTriple, driverIdent, std.ixx lookup. +std::expected enrich_toolchain_from_cl(Toolchain& tc); + } // namespace mcpp::toolchain::msvc namespace mcpp::toolchain::msvc { @@ -185,4 +227,162 @@ std::optional find_cl() { return std::nullopt; } +// ─── System-toolchain detection ────────────────────────────────────────── + +std::optional> +parse_cl_banner(std::string_view banner) { + // Version: first digit/dot run with at least two dots ("19.44.35211", + // possibly four components). Never anchored to the English word + // "Version" — localized banners reorder the sentence. + std::string version; + { + std::string run; + int dots = 0; + auto flush = [&] { + if (version.empty() && dots >= 2 && run.back() != '.') + version = run; + run.clear(); + dots = 0; + }; + for (char c : banner) { + if (c >= '0' && c <= '9') { run += c; } + else if (c == '.' && !run.empty()) { run += c; ++dots; } + else if (!run.empty()) { flush(); } + if (!version.empty()) break; + } + if (!run.empty()) flush(); + } + if (version.empty()) return std::nullopt; + + auto lower = mcpp::toolchain::lower_copy(banner); + std::string arch; + if (lower.find("arm64") != std::string::npos) arch = "arm64"; + else if (lower.find("x64") != std::string::npos) arch = "x64"; + else if (lower.find("x86") != std::string::npos) arch = "x86"; + + return std::pair{version, arch}; +} + +std::string triple_for_arch(std::string_view arch) { + if (arch == "arm64") return "aarch64-pc-windows-msvc"; + if (arch == "x86") return "i686-pc-windows-msvc"; + return "x86_64-pc-windows-msvc"; +} + +std::string install_guidance() { + return + "MSVC was not found on this system.\n" + " searched: vswhere.exe, VSINSTALLDIR / VS*COMNTOOLS, and the standard\n" + " 'Program Files\\Microsoft Visual Studio\\\\' paths\n" + " mcpp does not install MSVC — install it yourself, then retry:\n" + " - Visual Studio Installer: add the 'Desktop development with C++' workload\n" + " (component: Microsoft.VisualStudio.Component.VC.Tools.x86.x64)\n" + " - or Build Tools only: winget install Microsoft.VisualStudio.2022.BuildTools\n" + " then add the C++ workload in the installer\n" + " afterwards run: mcpp toolchain default msvc"; +} + +namespace { + +// "…\Microsoft Visual Studio\2022\BuildTools" → "2022 BuildTools". +[[maybe_unused]] std::string product_from_vs_root(const std::filesystem::path& vsRoot) { + std::vector parts; + for (auto& seg : vsRoot) parts.push_back(seg.string()); + for (std::size_t i = 0; i + 2 < parts.size(); ++i) { + if (parts[i] == "Microsoft Visual Studio") + return parts[i + 1] + " " + parts[i + 2]; + } + return {}; +} + +// Capture cl.exe's banner. cl prints it (plus a usage complaint) when run +// bare; the exit status is irrelevant — parse whatever came out. +std::string capture_cl_banner(const std::filesystem::path& cl) { + auto r = mcpp::platform::process::capture( + "\"" + cl.string() + "\" 2>&1"); + return r.output; +} + +} // namespace + +std::optional detect_installation() { +#if defined(_WIN32) + auto vs = find_vs_install_path(); + if (!vs) return std::nullopt; + auto tools = find_latest_msvc_tools(*vs); + if (!tools) return std::nullopt; + + MsvcInstallation inst; + inst.vsRoot = *vs; + inst.vsProduct = product_from_vs_root(*vs); + inst.toolsVersion = tools->filename().string(); + + // Host-native bin dir first (arm64 hosts run arm64 cl; everything else + // x64), with the remaining pairs as fallback. + std::vector> pairs; + if (mcpp::platform::host_arch == std::string_view("aarch64") + || mcpp::platform::host_arch == std::string_view("arm64")) { + pairs = {{"Hostarm64", "arm64"}, {"Hostx64", "x64"}, {"Hostx86", "x86"}}; + } else { + pairs = {{"Hostx64", "x64"}, {"Hostarm64", "arm64"}, {"Hostx86", "x86"}}; + } + for (auto [host, target] : pairs) { + auto cl = *tools / "bin" / host / target / "cl.exe"; + std::error_code ec; + if (std::filesystem::exists(cl, ec)) { + inst.clPath = cl; + inst.arch = std::string(target); + break; + } + } + if (inst.clPath.empty()) return std::nullopt; + + std::error_code ec; + inst.hasStdModules = + std::filesystem::exists(*tools / "modules" / "std.ixx", ec); + + // Version identification: banner is authoritative; tolerate failure + // (clVersion stays empty and display_version() falls back to the + // tools-dir version). + if (auto parsed = parse_cl_banner(capture_cl_banner(inst.clPath))) { + inst.clVersion = parsed->first; + if (!parsed->second.empty()) inst.arch = parsed->second; + } + return inst; +#else + return std::nullopt; +#endif +} + +std::expected enrich_toolchain_from_cl(Toolchain& tc) { + auto banner = capture_cl_banner(tc.binaryPath); + auto parsed = parse_cl_banner(banner); + if (!parsed) { + return std::unexpected(DetectError{std::format( + "'{}' looks like MSVC cl but produced no recognizable banner:\n{}", + tc.binaryPath.string(), banner)}); + } + tc.compiler = CompilerId::MSVC; + tc.version = parsed->first; + tc.targetTriple = triple_for_arch(parsed->second); + tc.driverIdent = mcpp::toolchain::normalize_driver_output(banner); + + // MSVC STL ships std.ixx next to the tools dir the cl binary lives in: + // \bin\Host*\*\cl.exe → \modules\std.ixx. + auto toolsDir = tc.binaryPath.parent_path() // x64 + .parent_path() // Hostx64 + .parent_path() // bin + .parent_path(); // + std::error_code ec; + if (auto ixx = toolsDir / "modules" / "std.ixx"; + std::filesystem::exists(ixx, ec)) { + tc.stdModuleSource = ixx; + tc.hasImportStd = true; + } else if (auto found = find_std_module_source()) { + tc.stdModuleSource = *found; + tc.hasImportStd = true; + } + return {}; +} + } // namespace mcpp::toolchain::msvc diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index a92688bd..d01a71a5 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -58,6 +58,11 @@ bool matches_default_toolchain(std::string_view configuredDefault, std::string_view compiler, std::string_view version); +// System toolchains are located on the machine, never installed/removed by +// mcpp. Today that's MSVC (`msvc@system`); the PATH-compiler escape hatch +// (`[toolchain] … = "system"`) is a separate, older mechanism. +bool is_system_toolchain(const ToolchainSpec& spec); + std::vector> available_toolchain_indexes(); std::filesystem::path derive_c_compiler(const Toolchain& tc); @@ -227,9 +232,16 @@ bool matches_default_toolchain(std::string_view configuredDefault, && configuredDefault == std::format("gcc@{}-musl", version)) { return true; } + // The persisted msvc default is always the stable "msvc@system" (never a + // concrete version), so it matches whatever version detection reports. + if (compiler == "msvc" && configuredDefault == "msvc@system") return true; return false; } +bool is_system_toolchain(const ToolchainSpec& spec) { + return spec.compiler == "msvc"; +} + std::vector> available_toolchain_indexes() { return { {"gcc", "gcc"}, diff --git a/tests/e2e/95_msvc_system_toolchain.sh b/tests/e2e/95_msvc_system_toolchain.sh new file mode 100755 index 00000000..2cdd9f88 --- /dev/null +++ b/tests/e2e/95_msvc_system_toolchain.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# requires: msvc +# 95_msvc_system_toolchain.sh — msvc@system detection & selection (Windows): +# - `toolchain default msvc` locates + identifies the system MSVC and +# persists the stable spec msvc@system +# - `toolchain list` shows the detected MSVC in a System section, starred +# - version pin-verify: `msvc@99` mismatches the detected install +# - `toolchain remove/install msvc`: mcpp never manages MSVC itself +# - `mcpp build` fails with the owned "not yet supported" gate message +set -e + +# This test flips the global default toolchain; save + restore it so later +# tests / CI steps keep their configured toolchain. +CONF="${MCPP_HOME:-$HOME/.mcpp}/config.toml" +ORIG_DEFAULT="" +if [[ -f "$CONF" ]]; then + ORIG_DEFAULT=$(sed -n '/^\[toolchain\]/,/^\[/p' "$CONF" \ + | grep '^default' | head -1 | cut -d'"' -f2 || true) +fi +TMP=$(mktemp -d) +restore() { + if [[ -n "$ORIG_DEFAULT" ]]; then + "$MCPP" toolchain default "$ORIG_DEFAULT" >/dev/null 2>&1 || true + fi + rm -rf "$TMP" +} +trap restore EXIT + +# Neutral cwd: `toolchain list` stars the *effective* default, and a project +# mcpp.toml [toolchain] in the cwd (e.g. the mcpp repo root, where run_all.sh +# executes) would shadow the global default we're about to set. +cd "$TMP" + +# 1) switch to msvc: detect + identify + persist +out=$("$MCPP" toolchain default msvc 2>&1) || { echo "FAIL: default msvc: $out"; exit 1; } +[[ "$out" == *"Detected"* ]] || { echo "FAIL: no Detected line: $out"; exit 1; } +[[ "$out" == *"msvc "* ]] || { echo "FAIL: no msvc version: $out"; exit 1; } +[[ "$out" == *"msvc@system"* ]] || { echo "FAIL: default not msvc@system: $out"; exit 1; } +[[ "$out" == *"cl:"* ]] || { echo "FAIL: no cl path: $out"; exit 1; } + +# 2) list shows the System section with the effective-default star +out=$("$MCPP" toolchain list 2>&1) +[[ "$out" == *"System:"* ]] || { echo "FAIL: no System section: $out"; exit 1; } +echo "$out" | grep -E '\*\s*msvc' >/dev/null \ + || { echo "FAIL: msvc row not starred as default: $out"; exit 1; } + +# 3) version pin-verify: an impossible major must mismatch +rc=0; out=$("$MCPP" toolchain default msvc@99 2>&1) || rc=$? +[[ $rc -ne 0 ]] || { echo "FAIL: msvc@99 should mismatch"; exit 1; } +[[ "$out" == *"requested"* ]] || { echo "FAIL: mismatch message: $out"; exit 1; } + +# 4) mcpp never manages the MSVC installation +rc=0; out=$("$MCPP" toolchain remove msvc 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"system toolchain"* ]] \ + || { echo "FAIL: remove msvc: rc=$rc out=$out"; exit 1; } +out=$("$MCPP" toolchain install msvc 2>&1) \ + || { echo "FAIL: install msvc (present) should exit 0: $out"; exit 1; } +[[ "$out" == *"already installed"* ]] \ + || { echo "FAIL: install msvc message: $out"; exit 1; } + +# 5) native cl.exe builds are gated with one owned message +"$MCPP" new hello_msvc >/dev/null 2>&1 +cd hello_msvc +rc=0; out=$("$MCPP" build 2>&1) || rc=$? +[[ $rc -ne 0 ]] || { echo "FAIL: build should be gated"; exit 1; } +[[ "$out" == *"not yet supported"* ]] || { echo "FAIL: gate message: $out"; exit 1; } +[[ "$out" == *"llvm@20.1.7"* ]] || { echo "FAIL: no alternative hint: $out"; exit 1; } + +echo "PASS: msvc@system detection, selection, guidance, and build gate" diff --git a/tests/e2e/96_msvc_unavailable_nonwindows.sh b/tests/e2e/96_msvc_unavailable_nonwindows.sh new file mode 100755 index 00000000..41e07d06 --- /dev/null +++ b/tests/e2e/96_msvc_unavailable_nonwindows.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# requires: unix-shell +# 96_msvc_unavailable_nonwindows.sh — msvc@system is Windows-only: +# - `toolchain default/install msvc` fail with a host error (no network, +# no sandbox mutation) +# - `toolchain list` has no System section off-Windows +# - a manifest pinned to msvc@system fails the same way at build time +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +rc=0; out=$("$MCPP" toolchain default msvc 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"only available on Windows"* ]] \ + || { echo "FAIL: default msvc: rc=$rc out=$out"; exit 1; } + +rc=0; out=$("$MCPP" toolchain install msvc 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"only available on Windows"* ]] \ + || { echo "FAIL: install msvc: rc=$rc out=$out"; exit 1; } + +out=$("$MCPP" toolchain list 2>&1) || true +[[ "$out" != *"System:"* ]] || { echo "FAIL: System section off-Windows: $out"; exit 1; } + +# Manifest pinned to msvc@system errors out at resolution, not mid-build. +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" +cat > mcpp.toml < src/main.cpp <<'EOF' +import std; +int main() { std::println("never built"); return 0; } +EOF +rc=0; out=$("$MCPP" build 2>&1) || rc=$? +[[ $rc -ne 0 && "$out" == *"only available on Windows"* ]] \ + || { echo "FAIL: build with msvc@system manifest: rc=$rc out=$out"; exit 1; } + +echo "PASS: msvc@system correctly rejected off-Windows" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index 6fff57ae..dade949b 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -67,6 +67,15 @@ case "$OS" in if [[ "${MSYS:-}" == *winsymlinks* ]] || cmd.exe /c "mklink /?" &>/dev/null 2>&1; then CAPS+=(symlink) fi + # msvc: a system Visual Studio / Build Tools with the VC workload + # (what `mcpp toolchain default msvc` must be able to detect). + VSWHERE="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" + if [[ -f "$VSWHERE" ]] \ + && "$VSWHERE" -latest -products '*' \ + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 \ + -property installationPath 2>/dev/null | grep -q .; then + CAPS+=(msvc) + fi # NOTE: Windows runners may have g++.exe (MinGW/Strawberry) in PATH # but it's not a proper mcpp-compatible GCC. Don't add gcc capability. # fresh-sandbox: not yet reliable on Windows — xlings LLVM auto-install diff --git a/tests/unit/test_toolchain_msvc.cpp b/tests/unit/test_toolchain_msvc.cpp new file mode 100644 index 00000000..a504834f --- /dev/null +++ b/tests/unit/test_toolchain_msvc.cpp @@ -0,0 +1,96 @@ +#include + +import std; +import mcpp.toolchain.model; +import mcpp.toolchain.msvc; +import mcpp.toolchain.registry; + +using namespace mcpp::toolchain; + +// ─── parse_cl_banner ───────────────────────────────────────────────────── + +TEST(MsvcBanner, ParsesEnglishBanner) { + auto r = msvc::parse_cl_banner( + "Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35211 for x64\n" + "Copyright (C) Microsoft Corporation. All rights reserved.\n" + "\n" + "usage: cl [ option... ] filename... [ /link linkoption... ]\n"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->first, "19.44.35211"); + EXPECT_EQ(r->second, "x64"); +} + +TEST(MsvcBanner, ParsesLocalizedBannerByTokens) { + // Chinese VS reorders the sentence; only the tokens are stable. + auto r = msvc::parse_cl_banner( + "用于 x64 的 Microsoft (R) C/C++ 优化编译器 19.44.35211 版\n" + "版权所有(C) Microsoft Corporation。保留所有权利。\n"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->first, "19.44.35211"); + EXPECT_EQ(r->second, "x64"); +} + +TEST(MsvcBanner, ParsesArm64AndFourComponentVersions) { + auto r = msvc::parse_cl_banner( + "Microsoft (R) C/C++ Optimizing Compiler Version 19.29.30133.0 for ARM64\n"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->first, "19.29.30133.0"); + EXPECT_EQ(r->second, "arm64"); +} + +TEST(MsvcBanner, RejectsGarbage) { + EXPECT_FALSE(msvc::parse_cl_banner("").has_value()); + EXPECT_FALSE(msvc::parse_cl_banner("bash: cl: command not found").has_value()); + // A bare two-component number is not a cl version. + EXPECT_FALSE(msvc::parse_cl_banner("something 1.2 else").has_value()); +} + +TEST(MsvcBanner, TripleForArch) { + EXPECT_EQ(msvc::triple_for_arch("x64"), "x86_64-pc-windows-msvc"); + EXPECT_EQ(msvc::triple_for_arch("x86"), "i686-pc-windows-msvc"); + EXPECT_EQ(msvc::triple_for_arch("arm64"), "aarch64-pc-windows-msvc"); + // Unknown/empty arch falls back to the x64 triple. + EXPECT_EQ(msvc::triple_for_arch(""), "x86_64-pc-windows-msvc"); +} + +// ─── install guidance ──────────────────────────────────────────────────── + +TEST(MsvcGuidance, MentionsInstallRoutesAndOwnership) { + auto g = msvc::install_guidance(); + ASSERT_FALSE(g.empty()); + EXPECT_NE(g.find("winget"), std::string::npos); + EXPECT_NE(g.find("does not install"), std::string::npos); + EXPECT_NE(g.find("mcpp toolchain default msvc"), std::string::npos); +} + +// ─── spec layer ────────────────────────────────────────────────────────── + +TEST(MsvcSpec, SystemToolchainClassification) { + for (auto s : {"msvc", "msvc@system", "msvc@19.44"}) { + auto spec = parse_toolchain_spec(s); + ASSERT_TRUE(spec.has_value()) << s; + EXPECT_TRUE(is_system_toolchain(*spec)) << s; + } + auto gcc = parse_toolchain_spec("gcc@16.1.0"); + ASSERT_TRUE(gcc.has_value()); + EXPECT_FALSE(is_system_toolchain(*gcc)); +} + +TEST(MsvcSpec, StableDefaultMatchesAnyDetectedVersion) { + EXPECT_TRUE(matches_default_toolchain("msvc@system", "msvc", "19.44.35211")); + EXPECT_TRUE(matches_default_toolchain("msvc@system", "msvc", "19.29.30133")); + EXPECT_FALSE(matches_default_toolchain("msvc@system", "gcc", "16.1.0")); + EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "msvc", "19.44.35211")); +} + +// ─── model traits ──────────────────────────────────────────────────────── + +TEST(MsvcModel, BmiTraitsUseIfc) { + Toolchain tc; + tc.compiler = CompilerId::MSVC; + auto t = bmi_traits(tc); + EXPECT_EQ(t.bmiDir, "ifc.cache"); + EXPECT_EQ(t.bmiExt, ".ifc"); + EXPECT_TRUE(t.needsExplicitModuleOutput); + EXPECT_FALSE(t.scanNeedsFModules); +}