diff --git a/.agents/docs/2026-07-13-single-pr-090-implementation-plan.md b/.agents/docs/2026-07-13-single-pr-090-implementation-plan.md new file mode 100644 index 00000000..50824df5 --- /dev/null +++ b/.agents/docs/2026-07-13-single-pr-090-implementation-plan.md @@ -0,0 +1,76 @@ +# 0.0.90 单 PR 实施计划(post-089 路线图全量) + +> 2026-07-13 · 执行 `2026-07-13-post-089-roadmap-and-std-dialect-flags-design.md` 的 +> **全部内容**于一个 PR。步骤是顺序实施的里程碑(每步本地 build+test 过),不拆 PR。 + +## 步骤 + +**S1 — D1 方言旗标(#210)** +manifest 解析 `[build] dialect_cxxflags`;`extract_dialect_flags(cxxflags)` +(known-list:reflection/contracts/char8_t/`-D_GLIBCXX_USE_CXX11_ABI=`; +fno-exceptions/rtti 首版不入);`BuildPlan.dialectFlags`;注入:flags.cppm 全局 +`$cxxflags`(cxx_std_flag 旁)、`ensure_built` 新形参 → gcc/clang/msvc std 命令、 +metadata 自然失效。单测 + e2e `98_reflection_import_std.sh`(两变体,flag 探测 SKIP)。 + +**S2 — A 债务** +stdmod `shellEsc` → `shq`;std/scan 命令的 `env …` 前缀 → argv+env(执行层带 +tc.envOverrides + runtime dirs);build_program/p1689 的 is_clang → caps/dialect。 + +**S3 — C1 环境模型** +msvc.cppm:`find_windows_sdk()`(注册表→Windows Kits 目录扫描,取最高版本)+ +`build_env(inst, sdk)` → INCLUDE/LIB/PATH(+`VSLANG=1033`);`enrich_toolchain_from_cl` +填 `tc.envOverrides`;SDK 缺失=检测时警告、构建时硬错误。doctor msvc 段加 SDK 行。 + +**S4 — 对象/标准旗标方言化(C2 前置)** +`object_filename_for` 接 objExt(.o/.m.o ↔ .obj/.m.obj);ninja std staging +`obj/std.o` 名参数化;`cppStandardFlag` 由 dialect.stdPrefix 产出 +(msvc:>c++20 → `/std:c++latest`,20→`/std:c++20`);defines/`/D` 走 dialect; +CRT:`/MD` 默认、linkage=static → `/MT`。 + +**S5 — C2/C5 msvc 规则发射** +ninja_backend:LinkStyle::SeparateLinker 时 `cxx_link` = `$ld /nologo /OUT:$out @rsp` +(rspfile/rspfile_content),`cxx_shared` = `/DLL /IMPLIB:`,`cxx_archive` = +dial.archiveCmd(lib.exe @rsp);compile 规则 `deps = msvc` + `/showIncludes`; +CompileFlags 增 `ldBinary`(link.exe,cl 同目录);registry archive_tool msvc → +lib.exe;flags.cppm msvc ld 分支(dep runtime dirs → /LIBPATH,user ldflags 透传)。 + +**S6 — C3 std/std.compat staging** +msvc.cppm `std_module_build_commands`(cl /c std.ixx /ifcOutput /Fo:)+ +std.compat.ixx 同法;`std_bmi_path`/`staged_std_bmi_path`/compat 变体 registry 分发 +(flags.cppm 的 clang 硬编码 staging 调用一并解除);stdmod:objectPath 扩展名 +per-dialect、msvc 分支走新命令、执行带 envOverrides;`hasImportStd` 已由 0.0.88 检测。 + +**S7 — C4 .ifc 管线 + /scanDependencies** +ninja `cxx_module` msvc 形态(moduleOutputPrefix=` /ifcOutput ` 已就绪;确认 +`/Fo:` 同行共存);`/ifcSearchDir`(bmiSearchPrefix,已就绪);cxx_scan msvc 变体 +`cl /scanDependencies $out /std:… /c $in`;provider msvc `has_builtin_p1689_scan=true`; +dyndep DyndepOptions(ifc.cache/.ifc)零改动。 + +**S8 — C7 删门 + fast-path env** +删 prepare.cppm MSVC 构建门;BuildResult/fast-path 缓存的单对 runtime env 推广为 +env 列表(持久化 tc.envOverrides,否则 msvc 增量构建丢 INCLUDE/LIB)。 + +**S9 — MinGW 打磨 + 基建** +lifecycle:mingw 在非 Windows 明确报 windows-only;e2e 97 objdump 路径 glob; +doctor mingw 段;release.yml publish-ecosystem timeout 20→30。 + +**S10 — 测试与 CI 面** +e2e `99_msvc_native_build.sh`(requires: msvc):default msvc → new → build → run → +多模块 → import std → 增量;**95 号改造**:门断言(“not yet supported”)翻转为构建 +成功断言;ci-windows.yml MSVC 步骤同步改为 build+run 矩阵(llvm+mingw+msvc); +单测:dialect 提取器、SDK 路径解析纯函数、std: 映射、msvc std 命令拼装。 + +**S11 — 版本与文档** +0.0.90(mcpp.toml/fingerprint.cppm/CHANGELOG);docs/03-toolchains.md MSVC 节 +撤 “not yet supported”、补构建说明;#210 关联说明。 + +**S12 — 流程** +临时分支(仅 ci-windows+release.yml)迭代绿 → 真 PR(全 CI)→ --admin 合入 → +tag v0.0.90 → release(修复版 mirror 的实战验收,publish-ecosystem 应全自动)→ +`xlings install mcpp@0.0.90` 验证 → bootstrap pin → 总汇报。 + +## 风险与回退 +- MSVC 模块管线在真实 CI 的未知坑(cl 版本细节、/showIncludes 本地化、rsp 转义): + 全部集中在 S5-S7,靠临时分支 CI 循环消化;门在 S8 才删,之前任何一步可安全暂停。 +- e2e 98 的 -freflection 依赖 gcc16 驱动接受度:探测 SKIP 兜底。 +- fast-path 缓存格式变更(S8):版本化格式头,老缓存自然失效重建。 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 9468d0b5..34c99f5e 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -254,17 +254,11 @@ jobs: "$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" + # native cl.exe build: full e2e (modules, import std, incremental) + cd "$GITHUB_WORKSPACE" + MCPP="$MCPP_SELF" bash tests/e2e/99_msvc_native_build.sh # 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)" 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/.github/workflows/release.yml b/.github/workflows/release.yml index 59d2679d..dda5c712 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -708,8 +708,10 @@ jobs: needs: [build-release, build-linux-aarch64, build-macos, build-windows] runs-on: ubuntu-latest # A4 hardening: a single stuck upload once held this job >1h (6h default - # ceiling). The mirror script now has per-file timeouts, so 20min is ample. - timeout-minutes: 20 + # ceiling). The mirror script has per-file timeouts and (post-0.0.89) + # batch-upload + ranged-GET verification — normal runs are minutes; 30 + # is the generous backstop (20 was hit by the old per-asset verify loop). + timeout-minutes: 30 env: XLINGS_RES_TOKEN: ${{ secrets.XLINGS_RES_TOKEN }} GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e2969fdf..8f55b802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,42 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.90] — 2026-07-13 + +### 新增 + +- **MSVC 原生构建后端(cl.exe)落地,0.0.88 的构建门移除**。选定 `msvc@system` + 后 `mcpp build/run` 直接用系统 MSVC 编译链接: + - 环境模型:`find_windows_sdk()` + 从检测到的 VC tools/SDK 直接合成 + INCLUDE/LIB/PATH(+`VSLANG=1033`),不跑 vcvarsall;SDK 缺失时检测/选择仍可用, + 构建报带指引的明确错误;doctor 新增 SDK 与 mingw 检查行。 + - 模块管线:std/std.compat.ixx 单命令 staging(`/ifcOutput` → ifc.cache), + 命名模块 `.cppm` 经 `/interface /TP` 编译、`/ifcSearchDir` 消费; + `/scanDependencies` 作为第三个编译器内建 P1689 扫描驱动接入 dyndep。 + - 链接:link.exe/lib.exe(SeparateLinker)+ 响应文件(绕 cmd 8191 限制), + DLL=`/DLL /IMPLIB:`;`deps=msvc`(/showIncludes)头文件依赖;`/MD|/MT` CRT + 随 linkage;`/std:c++20|c++latest` 映射;`.obj` 扩展名全链路。 + - fast-path 增量:构建缓存 env 槽新增 `@env` 多变量编码,增量构建重建 + INCLUDE/LIB 环境。e2e 99(模块/import std/增量)+ 95 改造为真实构建断言。 +- **`[build] dialect_cxxflags` + 方言旗标全图化(issue #210 修复)**。 + `-freflection` 等"改变标准库头声明集"的 flag 现随 `-std=` 的通道到达: + 全局 cxxflags(项目+依赖所有 TU)、std/std.compat BMI 预构建命令、P1689 扫描。 + known-list 自动提升(reflection/contracts/char8_t/`_GLIBCXX_USE_CXX11_ABI`)+ + 显式 `dialect_cxxflags` 逃生舱;指纹早已包含这些 flag,修的是命令构造。 + 实证:#210 的最小复现(gcc16 + `import std;` + `std::meta`)输出 `x 2/y 3`; + e2e 98 含依赖模块变体。 + +### 修复与优化 + +- mingw 在非 Windows 主机的 `toolchain install/default` 现在明确报 + windows-only(此前是 `invalid xpkg target 'xim:mingw-gcc@'`)。 +- std 模块 staging 命令在 Windows 用 `cd /d`(跨盘;工作区 D: + 缓存 C: 的 + 真实 CI 布局)。 +- release 的 publish-ecosystem:镜像脚本改为批量上传+带耐心的 ranged-GET 验证、 + **验证超时不再删除资产**(0.0.89 因逐资产"18s 即删重传"+全量 GET 探测触顶 + 20min 被杀);timeout 兜底 20→30。 +- stdmod 执行层支持工具链声明环境(capture_with_env);shell 引用平台化。 + ## [0.0.89] — 2026-07-13 ### 新增 diff --git a/docs/03-toolchains.md b/docs/03-toolchains.md index 5e6b8aaa..560d7a43 100644 --- a/docs/03-toolchains.md +++ b/docs/03-toolchains.md @@ -132,12 +132,14 @@ windows = "msvc@system" 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). +Since 0.0.90, **native cl.exe builds work**: mcpp synthesizes the +INCLUDE/LIB environment from the detected VC tools + Windows SDK (no +`vcvarsall` involved), stages `std.ixx`/`std.compat.ixx` as `.ifc` BMIs, +compiles `.cppm` module units via `/interface /TP /ifcOutput`, scans with +`/scanDependencies`, and links with `link.exe`/`lib.exe` through response +files. `[build] linkage = "static"` selects the `/MT` CRT. A missing Windows +SDK fails the build with installation guidance (`mcpp self doctor` reports +SDK status). ## Project-Level Version Pinning diff --git a/mcpp.toml b/mcpp.toml index 4d6cd60d..29b53974 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.89" +version = "0.0.90" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/execute.cppm b/src/build/execute.cppm index cafa4743..13fa9d38 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -335,8 +335,22 @@ export std::optional try_fast_build(const std::filesystem::path& projectRoo if (verbose) argv.push_back("-v"); std::vector> childEnv; - if (runtimeEnvKey != "-" && !runtimeEnvValue.empty()) + if (runtimeEnvKey == "@env") { + // Multi-var encoding (MSVC INCLUDE/LIB/PATH/VSLANG + optional runtime + // pair): \x1f-separated k=v records in the single value slot. + std::string_view rest = runtimeEnvValue; + while (!rest.empty()) { + auto sep = rest.find('\x1f'); + auto rec = rest.substr(0, sep); + if (auto eq = rec.find('='); eq != std::string_view::npos && eq > 0) + childEnv.emplace_back(std::string(rec.substr(0, eq)), + std::string(rec.substr(eq + 1))); + if (sep == std::string_view::npos) break; + rest.remove_prefix(sep + 1); + } + } else if (runtimeEnvKey != "-" && !runtimeEnvValue.empty()) { childEnv.emplace_back(runtimeEnvKey, runtimeEnvValue); + } auto t0 = std::chrono::steady_clock::now(); // capture_exec merges stderr into the captured output (replacing `2>&1`), diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 094ec9d1..54171631 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -27,9 +27,10 @@ struct CompileFlags { std::string cxx; // full cxxflags string std::string cc; // full cflags string std::string ld; // ldflags string - std::filesystem::path cxxBinary; // g++ / clang++ - std::filesystem::path ccBinary; // gcc / clang (derived) - std::filesystem::path arBinary; // ar path (may be empty → use PATH) + std::filesystem::path cxxBinary; // g++ / clang++ / cl.exe + std::filesystem::path ccBinary; // gcc / clang (derived; cl.exe = same) + std::filesystem::path arBinary; // ar / llvm-ar / lib.exe (empty → PATH) + std::filesystem::path ldBinary; // link.exe (SeparateLinker dialects only) std::string sysroot; // --sysroot=... (for ninja ldflags) std::string bFlag; // -B (for ninja ldflags) bool staticStdlib = true; @@ -142,7 +143,9 @@ CompileFlags compute_flags(const BuildPlan& plan) { f.cxxBinary = plan.toolchain.binaryPath; f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain); - // PIC? + const bool isMsvcDialect = (d.id == "msvc"); + + // PIC? (GNU-only concept; PE code is position independent by design.) bool need_pic = false; for (auto& lu : plan.linkUnits) { if (lu.kind == LinkUnit::SharedLibrary) { @@ -150,7 +153,7 @@ CompileFlags compute_flags(const BuildPlan& plan) { break; } } - std::string pic_flag = need_pic ? " -fPIC" : ""; + std::string pic_flag = (need_pic && !isMsvcDialect) ? " -fPIC" : ""; // Include dirs std::string include_flags; @@ -235,9 +238,21 @@ CompileFlags compute_flags(const BuildPlan& plan) { // unless the profile pins -O0. auto& prof = plan.manifest.buildConfig; std::string opt_flag = isMuslTc && prof.optLevel != "0" - ? " -Og" : std::format(" {}{}", d.optPrefix, prof.optLevel); + ? " -Og" + : (isMsvcDialect && prof.optLevel == "0") + ? " /Od" // MSVC's no-opt spelling (there is no /O0) + : std::format(" {}{}", d.optPrefix, prof.optLevel); if (prof.debug) opt_flag += std::format(" {}", d.debugFlags); - if (prof.lto) opt_flag += " -flto"; + if (prof.lto && !isMsvcDialect) opt_flag += " -flto"; + + // MSVC baseline: /nologo /EHsc /utf-8 (dialect alwaysFlags) + the CRT + // model — /MD default, /MT under static linkage (portable-by-default is + // impossible on MSVC-ABI; /MT at least removes the vcruntime DLL dep). + std::string msvc_base; + if (isMsvcDialect) { + msvc_base = std::format(" {}", d.alwaysFlags); + msvc_base += (plan.manifest.buildConfig.linkage == "static") ? " /MT" : " /MD"; + } // User link flags std::string user_ldflags; @@ -264,9 +279,8 @@ CompileFlags compute_flags(const BuildPlan& plan) { } std::string std_compat_module_flag; if (!traits.stdCompatBmiUsePrefix.empty() && !plan.stdCompatBmiPath.empty()) { - // NOTE: staging path is Clang's today; registry-dispatch when the - // MSVC backend lands (std.compat.ixx staging). - auto compatDst = mcpp::toolchain::clang::staged_std_compat_bmi_path(plan.outputDir); + auto compatDst = mcpp::toolchain::staged_std_compat_bmi_path( + plan.toolchain, plan.outputDir); std_compat_module_flag = std::string(traits.stdCompatBmiUsePrefix) + escape_path(compatDst); } @@ -287,11 +301,20 @@ CompileFlags compute_flags(const BuildPlan& plan) { std::string cxx_std_flag = plan.cppStandardFlag.empty() ? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag; - f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag, + // plan.dialectFlags rides right behind -std= (issue #210): module-graph- + // global dialect flags reach every TU (deps included) via this global + // cxxflags string, exactly like the standard flag itself. + f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags, + msvc_base, module_flag, std_module_flag, std_compat_module_flag, prebuilt_module_flag, opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags); - f.cc = std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag, - compile_toolchain_flags, b_flag, include_flags); + // MSVC compiles C with cl.exe too; /std: for C uses cN spellings — skip + // the C standard flag there (cl defaults are fine for the C entry TUs). + f.cc = isMsvcDialect + ? std::format("{}{}{}{}{}", msvc_base, opt_flag, compile_toolchain_flags, + b_flag, include_flags) + : std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag, + compile_toolchain_flags, b_flag, include_flags); // Link flags f.staticStdlib = plan.manifest.buildConfig.staticStdlib; @@ -339,6 +362,18 @@ CompileFlags compute_flags(const BuildPlan& plan) { if (prof.strip) link_extra += " -s"; if constexpr (mcpp::platform::is_windows) { + if (isMsvcDialect) { + // Native cl.exe: link.exe does the link (SeparateLinker). Search + // paths for dependency runtime import libs via /LIBPATH; user + // ldflags pass through verbatim; GNU link_extra (-flto/-s) does + // not apply. + f.ldBinary = mcpp::toolchain::link_tool(plan.toolchain); + std::string libpaths; + for (auto& dir : plan.depRuntimeLibraryDirs) + libpaths += " /LIBPATH:" + escape_path(dir); + f.ld = libpaths + user_ldflags; + return f; + } // PE link: no rpath/loader/payload model. MSVC-ABI Clang needs // nothing extra (MSVC STL/SDK via the driver); MinGW adds the static // libstdc++/libgcc pair (static_stdlib above) and -B so its own diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 6b79c5ec..1efbbf90 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -297,6 +297,14 @@ std::string emit_ninja_string(const BuildPlan& plan) { } else { append("ar = ar\n"); } + // Separate linker (link.exe) for the msvc dialect. + const bool separateLinker = + dial.linkStyle == mcpp::toolchain::CommandDialect::LinkStyle::SeparateLinker; + if (separateLinker) { + append(std::format("ld = {}\n", + flags.ldBinary.empty() ? std::string("link.exe") + : escape_ninja_path(flags.ldBinary))); + } if (dyndep) { append(std::format("mcpp = {}\n", escape_ninja_path(mcpp_exe_path()))); if (!plan.scanDepsPath.empty()) { @@ -339,14 +347,25 @@ std::string emit_ninja_string(const BuildPlan& plan) { // msvc); the rule *structure* is shared across compilers. std::string module_output_flag = traits.needsExplicitModuleOutput ? std::string(traits.moduleOutputPrefix) + "$bmi_out" : ""; + // msvc: /showIncludes feeds ninja's deps=msvc header tracking; the + // stable-English prefix is guaranteed by VSLANG=1033 in envOverrides. + const bool msvcDeps = dial.ninjaDepsMode == std::string_view("msvc"); const std::string compile_tail = std::format( - "{} $in {}$out", dial.compileOnly, dial.outputObjPrefix); + "{}{} $in {}$out", + msvcDeps ? "/showIncludes " : "", dial.compileOnly, dial.outputObjPrefix); + auto append_deps = [&] { + if (msvcDeps) append(" deps = msvc\n"); + }; + // cl.exe needs /TP (our module interfaces are .cppm, unknown to cl) and + // /interface to treat the TU as a module interface unit. + const std::string module_src_flags = msvcDeps ? " /interface /TP" : ""; append("rule cxx_module\n"); if constexpr (mcpp::platform::is_windows) { // Windows: skip BMI restat optimization (requires POSIX shell). append(std::format(" command = " - "$cxx $local_includes $cxxflags $unit_cxxflags{} {}\n", - module_output_flag, compile_tail)); + "$cxx $local_includes $cxxflags $unit_cxxflags{}{} {}\n", + module_output_flag, module_src_flags, compile_tail)); + append_deps(); } else { append(std::format(" command = " "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then " @@ -370,6 +389,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { " command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n", compile_tail)); append(" description = OBJ $out\n"); + append_deps(); if (dyndep) append(" restat = 1\n"); append("\n"); @@ -380,25 +400,47 @@ std::string emit_ninja_string(const BuildPlan& plan) { " command = $cc $local_includes $cflags $unit_cflags {}\n", compile_tail)); append(" description = CC $out\n"); + append_deps(); if (dyndep) append(" restat = 1\n"); append("\n"); } - // Link rule: driver-style today (g++/clang++ act as the linker). The - // dialect's LinkStyle::SeparateLinker (link.exe /OUT: + rspfile) is the - // MSVC backend's insertion point — unreachable until that lands. - append("rule cxx_link\n"); - append(" command = $cxx $in -o $out $ldflags $unit_ldflags\n"); - append(" description = LINK $out\n\n"); + // Link/archive/shared: driver-style (g++/clang++ are the linker) vs the + // msvc dialect's separate link.exe/lib.exe. The msvc commands go through + // response files — object lists exceed cmd.exe's 8191-char limit fast. + if (separateLinker) { + append("rule cxx_link\n"); + append(" command = $ld /nologo /OUT:$out @$out.rsp $ldflags $unit_ldflags\n"); + append(" rspfile = $out.rsp\n"); + append(" rspfile_content = $in\n"); + append(" description = LINK $out\n\n"); + + append("rule cxx_archive\n"); + append(" command = $ar /nologo /OUT:$out @$out.rsp\n"); + append(" rspfile = $out.rsp\n"); + append(" rspfile_content = $in\n"); + append(" description = AR $out\n\n"); + + append("rule cxx_shared\n"); + append(" command = $ld /nologo /DLL /OUT:$out /IMPLIB:$out.lib " + "@$out.rsp $ldflags $unit_ldflags\n"); + append(" rspfile = $out.rsp\n"); + append(" rspfile_content = $in\n"); + append(" description = SHARED $out\n\n"); + } else { + append("rule cxx_link\n"); + append(" command = $cxx $in -o $out $ldflags $unit_ldflags\n"); + append(" description = LINK $out\n\n"); - append("rule cxx_archive\n"); - append(std::format(" command = {}\n", dial.archiveCmd)); - append(" description = AR $out\n\n"); + append("rule cxx_archive\n"); + append(std::format(" command = {}\n", dial.archiveCmd)); + append(" description = AR $out\n\n"); - append("rule cxx_shared\n"); - append(" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n"); - append(" description = SHARED $out\n\n"); + append("rule cxx_shared\n"); + append(" command = $cxx -shared $in -o $out $ldflags $soname_flag $unit_ldflags\n"); + append(" description = SHARED $out\n\n"); + } append("rule runtime_alias\n"); if constexpr (mcpp::platform::is_windows) { @@ -413,7 +455,12 @@ std::string emit_ninja_string(const BuildPlan& plan) { // GCC: built-in -fdeps-format=p1689r5 flags during preprocessing. // Clang: external clang-scan-deps tool with -format=p1689. append("rule cxx_scan\n"); - if (plan.scanDepsPath.empty()) { + if (msvcDeps) { + // MSVC: compiler-integrated P1689 via /scanDependencies (scan + // only — no codegen); /TP because our module units are .cppm. + append(" command = $cxx $local_includes $cxxflags $unit_cxxflags " + "/scanDependencies $out /TP /c $in /Fo:$compile_target\n"); + } else if (plan.scanDepsPath.empty()) { // GCC path: compiler-integrated P1689 scanning. append(" command = $cxx $local_includes $cxxflags -fmodules " "$unit_cxxflags " @@ -445,7 +492,8 @@ std::string emit_ninja_string(const BuildPlan& plan) { // Stage prebuilt std artifacts into the compiler-specific BMI cache. auto std_bmi_dst = mcpp::toolchain::staged_std_bmi_path(plan.toolchain, {}); - auto std_o_dst = std::filesystem::path("obj") / "std.o"; + auto std_o_dst = std::filesystem::path("obj") + / std::format("std{}", dial.objExt); bool has_std_artifacts = !plan.stdBmiPath.empty() && !plan.stdObjectPath.empty(); if (has_std_artifacts) { @@ -456,8 +504,10 @@ std::string emit_ninja_string(const BuildPlan& plan) { } bool has_std_compat = !plan.stdCompatBmiPath.empty() && !plan.stdCompatObjectPath.empty(); - auto compat_bmi_dst = std::filesystem::path("pcm.cache") / "std.compat.pcm"; - auto compat_o_dst = std::filesystem::path("obj") / "std.compat.o"; + auto compat_bmi_dst = std::filesystem::path(traits.bmiDir) + / std::format("std.compat{}", traits.bmiExt); + auto compat_o_dst = std::filesystem::path("obj") + / std::format("std.compat{}", dial.objExt); if (has_std_compat) { // std.compat.pcm depends on std.pcm — ensure std.pcm is staged first // so clang can resolve the transitive dependency when loading std.compat.pcm. @@ -803,7 +853,22 @@ std::expected NinjaBackend::build(const BuildPlan& plan // Record ninja binary for P0 fast-path cache. BuildResult r; r.ninjaProgram = ninjaProgram; - if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) { + if (!plan.toolchain.envOverrides.empty()) { + // Toolchain-declared env (MSVC INCLUDE/LIB/PATH/VSLANG). Encode all + // pairs (plus any runtime-dirs pair) into the fast-path cache's + // single env slot: "@env" key + \x1f-separated k=v records — the + // fast path must re-create this exact environment for ninja. + r.runtimeEnvKey = "@env"; + std::string joined; + auto add = [&](const std::string& k, const std::string& v) { + if (!joined.empty()) joined += '\x1f'; + joined += k; joined += '='; joined += v; + }; + if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) + add(runtimeEnv->first, runtimeEnv->second); + for (auto& ev : plan.toolchain.envOverrides) add(ev.key, ev.value); + r.runtimeEnvValue = std::move(joined); + } else if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) { r.runtimeEnvKey = runtimeEnv->first; r.runtimeEnvValue = runtimeEnv->second; } else { @@ -824,12 +889,11 @@ std::expected NinjaBackend::build(const BuildPlan& plan if (opts.parallelJobs) nargv.push_back(std::format("-j{}", opts.parallelJobs)); + // Real env pairs for THIS run (the "@env" cache encoding above is only + // for the fast path's later re-creation of the same environment). std::vector> nenv; - if (r.runtimeEnvKey != "-" && !r.runtimeEnvValue.empty()) - nenv.emplace_back(r.runtimeEnvKey, r.runtimeEnvValue); - // Toolchain-declared env (empty for GCC/Clang; MSVC's INCLUDE/LIB/PATH). - // NOTE: not persisted in the fast-path cache yet — revisit when the MSVC - // backend lands (its fast path must re-derive these from detection). + if (auto runtimeEnv = runtime_env_for_dirs(plan.toolchain.compilerRuntimeDirs)) + nenv.emplace_back(runtimeEnv->first, runtimeEnv->second); for (auto& ev : plan.toolchain.envOverrides) nenv.emplace_back(ev.key, ev.value); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 5a1506ad..d7fcb761 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -10,6 +10,7 @@ import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.modgraph.scanner; import mcpp.toolchain.detect; +import mcpp.toolchain.dialect; import mcpp.toolchain.fingerprint; import mcpp.platform; @@ -47,6 +48,10 @@ struct BuildPlan { mcpp::toolchain::Fingerprint fingerprint; std::string cppStandard = "c++23"; std::string cppStandardFlag = "-std=c++23"; + // Module-graph-global dialect flags (issue #210), pre-joined with a + // leading space per flag (e.g. " -freflection"). Rides -std='s channels: + // global $cxxflags (all TUs incl. deps), std BMI prebuild, scans. + std::string dialectFlags; std::filesystem::path projectRoot; // where mcpp.toml lives std::filesystem::path outputDir; // target/// @@ -125,10 +130,13 @@ std::string sanitize_for_path(std::string_view module_name) { return s; } -std::string object_filename_for(const std::filesystem::path& src) { +std::string object_filename_for(const std::filesystem::path& src, + std::string_view objExt = ".o") { auto stem = src.stem().string(); // distinguish .cppm vs .cpp by extension prefix to avoid collisions - return stem + (src.extension() == ".cppm" ? ".m.o" : ".o"); + return stem + (src.extension() == ".cppm" + ? ".m" + std::string(objExt) + : std::string(objExt)); } std::string qualified_package_name(const mcpp::manifest::Manifest& manifest) { @@ -313,8 +321,16 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, plan.fingerprint = fp; if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) { plan.cppStandard = stdCfg->canonical; - plan.cppStandardFlag = stdCfg->flag; + // Spelled per-dialect: "-std=c++26" (gnu) vs "/std:c++latest" (msvc). + plan.cppStandardFlag = mcpp::toolchain::std_flag_for( + mcpp::toolchain::dialect_for(tc), stdCfg->canonical, stdCfg->level); } + for (auto& f : mcpp::manifest::dialect_flags(manifest.buildConfig)) { + plan.dialectFlags += ' '; + plan.dialectFlags += f; + } + // Object extension is dialect-spelled (.o vs .obj). + const std::string_view objExt = mcpp::toolchain::dialect_for(tc).objExt; plan.projectRoot = projectRoot; plan.outputDir = outputDir; plan.stdBmiPath = stdBmiPath; @@ -389,7 +405,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // derived from `/` so collisions are impossible. std::map basenameCount; for (auto idx : topoOrder) { - basenameCount[object_filename_for(graph.units[idx].path)]++; + basenameCount[object_filename_for(graph.units[idx].path, objExt)]++; } auto sanitize = [](const std::string& s) { std::string out; out.reserve(s.size()); @@ -406,7 +422,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, cu.localIncludeDirs = u.localIncludeDirs; cu.packageCflags = u.packageCflags; cu.packageCxxflags = u.packageCxxflags; - const auto fname = object_filename_for(u.path); + const auto fname = object_filename_for(u.path, objExt); if (basenameCount[fname] > 1) { // Use / as prefix to handle // both cross-package (multi-version mangling) and intra-package @@ -650,7 +666,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // Add main.cpp -> obj/main.o CompileUnit main_cu; main_cu.source = *lu.entryMain; - main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain); + main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain, objExt); main_cu.packageName = qualified_package_name(manifest); if (!packages.empty() && packages[0].usageResolved) { main_cu.localIncludeDirs = packages[0].privateBuild.includeDirs; diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 3157fe33..b4d2c4e4 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -17,6 +17,7 @@ import mcpp.modgraph.scanner; import mcpp.modgraph.validate; import mcpp.toolchain.clang; import mcpp.toolchain.detect; +import mcpp.toolchain.dialect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; @@ -202,6 +203,12 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { s += " cxxflag:"; s += flag; } + // Explicit [build] dialect_cxxflags (auto-promoted ones are already in + // cxxflags above) — they change every BMI in the graph. + for (auto const& flag : m.buildConfig.dialectCxxflags) { + s += " dialect:"; + s += flag; + } for (auto const& flag : m.buildConfig.ldflags) { s += " ldflag:"; s += flag; @@ -863,16 +870,17 @@ 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) { + // Native MSVC builds need the synthesized INCLUDE/LIB env — absent when + // detection found VC tools but no Windows SDK. Fail here with guidance + // instead of cl.exe's later "cannot open include file: 'corecrt.h'". + if (tc->compiler == mcpp::toolchain::CompilerId::MSVC + && tc->envOverrides.empty()) { 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", + "msvc {} was detected at {}, but no Windows SDK was found —\n" + " cl.exe cannot compile without the UCRT/SDK headers.\n" + " Install the 'Windows 11 SDK' component via the Visual Studio\n" + " Installer (it is part of the Desktop development with C++\n" + " workload), then retry.", tc->version, tc->binaryPath.string())); } @@ -2575,6 +2583,18 @@ prepare_build(bool print_fingerprint, return false; }); + // The dialect-complete standard flag: spelled per-dialect and carrying + // the module-graph-global dialect flags (issue #210). ONE string shared + // by the p1689 scan and the std BMI prebuild so scan-time, prebuild-time + // and compile-time dialect provably agree. + std::string stdFlagAndDialect = mcpp::toolchain::std_flag_for( + mcpp::toolchain::dialect_for(*tc), + m->cppStandard.canonical, m->cppStandard.level); + for (auto& f : mcpp::manifest::dialect_flags(m->buildConfig)) { + stdFlagAndDialect += ' '; + stdFlagAndDialect += f; + } + // Modgraph: regex scanner by default; opt-in to compiler-driven P1689 // scanner via env var MCPP_SCANNER=p1689 (see docs/27). auto scan = [&] { @@ -2583,7 +2603,8 @@ prepare_build(bool print_fingerprint, auto tmp = std::filesystem::temp_directory_path() / std::format("mcpp_p1689_{}", std::random_device{}()); std::filesystem::create_directories(tmp); - return mcpp::modgraph::scan_packages_p1689(packages, *tc, tmp, m->cppStandard.flag); + return mcpp::modgraph::scan_packages_p1689(packages, *tc, tmp, + stdFlagAndDialect); } return mcpp::modgraph::scan_packages(packages); }(); @@ -2633,8 +2654,13 @@ prepare_build(bool print_fingerprint, std::filesystem::path stdCompatBmiPath; std::filesystem::path stdCompatObjectPath; if (needsStdModule) { + // The std BMI must be compiled with the SAME dialect set its + // importers use (issue #210: -freflection gates libstdc++'s — + // a std BMI built without it structurally lacks std::meta). Both + // pieces were already in the fingerprint; this fixes the COMMAND + // construction the fingerprint promised (stdFlagAndDialect above). auto sm = mcpp::toolchain::ensure_built( - *tc, fp.hex, m->package.standard, m->cppStandard.flag, + *tc, fp.hex, m->package.standard, stdFlagAndDialect, mcpp::platform::macos::deployment_target( m->buildConfig.macosDeploymentTarget)); if (!sm) return std::unexpected(sm.error().message); diff --git a/src/doctor.cppm b/src/doctor.cppm index 60e168aa..a077b472 100644 --- a/src/doctor.cppm +++ b/src/doctor.cppm @@ -120,6 +120,34 @@ export int doctor_report() { warn("msvc not detected — run `mcpp toolchain default msvc` for " "setup guidance (mcpp does not install MSVC)"); } + // Windows SDK (native cl.exe builds need its UCRT/um headers). + if (auto sdk = mcpp::toolchain::msvc::find_windows_sdk()) { + ok(std::format("Windows SDK {} at {}", sdk->version, + sdk->root.string())); + } else { + warn("no Windows SDK found — native msvc builds will fail " + "(install the 'Windows 11 SDK' VS component)"); + } + + mcpp::ui::status("Checking", "mingw (xim:mingw-gcc)"); + { + auto pkgs = std::filesystem::path( + std::getenv("MCPP_HOME") ? std::getenv("MCPP_HOME") + : (std::string(std::getenv("USERPROFILE") + ? std::getenv("USERPROFILE") : "") + "\\.mcpp")) + / "registry" / "data" / "xpkgs" / "xim-x-mingw-gcc"; + std::error_code ec; + bool any = false; + if (std::filesystem::exists(pkgs, ec)) { + for (auto& v : std::filesystem::directory_iterator(pkgs, ec)) { + if (!v.is_directory(ec)) continue; + ok(std::format("mingw {} installed", v.path().filename().string())); + any = true; + } + } + if (!any) + ok("mingw not installed (optional — `mcpp toolchain install mingw 16.1.0`)"); + } } mcpp::ui::status("Checking", "std module"); diff --git a/src/manifest/toml.cppm b/src/manifest/toml.cppm index 747bdc79..be18e6ee 100644 --- a/src/manifest/toml.cppm +++ b/src/manifest/toml.cppm @@ -642,6 +642,11 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_bool("build.allow_host_libs")) m.buildConfig.allowHostLibs = *v; if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v; if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v; + // Module-graph-global dialect flags (issue #210) — see types.cppm + // dialect_flags(); this key is the explicit escape hatch for flags the + // known-list doesn't recognize yet. + if (auto v = doc->get_string_array("build.dialect_cxxflags")) + m.buildConfig.dialectCxxflags = *v; if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v; if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v; if (auto v = doc->get_string("build.default-profile")) m.buildConfig.defaultProfile = *v; diff --git a/src/manifest/types.cppm b/src/manifest/types.cppm index 7fdc9288..3b5767f5 100644 --- a/src/manifest/types.cppm +++ b/src/manifest/types.cppm @@ -149,6 +149,15 @@ struct BuildConfig { std::vector cflags; std::vector cxxflags; std::vector ldflags; + // Dialect-class C++ flags: flags that change what the standard library's + // headers DECLARE or participate in module dialect checks (issue #210's + // -freflection: libstdc++'s is gated on __cpp_impl_reflection). + // These are module-graph-global — they ride -std='s channels (global + // cxxflags for every TU incl. deps, the std/std.compat BMI prebuild, + // scan commands). Populated from [build] dialect_cxxflags plus + // auto-promotion of known flags found in [build] cxxflags + // (see dialect_flags()). + std::vector dialectCxxflags; std::string cStandard; // Escape hatch for the hermetic link check: a sandbox toolchain whose // CRT/loader resolve OUTSIDE the sandbox is a hard error by default @@ -393,6 +402,16 @@ struct ManifestError { std::expected normalize_cpp_standard(std::string_view raw); +// The module-graph-global dialect flag set: explicit [build] dialect_cxxflags +// plus KNOWN dialect-class flags auto-promoted out of [build] cxxflags +// (they also stay per-unit there — duplication is harmless and keeps the +// mechanism explainable). Deduplicated, declaration order preserved. +std::vector dialect_flags(const BuildConfig& bc); + +// True when `flag` belongs to the known dialect-class list (changes what +// libstdc++/libc++ headers declare, or participates in BMI dialect checks). +bool is_dialect_flag(std::string_view flag); + std::filesystem::path resolve_lib_root_path(const Manifest& manifest); // True if the manifest declares at least one `kind = "lib"` target. @@ -431,6 +450,34 @@ std::optional validate_target_soname(const Target& t, } +bool is_dialect_flag(std::string_view flag) { + // Deliberately conservative first list (design doc §1.3a): + // -fno-exceptions / -fno-rtti stay per-unit until separately reviewed + // (dependencies may assume exceptions are available). + static constexpr std::string_view exact[] = { + "-freflection", "-fno-reflection", // P2996 (GCC 16+) + "-fcontracts", "-fno-contracts", // P2900 + "-fchar8_t", "-fno-char8_t", + }; + for (auto e : exact) + if (flag == e) return true; + // libstdc++ dual-ABI switch changes declared symbols/types wholesale. + if (flag.starts_with("-D_GLIBCXX_USE_CXX11_ABI=")) return true; + return false; +} + +std::vector dialect_flags(const BuildConfig& bc) { + std::vector out; + auto add = [&](const std::string& f) { + if (std::find(out.begin(), out.end(), f) == out.end()) + out.push_back(f); + }; + for (auto& f : bc.dialectCxxflags) add(f); + for (auto& f : bc.cxxflags) + if (is_dialect_flag(f)) add(f); + return out; +} + std::expected normalize_cpp_standard(std::string_view raw) { auto trim_copy = [](std::string_view input) { std::size_t begin = 0; diff --git a/src/toolchain/dialect.cppm b/src/toolchain/dialect.cppm index 591d560e..29288108 100644 --- a/src/toolchain/dialect.cppm +++ b/src/toolchain/dialect.cppm @@ -56,6 +56,13 @@ struct CommandDialect { // Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc. const CommandDialect& dialect_for(const Toolchain& tc); +// The full -std=/-/std: flag for a normalized standard (canonical like +// "c++26"/"gnu++23", numeric level). MSVC: /std:c++20 exists; everything +// newer maps to /std:c++latest (required for import std); gnu dialects have +// no MSVC equivalent and take the same mapping. +std::string std_flag_for(const CommandDialect& d, + std::string_view canonical, int level); + } // namespace mcpp::toolchain namespace mcpp::toolchain { @@ -106,4 +113,13 @@ const CommandDialect& dialect_for(const Toolchain& tc) { return kGnuDialect; } +std::string std_flag_for(const CommandDialect& d, + std::string_view canonical, int level) { + if (d.id == "msvc") { + if (level <= 20) return "/std:c++20"; + return "/std:c++latest"; + } + return std::format("{}{}", d.stdPrefix, canonical); +} + } // namespace mcpp::toolchain diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 40980922..45047bb8 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.89"; +inline constexpr std::string_view MCPP_VERSION = "0.0.90"; struct FingerprintInputs { Toolchain toolchain; diff --git a/src/toolchain/gcc.cppm b/src/toolchain/gcc.cppm index 5e5fdfc0..fb5a070d 100644 --- a/src/toolchain/gcc.cppm +++ b/src/toolchain/gcc.cppm @@ -3,6 +3,7 @@ export module mcpp.toolchain.gcc; import std; +import mcpp.platform; import mcpp.toolchain.model; import mcpp.toolchain.probe; import mcpp.xlings; @@ -141,8 +142,12 @@ std::string std_module_build_command(const Toolchain& tc, } } + // Windows (MinGW): cmd.exe needs `/d` to change DRIVE (project on D:, + // BMI cache on C: is the real CI layout — same-drive runs masked this). + const char* cd = mcpp::platform::is_windows ? "cd /d" : "cd"; return std::format( - "cd {} && {}{} {} -fmodules -O2{}{} -c {} -o std.o 2>&1", + "{} {} && {}{} {} -fmodules -O2{}{} -c {} -o std.o 2>&1", + cd, mcpp::xlings::shq(cacheDir.string()), mcpp::toolchain::compiler_env_prefix(tc), mcpp::xlings::shq(tc.binaryPath.string()), diff --git a/src/toolchain/lifecycle.cppm b/src/toolchain/lifecycle.cppm index 13434a43..da926ad0 100644 --- a/src/toolchain/lifecycle.cppm +++ b/src/toolchain/lifecycle.cppm @@ -183,6 +183,18 @@ int msvc_wrong_host() { return 1; } +// MinGW is a Windows-native toolchain package; on other hosts fail with a +// clear message instead of the confusing empty-version xim error +// (`invalid xpkg target 'xim:mingw-gcc@'`). +bool mingw_wrong_host(const mcpp::toolchain::XimToolchainPackage& pkg) { + if (pkg.ximName == "mingw-gcc" && !mcpp::platform::is_windows) { + mcpp::ui::error("mingw is a Windows-only toolchain (MinGW-w64 GCC); " + "on this host use gcc/llvm instead"); + return true; + } + return false; +} + void msvc_print_detected(const mcpp::toolchain::msvc::MsvcInstallation& inst) { mcpp::ui::status("Detected", std::format( "msvc {}{} (VC tools {})", @@ -370,6 +382,7 @@ export int toolchain_install(const mcpp::config::GlobalConfig& cfg, } auto pkg = mcpp::toolchain::to_xim_package(*spec); + if (mingw_wrong_host(pkg)) return 1; // Partial-version resolution: `gcc 15` → highest available 15.x.y in // the synced index. Empty version → latest of any major. @@ -490,6 +503,7 @@ export int toolchain_set_default(const mcpp::config::GlobalConfig& cfg, } auto pkg = mcpp::toolchain::to_xim_package(*spec); + if (mingw_wrong_host(pkg)) return 1; // Partial-version resolution against installed payloads. if (auto picked = resolve_version_match( diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 30526e23..28c98cbc 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -20,6 +20,7 @@ import std; import mcpp.platform; import mcpp.toolchain.model; import mcpp.toolchain.probe; +import mcpp.xlings; export namespace mcpp::toolchain::msvc { @@ -72,9 +73,48 @@ std::string triple_for_arch(std::string_view arch); std::string install_guidance(); // Classify + enrich an already-probed cl.exe binary for detect(): -// version/arch from the banner, targetTriple, driverIdent, std.ixx lookup. +// version/arch from the banner, targetTriple, driverIdent, std.ixx lookup, +// and the build env (INCLUDE/LIB/PATH from VC tools + Windows SDK) into +// tc.envOverrides. Missing SDK leaves envOverrides empty — detection still +// succeeds (selection UX must work on SDK-less boxes); the build path +// checks and errors with guidance. std::expected enrich_toolchain_from_cl(Toolchain& tc); +// ─── Windows SDK + build environment (native cl.exe builds) ────────────── + +struct WindowsSdk { + std::filesystem::path root; // C:\Program Files (x86)\Windows Kits\10 + std::string version; // "10.0.26100.0" (highest usable) +}; + +// Locate the Windows 10/11 SDK (highest version with ucrt headers). +std::optional find_windows_sdk(); + +// Synthesize the environment cl.exe/link.exe need — what vcvars would set, +// derived directly from the located VC tools + SDK (no vcvarsall.bat run): +// INCLUDE = \include; \Include\\{ucrt,um,shared,winrt} +// LIB = \lib\; \Lib\\{ucrt,um}\ +// PATH = ; (mspdb*.dll etc.) +// VSLANG = 1033 (stable English /showIncludes prefix for ninja deps=msvc) +std::vector build_env_for_cl(const std::filesystem::path& clPath, + std::string_view arch, + const WindowsSdk& sdk); + +// std / std.compat module staging commands (single cl step each): +// cl /nologo /EHsc /W0 /O2 /c \modules\std.ixx +// /ifcOutput \ifc.cache\std.ifc /Fo:\std.obj +std::vector std_module_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag); +std::vector std_compat_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag); + +std::filesystem::path std_bmi_path(const std::filesystem::path& cacheDir); +std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir); +std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir); +std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir); + } // namespace mcpp::toolchain::msvc namespace mcpp::toolchain::msvc { @@ -358,6 +398,131 @@ std::optional detect_installation() { #endif } +std::optional find_windows_sdk() { +#if defined(_WIN32) + // Directory scan of the conventional install roots; highest version dir + // that actually carries the UCRT headers wins. (Registry Installed + // Roots would be marginally more correct — the path scan covers every + // real installer layout seen so far and needs no Win32 API surface.) + for (const char* base : {"C:\\Program Files (x86)\\Windows Kits\\10", + "C:\\Program Files\\Windows Kits\\10"}) { + std::filesystem::path root{base}; + std::error_code ec; + if (!std::filesystem::exists(root / "Include", ec)) continue; + std::string best; + for (auto& e : std::filesystem::directory_iterator(root / "Include", ec)) { + if (!e.is_directory()) continue; + auto v = e.path().filename().string(); + if (std::filesystem::exists(e.path() / "ucrt" / "corecrt.h", ec) + && v > best) + best = v; + } + if (!best.empty()) return WindowsSdk{root, best}; + } +#endif + return std::nullopt; +} + +std::vector build_env_for_cl(const std::filesystem::path& clPath, + std::string_view arch, + const WindowsSdk& sdk) { + // \bin\Host\\cl.exe → + auto clDir = clPath.parent_path(); + auto tools = clDir.parent_path().parent_path().parent_path(); + std::string a = arch.empty() ? std::string("x64") : std::string(arch); + + auto join = [](std::initializer_list ps) { + std::string s; + for (auto& p : ps) { + if (!s.empty()) s += ';'; + s += p.string(); + } + return s; + }; + + std::vector env; + env.push_back({"INCLUDE", join({ + tools / "include", + sdk.root / "Include" / sdk.version / "ucrt", + sdk.root / "Include" / sdk.version / "um", + sdk.root / "Include" / sdk.version / "shared", + sdk.root / "Include" / sdk.version / "winrt", + })}); + env.push_back({"LIB", join({ + tools / "lib" / a, + sdk.root / "Lib" / sdk.version / "ucrt" / a, + sdk.root / "Lib" / sdk.version / "um" / a, + })}); + std::string path = clDir.string(); + if (const char* p = std::getenv("PATH"); p && *p) { + path += ';'; + path += p; + } + env.push_back({"PATH", std::move(path)}); + // Stable English "Note: including file:" prefix for ninja's deps=msvc. + env.push_back({"VSLANG", "1033"}); + return env; +} + +std::filesystem::path std_bmi_path(const std::filesystem::path& cacheDir) { + return cacheDir / "ifc.cache" / "std.ifc"; +} +std::filesystem::path staged_std_bmi_path(const std::filesystem::path& outputDir) { + return outputDir / "ifc.cache" / "std.ifc"; +} +std::filesystem::path std_compat_bmi_path(const std::filesystem::path& cacheDir) { + return cacheDir / "ifc.cache" / "std.compat.ifc"; +} +std::filesystem::path staged_std_compat_bmi_path(const std::filesystem::path& outputDir) { + return outputDir / "ifc.cache" / "std.compat.ifc"; +} + +namespace { + +std::string cl_stage_command(const Toolchain& tc, + const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag, + const std::filesystem::path& source, + const std::filesystem::path& ifcOut, + std::string_view objName, + std::string_view extraRef) { + // cd into the cache dir (relative outputs land there); env (INCLUDE/LIB) + // comes from tc.envOverrides via the executor, not the command string. + // `/d`: cmd.exe won't change DRIVE without it (workspace on D:, BMI + // cache on C: is the real CI layout). + return std::format( + "cd /d {} && {} /nologo {} /EHsc /O2 /W0{} /c {} /ifcOutput {} /Fo:{} 2>&1", + mcpp::xlings::shq(cacheDir.string()), + mcpp::xlings::shq(tc.binaryPath.string()), + cppStandardFlag, + extraRef, + mcpp::xlings::shq(source.string()), + mcpp::xlings::shq(ifcOut.string()), + objName); +} + +} // namespace + +std::vector std_module_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag) { + return { cl_stage_command(tc, cacheDir, cppStandardFlag, + tc.stdModuleSource, + std_bmi_path(cacheDir), "std.obj", "") }; +} + +std::vector std_compat_build_commands( + const Toolchain& tc, const std::filesystem::path& cacheDir, + std::string_view cppStandardFlag) { + // std.compat imports std — reference the freshly staged std.ifc. + auto ref = std::format(" /reference std={}", + mcpp::xlings::shq(std_bmi_path(cacheDir).string())); + return { cl_stage_command(tc, cacheDir, cppStandardFlag, + tc.stdCompatSource, + std_compat_bmi_path(cacheDir), "std.compat.obj", + ref) }; +} + std::expected enrich_toolchain_from_cl(Toolchain& tc) { auto banner = capture_cl_banner(tc.binaryPath); auto parsed = parse_cl_banner(banner); @@ -386,6 +551,17 @@ std::expected enrich_toolchain_from_cl(Toolchain& tc) { tc.stdModuleSource = *found; tc.hasImportStd = true; } + if (auto compat = toolsDir / "modules" / "std.compat.ixx"; + std::filesystem::exists(compat, ec)) { + tc.stdCompatSource = compat; + } + + // Build environment (INCLUDE/LIB/PATH/VSLANG). SDK absence keeps + // detection working (selection UX on SDK-less boxes); the build path + // errors with guidance when envOverrides is empty. + if (auto sdk = find_windows_sdk()) { + tc.envOverrides = build_env_for_cl(tc.binaryPath, parsed->second, *sdk); + } return {}; } diff --git a/src/toolchain/provider.cppm b/src/toolchain/provider.cppm index d3246e14..1bc32adf 100644 --- a/src/toolchain/provider.cppm +++ b/src/toolchain/provider.cppm @@ -95,9 +95,9 @@ ProviderCapabilities capabilities_for(const Toolchain& tc) { } case CompilerId::MSVC: { - // Pure MSVC (cl.exe) — not yet fully supported, but stubs are here - // so callers can branch on it without another unknown-compiler guard. caps.has_scan_deps = false; + // cl.exe emits P1689 itself via /scanDependencies. + caps.has_builtin_p1689_scan = true; caps.stdlib_id = "msvc-stl"; caps.archive_format = "lib.exe"; break; diff --git a/src/toolchain/registry.cppm b/src/toolchain/registry.cppm index 813e3c55..ec793ded 100644 --- a/src/toolchain/registry.cppm +++ b/src/toolchain/registry.cppm @@ -8,6 +8,7 @@ import mcpp.toolchain.clang; import mcpp.toolchain.gcc; import mcpp.toolchain.llvm; import mcpp.toolchain.model; +import mcpp.toolchain.msvc; export namespace mcpp::toolchain { @@ -68,8 +69,11 @@ std::vector> available_toolchain_indexes(); std::filesystem::path derive_c_compiler(const Toolchain& tc); std::filesystem::path archive_tool(const Toolchain& tc); +std::filesystem::path link_tool(const Toolchain& tc); std::filesystem::path staged_std_bmi_path(const Toolchain& tc, const std::filesystem::path& outputDir); +std::filesystem::path staged_std_compat_bmi_path(const Toolchain& tc, + const std::filesystem::path& outputDir); } // namespace mcpp::toolchain @@ -273,6 +277,12 @@ std::filesystem::path derive_c_compiler(const Toolchain& tc) { } std::filesystem::path archive_tool(const Toolchain& tc) { + if (tc.compiler == CompilerId::MSVC) { + auto lib = tc.binaryPath.parent_path() / "lib.exe"; + std::error_code ec; + if (std::filesystem::exists(lib, ec)) return lib; + return {}; + } if (is_clang(tc)) return mcpp::toolchain::clang::archive_tool(tc); // MinGW bundles its own binutils next to the frontend (self-contained, @@ -302,8 +312,27 @@ std::filesystem::path archive_tool(const Toolchain& tc) { std::filesystem::path staged_std_bmi_path(const Toolchain& tc, const std::filesystem::path& outputDir) { + if (tc.compiler == CompilerId::MSVC) + return mcpp::toolchain::msvc::staged_std_bmi_path(outputDir); if (is_clang(tc)) return mcpp::toolchain::clang::staged_std_bmi_path(outputDir); return mcpp::toolchain::gcc::staged_std_bmi_path(outputDir); } +std::filesystem::path staged_std_compat_bmi_path(const Toolchain& tc, + const std::filesystem::path& outputDir) { + if (tc.compiler == CompilerId::MSVC) + return mcpp::toolchain::msvc::staged_std_compat_bmi_path(outputDir); + return mcpp::toolchain::clang::staged_std_compat_bmi_path(outputDir); +} + +// Separate linker binary for SeparateLinker dialects (link.exe beside cl). +// Empty for driver-link toolchains. +std::filesystem::path link_tool(const Toolchain& tc) { + if (tc.compiler != CompilerId::MSVC) return {}; + auto link = tc.binaryPath.parent_path() / "link.exe"; + std::error_code ec; + if (std::filesystem::exists(link, ec)) return link; + return {}; +} + } // namespace mcpp::toolchain diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 8c309eb9..d2251c99 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -30,6 +30,7 @@ import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.gcc; import mcpp.toolchain.linkmodel; +import mcpp.toolchain.msvc; export namespace mcpp::toolchain { @@ -64,8 +65,12 @@ namespace mcpp::toolchain { namespace { -std::expected run_capture_command(const std::string& cmd) { - auto r = mcpp::platform::process::capture(cmd); +std::expected run_capture_command( + const std::string& cmd, + const std::vector>& env) { + auto r = env.empty() + ? mcpp::platform::process::capture(cmd) + : mcpp::platform::process::capture_with_env(cmd, env); if (r.exit_code != 0) { // Include the command: its --sysroot/-isystem flags are the first // thing needed to diagnose header-resolution failures. @@ -154,10 +159,16 @@ std::expected write_metadata(const std::filesystem::path& pat return {}; } -std::expected run_commands(const std::vector& commands) { +// Toolchain-declared env (MSVC's INCLUDE/LIB/PATH) applies to every std +// module build command; empty for GCC/Clang (their LD_LIBRARY_PATH need is +// carried as an in-command `env` prefix on POSIX for now). +std::expected run_commands( + const std::vector& commands, const Toolchain& tc) { + std::vector> env; + for (auto& ev : tc.envOverrides) env.emplace_back(ev.key, ev.value); std::string out; for (auto const& cmd : commands) { - if (auto r = run_capture_command(cmd); !r) return std::unexpected(r.error()); + if (auto r = run_capture_command(cmd, env); !r) return std::unexpected(r.error()); else out += *r; } return out; @@ -188,12 +199,14 @@ std::expected ensure_built( "toolchain has no std module source (import std unsupported on this compiler)"}); } + const bool isMsvc = tc.compiler == CompilerId::MSVC; StdModule sm; sm.cacheDir = cache_root / std::string(fingerprint_hex); - sm.bmiPath = is_clang(tc) + sm.bmiPath = isMsvc ? mcpp::toolchain::msvc::std_bmi_path(sm.cacheDir) + : is_clang(tc) ? mcpp::toolchain::clang::std_bmi_path(sm.cacheDir) : mcpp::toolchain::gcc::std_bmi_path(sm.cacheDir); - sm.objectPath = sm.cacheDir / "std.o"; + sm.objectPath = sm.cacheDir / (isMsvc ? "std.obj" : "std.o"); // Build sysroot + include flags for std module precompilation, derived // from the shared toolchain link model (same resolver as flags.cppm — @@ -222,18 +235,26 @@ std::expected ensure_built( macos_deployment_target); } - // Both providers expose the same command-sequence shape (A5 backend + // All three providers expose the same command-sequence shape (A5 backend // surface normalization) — no per-compiler arity branching here. - std::vector stdCommands = is_clang(tc) + std::vector stdCommands = + isMsvc ? mcpp::toolchain::msvc::std_module_build_commands( + tc, sm.cacheDir, cpp_standard_flag) + : is_clang(tc) ? mcpp::toolchain::clang::std_module_build_commands( tc, sm.cacheDir, sm.bmiPath, sysroot_flag, cpp_standard_flag) : mcpp::toolchain::gcc::std_module_build_commands( tc, sm.cacheDir, sysroot_flag, cpp_standard_flag); std::vector compatCommands; - if (is_clang(tc) && !tc.stdCompatSource.empty()) { - auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); - compatCommands = mcpp::toolchain::clang::std_compat_build_commands( - tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag, cpp_standard_flag); + if (!tc.stdCompatSource.empty()) { + if (isMsvc) { + compatCommands = mcpp::toolchain::msvc::std_compat_build_commands( + tc, sm.cacheDir, cpp_standard_flag); + } else if (is_clang(tc)) { + auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); + compatCommands = mcpp::toolchain::clang::std_compat_build_commands( + tc, sm.cacheDir, compatBmi, sm.bmiPath, sysroot_flag, cpp_standard_flag); + } } auto metadata = metadata_for(tc, cpp_standard, cpp_standard_flag, stdCommands, compatCommands); auto metaPath = metadata_path(sm.cacheDir); @@ -248,7 +269,7 @@ std::expected ensure_built( if (ec) return std::unexpected(StdModError{ std::format("cannot create '{}': {}", sm.bmiPath.parent_path().string(), ec.message())}); - auto out = run_commands(stdCommands); + auto out = run_commands(stdCommands, tc); if (!out) return std::unexpected(out.error()); if (!std::filesystem::exists(sm.bmiPath)) { @@ -259,17 +280,19 @@ std::expected ensure_built( rebuiltStd = true; } - // Build std.compat after std (std.compat depends on std, Clang only). - if (is_clang(tc) && !tc.stdCompatSource.empty()) { - auto compatBmi = mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); + // Build std.compat after std (std.compat imports std; Clang + MSVC). + if (!compatCommands.empty()) { + auto compatBmi = isMsvc + ? mcpp::toolchain::msvc::std_compat_bmi_path(sm.cacheDir) + : mcpp::toolchain::clang::std_compat_bmi_path(sm.cacheDir); if (rebuiltStd || !std::filesystem::exists(compatBmi) || !metadata_matches(metaPath, metadata)) { - if (auto out = run_commands(compatCommands); !out) { + if (auto out = run_commands(compatCommands, tc); !out) { return std::unexpected(out.error()); } } sm.compatBmiPath = compatBmi; - sm.compatObjectPath = sm.cacheDir / "std.compat.o"; + sm.compatObjectPath = sm.cacheDir / (isMsvc ? "std.compat.obj" : "std.compat.o"); } if (auto r = write_metadata(metaPath, metadata); !r) { diff --git a/tests/e2e/95_msvc_system_toolchain.sh b/tests/e2e/95_msvc_system_toolchain.sh index 2cdd9f88..0057c92b 100755 --- a/tests/e2e/95_msvc_system_toolchain.sh +++ b/tests/e2e/95_msvc_system_toolchain.sh @@ -58,12 +58,13 @@ out=$("$MCPP" toolchain install msvc 2>&1) \ [[ "$out" == *"already installed"* ]] \ || { echo "FAIL: install msvc message: $out"; exit 1; } -# 5) native cl.exe builds are gated with one owned message +# 5) native cl.exe builds WORK (0.0.90; the 0.0.88 gate is gone) — the full +# modules/import-std flow is covered by 99_msvc_native_build.sh; here we +# assert the basic hello builds and runs under msvc@system. "$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; } +out=$("$MCPP" run 2>&1) || { echo "FAIL: msvc hello build/run: $out"; exit 1; } +[[ "$out" == *"Hello"* || "$out" == *"hello"* ]] \ + || { echo "FAIL: hello output: $out"; exit 1; } -echo "PASS: msvc@system detection, selection, guidance, and build gate" +echo "PASS: msvc@system detection, selection, guidance, and native build" diff --git a/tests/e2e/97_mingw_toolchain.sh b/tests/e2e/97_mingw_toolchain.sh index f897ab69..a81864cc 100755 --- a/tests/e2e/97_mingw_toolchain.sh +++ b/tests/e2e/97_mingw_toolchain.sh @@ -64,7 +64,7 @@ run_out=$("$MCPP" run 2>&1) || { echo "FAIL: run: $run_out"; exit 1; } # it must run from a clean dir without the toolchain bin on PATH. EXE=$(find target -name "hello_mingw.exe" -path "*/bin/*" | head -1) [[ -n "$EXE" ]] || { echo "FAIL: no exe produced"; exit 1; } -OBJDUMP="${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-mingw-gcc/16.1.0/bin/objdump.exe" +OBJDUMP=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-mingw-gcc/*/bin/objdump.exe 2>/dev/null | head -1) imports="" if [[ -x "$OBJDUMP" ]]; then imports=$("$OBJDUMP" -p "$EXE" 2>/dev/null | grep -i "DLL Name" || true) diff --git a/tests/e2e/98_reflection_import_std.sh b/tests/e2e/98_reflection_import_std.sh new file mode 100755 index 00000000..86c046a3 --- /dev/null +++ b/tests/e2e/98_reflection_import_std.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# requires: gcc +# 98_reflection_import_std.sh — issue #210: dialect-class flags reach the std +# module BMI prebuild AND every TU in the graph: +# - [build] cxxflags = ["-freflection"] → `import std;` exposes std::meta +# - a dependency module that imports std builds in the same graph (the +# fmt.gcm-class secondary failure from the issue) +# - std-module.json records the flag in the std build command +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +# The resolved toolchain must accept -freflection (GCC 16+). Probe via the +# project's own toolchain resolution: build a trivial reflection TU. +GXX=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1) +if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then + echo "SKIP-INLINE: no -freflection-capable gcc payload available" + exit 0 +fi + +mkdir -p "$TMP/proj/src" "$TMP/proj/meta_dep/src" +cd "$TMP/proj" + +# Path dependency providing a module that ITSELF imports std — must be +# compiled with the same dialect set or GCC rejects the BMI mix. +cat > meta_dep/mcpp.toml <<'EOF' +[package] +name = "meta_dep" +version = "0.1.0" +standard = "c++26" + +[targets.meta_dep] +kind = "lib" +EOF +cat > meta_dep/src/meta_dep.cppm <<'EOF' +export module meta_dep; +import std; +export namespace meta_dep { +std::string tag() { return "dep-ok"; } +} +EOF + +cat > mcpp.toml <<'EOF' +[package] +name = "refl" +version = "0.1.0" +standard = "c++26" + +[build] +cxxflags = ["-freflection"] + +[dependencies] +meta_dep = { path = "meta_dep" } +EOF +cat > src/main.cpp <<'EOF' +import std; +import meta_dep; + +void print_struct(auto &&value) { + constexpr auto info = std::meta::remove_cvref(^^decltype(value)); + constexpr auto no_check = std::meta::access_context::unchecked(); + static constexpr auto members = std::define_static_array( + std::meta::nonstatic_data_members_of(info, no_check)); + template for (constexpr auto e : members) { + auto &&member = value.[:e:]; + std::println("{} {}", identifier_of(e), member); + } +} + +struct Point { double x{}; double y{}; }; +int main() { + print_struct(Point{2, 3}); + std::println("{}", meta_dep::tag()); +} +EOF + +out=$("$MCPP" run 2>&1) || { echo "FAIL: build/run: $out"; exit 1; } +[[ "$out" == *"x 2"* && "$out" == *"y 3"* ]] || { echo "FAIL: reflection output: $out"; exit 1; } +[[ "$out" == *"dep-ok"* ]] || { echo "FAIL: dep module output: $out"; exit 1; } + +# The std BMI build command must carry the dialect flag. +grep -rl '"std_flag": "[^"]*-freflection' "${MCPP_HOME:-$HOME/.mcpp}/bmi/" >/dev/null \ + || { echo "FAIL: std-module.json lacks -freflection in std_flag"; exit 1; } + +echo "PASS: dialect flags reach std BMI + whole module graph (issue #210)" diff --git a/tests/e2e/99_msvc_native_build.sh b/tests/e2e/99_msvc_native_build.sh new file mode 100755 index 00000000..3a91af42 --- /dev/null +++ b/tests/e2e/99_msvc_native_build.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# requires: msvc +# 99_msvc_native_build.sh — native cl.exe builds (0.0.90): +# - toolchain default msvc → new → build → run (import std via std.ixx/.ifc) +# - a named module (.cppm through /interface /TP + /ifcOutput) +# - incremental: touching one .cppm rebuilds, run still correct +set -e + +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 + +cd "$TMP" +"$MCPP" toolchain default msvc >/dev/null \ + || { echo "FAIL: toolchain default msvc"; exit 1; } + +"$MCPP" new hello_cl >/dev/null 2>&1 +cd hello_cl +mkdir -p src +cat > src/greet.cppm <<'EOF' +export module hello.greet; +import std; +export namespace hello { +std::string greet() { return "cl-ok"; } +} +EOF +cat > src/main.cpp <<'EOF' +import std; +import hello.greet; +int main() { std::println("{}", hello::greet()); return 0; } +EOF + +out=$("$MCPP" build 2>&1) || { echo "FAIL: msvc build: $out"; exit 1; } +run_out=$("$MCPP" run 2>&1) || { echo "FAIL: msvc run: $run_out"; exit 1; } +[[ "$run_out" == *"cl-ok"* ]] || { echo "FAIL: run output: $run_out"; exit 1; } + +# .obj + .ifc artifacts really came from the msvc pipeline +find target -name "*.obj" | grep -q . || { echo "FAIL: no .obj produced"; exit 1; } +find target -path "*ifc.cache*" -name "*.ifc" | grep -q . \ + || { echo "FAIL: no .ifc produced"; exit 1; } + +# incremental: edit the module, expect rebuild + updated output +sleep 1 +sed -i 's/cl-ok/cl-ok2/' src/greet.cppm +run_out=$("$MCPP" run 2>&1) || { echo "FAIL: incremental run: $run_out"; exit 1; } +[[ "$run_out" == *"cl-ok2"* ]] || { echo "FAIL: incremental output: $run_out"; exit 1; } + +echo "PASS: native MSVC build — modules, import std, incremental" diff --git a/tests/unit/test_toolchain_dialect.cpp b/tests/unit/test_toolchain_dialect.cpp index 4ca3d2a1..7e7c9726 100644 --- a/tests/unit/test_toolchain_dialect.cpp +++ b/tests/unit/test_toolchain_dialect.cpp @@ -1,6 +1,7 @@ #include import std; +import mcpp.manifest; import mcpp.toolchain.dialect; import mcpp.toolchain.model; import mcpp.toolchain.registry; @@ -81,6 +82,43 @@ TEST(MingwSpec, DisplayAndDefaultMatching) { EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "mingw-gcc", "16.1.0")); } +TEST(StdFlagFor, PerDialectSpelling) { + const auto& gnu = dialect_for(make_tc(CompilerId::GCC)); + const auto& msvc = dialect_for(make_tc(CompilerId::MSVC)); + EXPECT_EQ(std_flag_for(gnu, "c++26", 26), "-std=c++26"); + EXPECT_EQ(std_flag_for(gnu, "gnu++23", 23), "-std=gnu++23"); + EXPECT_EQ(std_flag_for(msvc, "c++20", 20), "/std:c++20"); + EXPECT_EQ(std_flag_for(msvc, "c++23", 23), "/std:c++latest"); + EXPECT_EQ(std_flag_for(msvc, "c++26", 26), "/std:c++latest"); +} + +// ─── issue #210: dialect-class flag extraction ─────────────────────────── + +TEST(DialectFlags, KnownListAndEscapeHatch) { + mcpp::manifest::BuildConfig bc; + bc.cxxflags = {"-freflection", "-O3", "-Wall", "-D_GLIBCXX_USE_CXX11_ABI=0"}; + bc.dialectCxxflags = {"-fcustom-std-thing", "-freflection"}; // dup dedups + auto flags = mcpp::manifest::dialect_flags(bc); + ASSERT_EQ(flags.size(), 3u); + EXPECT_EQ(flags[0], "-fcustom-std-thing"); // explicit first, order kept + EXPECT_EQ(flags[1], "-freflection"); // deduped against cxxflags copy + EXPECT_EQ(flags[2], "-D_GLIBCXX_USE_CXX11_ABI=0"); // auto-promoted +} + +TEST(DialectFlags, ExtractionDetails) { + using mcpp::manifest::is_dialect_flag; + EXPECT_TRUE(is_dialect_flag("-freflection")); + EXPECT_TRUE(is_dialect_flag("-fcontracts")); + EXPECT_TRUE(is_dialect_flag("-fno-char8_t")); + EXPECT_TRUE(is_dialect_flag("-D_GLIBCXX_USE_CXX11_ABI=1")); + // Conservative first list: these stay per-unit. + EXPECT_FALSE(is_dialect_flag("-fno-exceptions")); + EXPECT_FALSE(is_dialect_flag("-fno-rtti")); + EXPECT_FALSE(is_dialect_flag("-O2")); + EXPECT_FALSE(is_dialect_flag("-Wall")); + EXPECT_FALSE(is_dialect_flag("-fPIC")); +} + TEST(MingwModel, TargetPredicate) { EXPECT_TRUE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-w64-mingw32"))); EXPECT_FALSE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-linux-gnu")));