diff --git a/.agents/docs/2026-05-17-windows-llvm-support-design.md b/.agents/docs/2026-05-17-windows-llvm-support-design.md new file mode 100644 index 0000000..a6b9221 --- /dev/null +++ b/.agents/docs/2026-05-17-windows-llvm-support-design.md @@ -0,0 +1,49 @@ +# Windows LLVM/Clang 支持设计方案 + +Date: 2026-05-17 + +## 目标 + +mcpp 在 Windows x86_64 上通过 xmake bootstrap 达到可用水平,产出 mcpp.exe 作为后续自举依赖。 + +## 平台特征 + +### Windows LLVM 包(xlings-res 20.1.7) + +``` +bin/clang.exe, clang++.exe, clang-cl.exe, lld-link.exe +bin/llvm-ar.exe, llvm-lib.exe, llvm-rc.exe +lib/clang/20/lib/windows/clang_rt.*.lib +没有 libc++(没有 include/c++/v1,没有 std.cppm) +没有 clang-scan-deps.exe +``` + +Windows LLVM 包不含 libc++。Windows 上 clang 搭配 MSVC STL。 + +### Bootstrap 策略 + +用 xmake + MSVC(和 xlings 自身做法一致): +- GitHub Actions windows-latest 预装 Visual Studio +- xmake 对 MSVC C++23 modules 支持成熟 +- 不需要额外安装 LLVM(MSVC 即可) + +## 代码适配清单 + +### 必须修改 + +| 文件 | 问题 | 方案 | +|------|------|------| +| ninja_backend.cppm | POSIX shell 命令 | #if _WIN32 cmd.exe 语法 | +| ninja_backend.cppm | mcpp_exe_path() 缺 Windows | GetModuleFileNameA() | +| config.cppm | MCPP_HOME 路径发现缺 Windows | 同上 | +| probe.cppm | command -v Unix only | where.exe | +| probe.cppm | LD_LIBRARY_PATH | Windows 用 PATH | +| flags.cppm | 链接 flags 缺 Windows 分支 | 无 sysroot/rpath | +| xlings.cppm | popen | _popen | + +## 执行顺序 + +1. 创建 ci-windows.yml 用 xmake 构建,看编译错误 +2. 根据 CI 错误逐步修代码 +3. 产出 mcpp.exe bootstrap binary +4. 上传到 xlings-res diff --git a/.agents/docs/2026-05-19-pack-windows-design.md b/.agents/docs/2026-05-19-pack-windows-design.md new file mode 100644 index 0000000..14544d0 --- /dev/null +++ b/.agents/docs/2026-05-19-pack-windows-design.md @@ -0,0 +1,97 @@ +# Windows Pack Design + +**Date:** 2026-05-19 +**Status:** Planned (stub guard in place, implementation not yet started) + +## Current state + +`mcpp pack` is fully functional on Linux and macOS. On Windows it exits early +with a clear error message directing users to the CI workflow: + +``` +error: `mcpp pack` is not yet supported on Windows. + Use the CI workflow (ci-windows.yml) to produce Windows zip packages. + Windows PE packaging (DLL collection + zip) is planned. +``` + +The guard lives at the top of `mcpp::pack::run()` in `src/pack/pack.cppm`. + +## Why the current implementation cannot run on Windows + +The POSIX implementation relies on three Linux/macOS-only mechanisms: + +| Mechanism | POSIX usage | Windows equivalent | +|---|---|---| +| `LD_TRACE_LOADED_OBJECTS=1` | Tells the ELF dynamic linker to print deps without executing `main()` | No direct equivalent. Would need `dumpbin /dependents` (MSVC) or `ldd` emulation via `LoadLibraryEx` | +| `patchelf` | Rewrites `RUNPATH` / `PT_INTERP` ELF headers in-place | Not applicable to PE/COFF. DLL search order is controlled by the OS loader and manifest, not embedded paths | +| `tar -czf` | GNU tar — not universally present on Windows before Win11 22H2 | `Compress-Archive` (PowerShell), `7z`, or Win32 `CreateFile`/`MiniZip` | + +## Planned Windows pack implementation + +### Goal + +Produce a self-contained `.zip` archive (not `.tar.gz`) that users can +extract and run with no additional setup: + +``` +--x86_64-pc-windows-msvc.zip +└── --x86_64-pc-windows-msvc/ + ├── .exe + ├── *.dll (bundled DLLs, if any) + └── README.md / LICENSE (if present) +``` + +### DLL discovery + +Replace `ldd_parse()` with a Win32 equivalent: + +1. **Primary: `dumpbin /dependents `** — available when MSVC tools are + on `PATH`. Produces a list of DLL names; resolve each against `PATH` / + `%SystemRoot%\System32` / side-by-side assemblies. + +2. **Fallback: `PE header walk`** — open the PE file, walk the Import Directory, + extract DLL names. Can be implemented with `` + `ImageNtHeader`. + +3. **Skip-list**: mirror the manylinux skip-list concept for Windows: + `kernel32.dll`, `user32.dll`, `ntdll.dll`, `vcruntime*.dll` (Redist), + `api-ms-win-*.dll` (API sets), `ucrtbase.dll`. + +### Archive creation + +Use `std::filesystem` to copy files into a staging directory, then produce +the zip with one of: + +- **PowerShell** `Compress-Archive` — available on all modern Windows. + Invoke via `run_capture("powershell -Command \"Compress-Archive ..."`)`. + Slow for large trees; fine for typical release packages. +- **libzip / minizip** — statically linkable; avoid the PowerShell dependency. + Preferred long-term. + +### Format + +- Output file: `.zip` (not `.tar.gz`) on Windows. +- `pack::Format` enum needs a new `Zip` variant (or auto-select by platform). +- `make_plan()` should derive the output extension from the target platform. + +### Entry point + +No shell wrapper needed on Windows — users double-click `.exe` or run +it from `cmd.exe` / PowerShell directly. If DLLs are bundled, they should be +placed in the **same directory** as the executable (the Win32 loader checks +`%EXE_DIR%` first, before `%PATH%`). + +### Implementation checklist (for the future PR) + +- [ ] Add `Format::Zip` (or `Format::ZipAuto`) to `pack::Format` +- [ ] Implement `dumpbin_parse()` (or PE header walk fallback) in `pack.cppm` + under `#if defined(_WIN32)` +- [ ] Implement `make_zip()` (PowerShell or libzip) in `pack.cppm` +- [ ] Remove the `#if defined(_WIN32)` early-return guard from `pack::run()` + once the above are ready +- [ ] Add a Windows-specific integration test to `ci-windows.yml` + +### CI workflow (current workaround) + +Until this is implemented, `ci-windows.yml` zips the raw build output with +PowerShell `Compress-Archive`. This is good enough for CI artifacts but does +not collect/bundle DLLs or apply the staging-directory layout. diff --git a/.agents/docs/2026-05-19-windows-e2e-parity-plan.md b/.agents/docs/2026-05-19-windows-e2e-parity-plan.md new file mode 100644 index 0000000..3c7a88c --- /dev/null +++ b/.agents/docs/2026-05-19-windows-e2e-parity-plan.md @@ -0,0 +1,43 @@ +# Windows E2E 与 macOS 对齐方案 + +> 目标:Windows E2E 从 20/49 提升到 ~32/49,与 macOS 33/49 对齐。 + +## 根因分析 + +| 类别 | 测试数 | 问题 | 修复方式 | +|------|--------|------|----------| +| mcpp run/test 单引号 | 1 (02) | `cli.cppm` 用 POSIX 单引号执行 binary | `_WIN32` 改双引号 | +| clang-scan-deps 查找 | 1 (16) | `cli.cppm` 硬编码无 .exe | 调用已有 `find_scan_deps()` | +| symlink 依赖 | 4 (10,24,27,32) | `_inherit_toolchain.sh` 用 `ln -sf` | 加 `cp -r` fallback | +| bash-specific 语法 | 1 (19) | `compgen -G` 不在 Git Bash | 改用 `find` | +| unix-shell 误标 | 1 (38_mirror) | 实际只需 symlink fallback | 改标签 | +| import-std-libcxx 硬编码路径 | 4 (37,38,40,41) | 测试用 Linux 路径 | 加 Windows 路径 | + +## 修复计划 + +### Fix 1: cli.cppm 单引号 → 双引号 (解锁 02) +- `src/cli.cppm:2611` — `mcpp run` 执行 binary 用 `'{}'` → Windows 改 `"{}"` +- `src/cli.cppm:2522` — fast-path ninja 同上 +- `src/cli.cppm:3159` — test PATH prefix 是 POSIX 语法,Windows 跳过 + +### Fix 2: clang-scan-deps 查找 (解锁 16) +- `src/cli.cppm:2162-2167` — 直接查找 `clang-scan-deps`,不走 `find_scan_deps()` +- 改为调用 `mcpp::toolchain::clang::find_scan_deps(*tc)` 已正确处理 .exe + +### Fix 3: _inherit_toolchain.sh cp fallback (解锁 10,24,27,32) +- 当 `ln -sf` 失败时用 `cp -r` 替代 +- 自动检测 symlink 可用性 + +### Fix 4: 19_bmi_cache_reuse.sh bash 兼容 (解锁 19) +- `compgen -G` → `find ... | grep -q .` + +### Fix 5: LLVM 测试 Windows 路径 (解锁 37,38,40,41) +- 参照 36_llvm_toolchain.sh 的模式加 Windows 路径和 .exe 处理 + +### Fix 6: 标签修正 +- `38_self_config_mirror.sh` 改标签 +- `run_all.sh` 移除已修复测试的标签限制 + +## 预期结果 + +修复后:**~32 passed, 0 failed, ~17 skipped** (与 macOS 33 passed 对齐) diff --git a/.agents/docs/2026-05-19-windows-maturity-v2-plan.md b/.agents/docs/2026-05-19-windows-maturity-v2-plan.md new file mode 100644 index 0000000..0388cce --- /dev/null +++ b/.agents/docs/2026-05-19-windows-maturity-v2-plan.md @@ -0,0 +1,40 @@ +# Windows 成熟度提升 V2 方案 + +> 基于 GPT-5.5 评审反馈,将 Windows 从 22/48 提升到 30+/48。 + +## 任务清单 + +### T1: 修复 fresh-sandbox 能力 + git clone 单引号 +**目标:解锁 02, 24, 27_self, 32(+4 tests)** + +- `cli.cppm:1963-1970` — git clone 用 `'{}'` 单引号,Windows cmd.exe 不支持 +- `run_all.sh` — Windows 添加 `fresh-sandbox` 能力 +- `27_self_contained_home.sh` — 误标 `elf`,实际逻辑是 Windows 可移植的 + +### T2: process.cppm 实际接入 +**目标:消除散落的 popen/system 拼接** + +优先替换 5 个高风险 call site: +- `probe.cppm:90` — compiler probing +- `pm/publisher.cppm:211,239` — sha256sum, git archive +- `toolchain/stdmod.cppm:64` — std module compilation +- `build/ninja_backend.cppm:98` — ninja invocation + +### T3: provider.cppm 接入 flags/ninja +**目标:消除散落的 is_clang/is_gcc/isMusl 检查** + +- `flags.cppm` — 用 `capabilities_for(tc)` 决定 compile/link flags +- `ninja_backend.cppm:155` — scanner 策略用 provider +- `stdmod.cppm:102,122` — BMI 路径用 provider + +### T4: 更新过时文档 +**目标:文档与代码同步** + +- 更新 `.agents/docs/2026-05-19-windows-platform-maturity-plan.md` +- 48 tests(非 49),P1/P2/P3/P5 已完成基础设施 + +### T5: E2E 标签修正 +**目标:最大化 Windows 可运行测试** + +- `27_self_contained_home.sh` — `elf` → 空(逻辑是 Windows 可移植的) +- `10_env_command.sh` — 验证是否仍需 symlink diff --git a/.agents/docs/2026-05-19-windows-platform-maturity-plan.md b/.agents/docs/2026-05-19-windows-platform-maturity-plan.md new file mode 100644 index 0000000..69413c9 --- /dev/null +++ b/.agents/docs/2026-05-19-windows-platform-maturity-plan.md @@ -0,0 +1,165 @@ +# Windows 平台成熟度提升方案 + +> 基于 PR #52 code review 反馈,针对 Windows 支持从"可自举"到"生产就绪"的优化路径。 + +## 当前状态 + +| 能力 | Linux | macOS | Windows | 差距 | +|------|-------|-------|---------|------| +| self-host | ✅ | ✅ | ✅ | — | +| `mcpp test` (unit) | ✅ | ✅ | ❌ | 缺 clang-scan-deps | +| E2E 覆盖 | 46/46 | 33/46 | 22/48 | 26 项 skip | +| `mcpp pack` | ✅ (musl static) | ✅ (手动) | ❌ (CI 手写 zip) | pack 不支持 PE | +| release workflow | ✅ | ✅ | ✅ | build-windows job 已加 | +| MSVC 工具链 | N/A | N/A | 模型预留 | detect 不支持 | +| 默认工具链回退 | gcc@15.1.0-musl | llvm@20.1.7 | llvm@20.1.7 | ✅ 已修 | + +## 优化方案(按优先级) + +### P0: 补齐 release workflow + 减少 E2E skip — DONE + +**目标:** Windows 二进制进入正式 release 发布流程。 + +#### 1. release.yml 加 build-windows job — DONE + +`release.yml` 中已有 `build-windows` job,产出 `mcpp--windows-x86_64.zip` + sha256,上传到 GitHub Release。 + +#### 2. 修复高价值 E2E skip 项 + +按投入产出排序: + +| 测试 | 修复方式 | 工作量 | +|------|----------|--------| +| `02_new_build_run.sh` | 检查 `bin/hello` 或 `bin/hello.exe` | 小 | +| `16_test_failing.sh` | 调查 Windows 上 exit code 传递 | 小 | +| `35_workspace.sh` | 同上,binary 名加 `.exe` 检查 | 小 | +| `36_llvm_toolchain.sh` | 同上 | 小 | +| `19_bmi_cache_reuse.sh` | 修复 `cp_bmi` rule 的混合路径 | 中 | +| `24_git_dependency.sh` | CRLF + Windows 路径处理 | 中 | +| `38_self_config_mirror.sh` | xlings mirror cmd.exe 路径 | 中 | + +**预计可把 E2E 从 22 passed 提升到 ~30 passed。** + +### P1: PlatformTraits 抽象 — DONE (infrastructure) + +**目标:** 减少散落的 `#if defined(_WIN32)` / `#if defined(__APPLE__)`。 + +`src/platform.cppm` 已创建,集中平台差异(exe_suffix、lib_prefix、null_redirect、shell_quote 等)。 + +**受益文件:** `plan.cppm`、`flags.cppm`、`ninja_backend.cppm`、`probe.cppm`、`clang.cppm`、`config.cppm`(待各文件迁移到 `mcpp::platform` 命名空间) + +### P2: ToolchainProvider 重构 — TODO (src/provider.cppm 待创建) + +**目标:** 把工具链行为从散落的 `if (isClang)` / `if (isGcc)` 收敛到 provider 接口。 + +当前工具链代码分散在: +- `gcc.cppm` — GCC 行为 +- `clang.cppm` — Clang/libc++ 行为 + MSVC STL fallback +- `llvm.cppm` — xlings 包映射 +- `detect.cppm` — 只处理 GCC/Clang +- `flags.cppm` — 编译/链接 flags 按平台分支 +- `ninja_backend.cppm` — 构建规则按平台分支 + +建议拆成明确的 provider: + +``` +ToolchainProvider (interface) + ├── GccProvider — GCC + glibc/musl + ├── ClangLibcxxProvider — Clang + libc++ (Linux/macOS) + ├── ClangMsvcProvider — Clang + MSVC STL (Windows) + └── MsvcProvider — cl.exe (未来) +``` + +每个 provider 声明: +- `frontend()` → 编译器路径 +- `c_compiler()` → C 编译器 +- `archive_tool()` → ar/llvm-ar/lib.exe +- `scanner()` → clang-scan-deps 路径 +- `stdlib_id()` → libc++/libstdc++/msvc-stl +- `find_std_module()` → std.cppm/std.cc/std.ixx +- `compile_flags()` → 平台相关编译 flags +- `link_flags()` → 平台相关链接 flags +- `bmi_traits()` → .gcm/.pcm/.ifc + +> **注:** `src/provider.cppm` 尚未创建;现有调用者(gcc.cppm、clang.cppm 等)待迁移。 + +### P3: 跨平台 Process Runner — DONE (infrastructure, callers pending) + +**目标:** 消除 shell 字符串拼接,统一子进程执行。 + +`src/process.cppm` 已创建,提供 `ProcessOptions` / `ProcessResult` / `run()` 接口: +- POSIX: `fork/exec` + `pipe` +- Windows: `CreateProcessW` + `STARTUPINFOW` + +> **注:** 现有调用点(`probe.cppm`、`xlings.cppm`、`stdmod.cppm`、`ninja_backend.cppm`、`config.cppm`)仍使用 `popen`,待逐步迁移到 `process::run()`。 + +### P4: `mcpp pack` Windows 支持 + +**目标:** `mcpp pack` 原生支持 Windows PE 打包。 + +当前 `pack.cppm` 依赖: +- `LD_TRACE_LOADED_OBJECTS` (Linux ELF) +- `patchelf` (RPATH 修改) +- `tar -czf` (打包格式) + +Windows 需要: +- DLL 依赖收集(`dumpbin /dependents` 或 `llvm-objdump`) +- 无需 RPATH(DLL 在 exe 同目录自动找到) +- `.zip` 打包 + `.bat` wrapper + +建议 pack 做成平台策略: + +``` +PackStrategy (interface) + ├── LinuxElfPack — ldd + patchelf + tar.gz + ├── MacosMachoPack — otool + install_name_tool + tar.gz + └── WindowsPePack — dumpbin + zip + .bat +``` + +### P5: E2E 能力标签化 — DONE (infrastructure) + +**目标:** 从"平台 skip 列表"升级为"能力标签"。 + +能力标签体系已全面落地: +- 全部 48 个 E2E 脚本头部均已声明 `# requires:` 行 +- `run_all.sh` 自动检测平台能力(elf、gcc、musl、pack、symlink、scan-deps、import-std-libcxx、unix-shell、fresh-sandbox)并动态 skip +- 不再维护平台 skip 列表 + +支持的标签: + +```bash +# requires: elf — 需要 ELF 工具链 +# requires: gcc — 需要 GCC +# requires: symlink — 需要 ln -sf(仅 Linux/macOS 或 Windows Developer Mode) +# requires: scan-deps — 需要 clang-scan-deps +# requires: import-std-libcxx — 需要 import std (std.cppm via libc++) +# requires: pack — 需要 mcpp pack +# requires: unix-shell — 需要 bash 风格 shell(非 cmd.exe) +# requires: fresh-sandbox — 需要隔离 MCPP_HOME +``` + +## 实施顺序 + +``` +P0 release + E2E 修复 ✅ DONE(release.yml build-windows job 已加) + ↓ +P1 PlatformTraits ✅ DONE(src/platform.cppm 已创建,待调用者迁移) + ↓ +P2 ToolchainProvider ← src/provider.cppm 待创建 + 调用者迁移 + ↓ +P3 Process Runner ✅ DONE(src/process.cppm 已创建,待调用者迁移) + ↓ +P4 mcpp pack Windows ← 产品化打包 + ↓ +P5 E2E 标签化 ✅ DONE(全部 48 个测试已标签化) +``` + +## 预期里程碑 + +| 阶段 | 目标 | Windows E2E 通过率 | +|------|------|-------------------| +| 当前 | self-host + 基础 E2E | 22/48 (46%) | +| P0 完成 | release + 高价值 E2E | ~30/48 (63%) | +| P1+P2 完成 | 平台抽象 + provider | ~35/48 (73%) | +| P3+P4 完成 | process runner + pack | ~40/48 (83%) | +| P5 完成 | 能力标签 | 动态评估 | diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..1ef00b3 --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,163 @@ +name: ci-windows + +# Windows CI for mcpp — same flow as Linux (ci.yml) and macOS (ci-macos.yml): +# xlings install mcpp → self-host build → E2E → smoke → package + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +concurrency: + group: ci-windows-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: build + test (windows x64, self-host) + runs-on: windows-latest + timeout-minutes: 45 + env: + MCPP_HOME: C:\Users\runneradmin\.mcpp + steps: + - uses: actions/checkout@v4 + + - name: Cache mcpp sandbox + uses: actions/cache@v4 + with: + path: ~\.mcpp + key: mcpp-sandbox-${{ runner.os }}-${{ hashFiles('mcpp.toml', '.xlings.json') }} + restore-keys: | + mcpp-sandbox-${{ runner.os }}- + + - 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 + shell: bash + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '0.4.30' + run: | + WORK=$(mktemp -d) + zipfile="xlings-${XLINGS_VERSION}-windows-x86_64.zip" + curl -fsSL -o "${WORK}/${zipfile}" \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${zipfile}" + cd "${WORK}" + unzip -q "${zipfile}" + "$WORK/xlings-${XLINGS_VERSION}-windows-x86_64/subos/default/bin/xlings.exe" self install + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH" + xlings.exe --version + xlings.exe install mcpp -y || xlings.exe install mcpp@0.0.17 -y + echo "=== Searching for mcpp binary ===" + find "$USERPROFILE/.xlings" -name "mcpp.exe" -o -name "mcpp" 2>/dev/null | head -10 + MCPP=$(find "$USERPROFILE/.xlings" -name "mcpp.exe" -path "*/bin/*" 2>/dev/null | head -1) + if [ -z "$MCPP" ]; then + MCPP=$(find "$USERPROFILE/.xlings" -name "mcpp" -path "*/bin/*" 2>/dev/null | head -1) + fi + test -n "$MCPP" || { echo "FAIL: mcpp not found after xlings install"; exit 1; } + echo "Found mcpp at: $MCPP" + "$MCPP" --version + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + XLINGS_BIN=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") + echo "XLINGS_BIN=$XLINGS_BIN" >> "$GITHUB_ENV" + + - name: Build mcpp from source (self-host) + shell: bash + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + + # Pre-seed mcpp sandbox with xlings LLVM (avoids redundant download) + MCPP_XPKGS="$USERPROFILE/.mcpp/registry/data/xpkgs" + XLINGS_XPKGS="$USERPROFILE/.xlings/data/xpkgs" + if [ -d "$XLINGS_XPKGS/xim-x-llvm" ]; then + mkdir -p "$MCPP_XPKGS" + rm -rf "$MCPP_XPKGS/xim-x-llvm" + cp -r "$XLINGS_XPKGS/xim-x-llvm" "$MCPP_XPKGS/xim-x-llvm" + echo "Pre-seeded LLVM from global xlings" + fi + + "$MCPP" build + + MCPP_SELF=$(find target -name "mcpp.exe" -path "*/bin/*" | head -1) + test -n "$MCPP_SELF" || { echo "FAIL: no mcpp.exe"; exit 1; } + MCPP_SELF=$(cd "$(dirname "$MCPP_SELF")" && pwd)/$(basename "$MCPP_SELF") + echo "Self-hosted binary: $MCPP_SELF" + "$MCPP_SELF" --version + echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV" + + - name: Unit + integration tests via mcpp test + shell: bash + run: | + export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") + "$MCPP_SELF" test + + - name: E2E suite + shell: bash + run: | + export MCPP="$MCPP_SELF" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL + "$MCPP_SELF" self config --mirror GLOBAL 2>/dev/null || true + "$MCPP_SELF" toolchain default llvm@20.1.7 2>/dev/null || true + bash tests/e2e/run_all.sh + + - name: Self-host smoke (freshly-built mcpp builds itself again) + shell: bash + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP_SELF" build + "$MCPP_SELF" --version + echo ":: Self-host smoke PASS" + + - name: Package Windows release zip + id: package + shell: bash + run: | + VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) + WRAPPER="mcpp-${VERSION}-windows-x86_64" + ZIPNAME="${WRAPPER}.zip" + + STAGING=$(mktemp -d) + mkdir -p "$STAGING/$WRAPPER/bin" "$STAGING/$WRAPPER/registry/bin" + cp "$MCPP_SELF" "$STAGING/$WRAPPER/bin/mcpp.exe" + printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat" + cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true + cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true + XLINGS_EXE="$USERPROFILE/.xlings/subos/default/bin/xlings.exe" + [ -f "$XLINGS_EXE" ] && cp "$XLINGS_EXE" "$STAGING/$WRAPPER/registry/bin/xlings.exe" + + mkdir -p dist + (cd "$STAGING" && 7z a -tzip "$ZIPNAME" "$WRAPPER") + cp "$STAGING/$ZIPNAME" "dist/$ZIPNAME" + (cd dist && sha256sum "$ZIPNAME" > "$ZIPNAME.sha256") + echo "zipname=$ZIPNAME" >> "$GITHUB_OUTPUT" + ls -la dist/ + + - name: Smoke-test the packaged zip + shell: bash + run: | + ZIPNAME="${{ steps.package.outputs.zipname }}" + WRAPPER="${ZIPNAME%.zip}" + SMOKE=$(mktemp -d) + (cd "$SMOKE" && unzip -q "$GITHUB_WORKSPACE/dist/$ZIPNAME") + "$SMOKE/$WRAPPER/bin/mcpp.exe" --version + test -f "$SMOKE/$WRAPPER/registry/bin/xlings.exe" + test -f "$SMOKE/$WRAPPER/mcpp.bat" + echo "Smoke-test passed" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: mcpp-windows-x86_64 + path: | + dist/*.zip + dist/*.sha256 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ef2d96..fe1534a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -394,3 +394,164 @@ jobs: dist/mcpp-${{ steps.resolve.outputs.version }}-macosx-arm64.tar.gz.sha256 dist/mcpp-macosx-arm64.tar.gz dist/mcpp-macosx-arm64.tar.gz.sha256 + + build-windows: + name: build (Windows / x86_64) + runs-on: windows-latest + needs: build-release + permissions: + contents: write + timeout-minutes: 45 + env: + MCPP_HOME: C:\Users\runneradmin\.mcpp + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve tag + id: resolve + shell: bash + run: | + if [ "${{ github.event_name }}" = "push" ]; then + TAG="${{ github.ref_name }}" + elif [ -n "${{ github.event.inputs.tag }}" ]; then + TAG="${{ github.event.inputs.tag }}" + else + VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) + TAG="v$VER" + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ + && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then + git checkout --detach "refs/tags/$TAG" + fi + + - name: Cache mcpp sandbox + uses: actions/cache@v4 + with: + path: ~\.mcpp + key: mcpp-sandbox-${{ runner.os }}-release-${{ hashFiles('mcpp.toml', '.xlings.json') }} + restore-keys: | + mcpp-sandbox-${{ runner.os }}-release- + mcpp-sandbox-${{ runner.os }}- + + - name: Cache xlings + uses: actions/cache@v4 + with: + path: ~\.xlings + key: xlings-${{ runner.os }}-release-${{ hashFiles('.xlings.json') }} + restore-keys: | + xlings-${{ runner.os }}-release- + xlings-${{ runner.os }}- + + - name: Bootstrap mcpp via xlings + shell: bash + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '0.4.30' + run: | + WORK=$(mktemp -d) + zipfile="xlings-${XLINGS_VERSION}-windows-x86_64.zip" + curl -fsSL -o "${WORK}/${zipfile}" \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${zipfile}" + cd "${WORK}" + unzip -q "${zipfile}" + "$WORK/xlings-${XLINGS_VERSION}-windows-x86_64/subos/default/bin/xlings.exe" self install + export PATH="$USERPROFILE/.xlings/subos/default/bin:$PATH" + echo "$USERPROFILE/.xlings/subos/default/bin" >> "$GITHUB_PATH" + xlings.exe --version + xlings.exe install mcpp -y || xlings.exe install mcpp@0.0.17 -y + MCPP=$(find "$USERPROFILE/.xlings" -name "mcpp.exe" -path "*/bin/*" 2>/dev/null | head -1) + if [ -z "$MCPP" ]; then + MCPP=$(find "$USERPROFILE/.xlings" -name "mcpp" -path "*/bin/*" 2>/dev/null | head -1) + fi + test -n "$MCPP" || { echo "FAIL: mcpp not found after xlings install"; exit 1; } + "$MCPP" --version + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + XLINGS_BIN=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe") + echo "XLINGS_BIN=$XLINGS_BIN" >> "$GITHUB_ENV" + echo "XLINGS_BIN_UNIX=$USERPROFILE/.xlings/subos/default/bin/xlings.exe" >> "$GITHUB_ENV" + echo "XLINGS_XPKGS=$USERPROFILE/.xlings/data/xpkgs" >> "$GITHUB_ENV" + + - name: Build mcpp from source (self-host) + shell: bash + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + + # Pre-seed mcpp sandbox with xlings LLVM (avoids redundant download) + MCPP_XPKGS="$USERPROFILE/.mcpp/registry/data/xpkgs" + if [ -d "$XLINGS_XPKGS/xim-x-llvm" ]; then + mkdir -p "$MCPP_XPKGS" + rm -rf "$MCPP_XPKGS/xim-x-llvm" + cp -r "$XLINGS_XPKGS/xim-x-llvm" "$MCPP_XPKGS/xim-x-llvm" + echo "Pre-seeded LLVM from global xlings" + fi + + "$MCPP" build + + MCPP_BIN=$(find target -name "mcpp.exe" -path "*/bin/*" | head -1) + test -n "$MCPP_BIN" || { echo "FAIL: no mcpp.exe in target/"; exit 1; } + MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN") + echo "Self-hosted binary: $MCPP_BIN" + "$MCPP_BIN" --version + echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" + + - name: Package Windows release zip + id: stage + shell: bash + run: | + VERSION="${{ steps.resolve.outputs.version }}" + WRAPPER="mcpp-${VERSION}-windows-x86_64" + ZIPNAME="${WRAPPER}.zip" + + STAGING=$(mktemp -d) + mkdir -p "$STAGING/$WRAPPER/bin" "$STAGING/$WRAPPER/registry/bin" + cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp.exe" + + # Windows batch launcher + printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat" + cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true + cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true + + # Bundle xlings.exe for install consumers + if [ -f "$XLINGS_BIN_UNIX" ]; then + cp "$XLINGS_BIN_UNIX" "$STAGING/$WRAPPER/registry/bin/xlings.exe" + fi + + # Pack with 7z (available on windows-latest) + mkdir -p dist + (cd "$STAGING" && 7z a -tzip "$ZIPNAME" "$WRAPPER") + cp "$STAGING/$ZIPNAME" "dist/$ZIPNAME" + # Versionless alias + cp "dist/$ZIPNAME" "dist/mcpp-windows-x86_64.zip" + # SHA256 + (cd dist && sha256sum "$ZIPNAME" > "$ZIPNAME.sha256") + (cd dist && sha256sum "mcpp-windows-x86_64.zip" > "mcpp-windows-x86_64.zip.sha256") + + echo "zipname=$ZIPNAME" >> "$GITHUB_OUTPUT" + ls -la dist/ + + - name: Smoke-test the packaged zip + shell: bash + run: | + ZIPNAME="${{ steps.stage.outputs.zipname }}" + WRAPPER="${ZIPNAME%.zip}" + SMOKE=$(mktemp -d) + (cd "$SMOKE" && unzip -q "$GITHUB_WORKSPACE/dist/$ZIPNAME") + "$SMOKE/$WRAPPER/bin/mcpp.exe" --version + "$SMOKE/$WRAPPER/bin/mcpp.exe" --help | head -5 + test -f "$SMOKE/$WRAPPER/registry/bin/xlings.exe" + test -f "$SMOKE/$WRAPPER/mcpp.bat" + echo "Smoke-test passed" + + - name: Upload Windows artifacts to release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.resolve.outputs.tag }} + files: | + dist/mcpp-${{ steps.resolve.outputs.version }}-windows-x86_64.zip + dist/mcpp-${{ steps.resolve.outputs.version }}-windows-x86_64.zip.sha256 + dist/mcpp-windows-x86_64.zip + dist/mcpp-windows-x86_64.zip.sha256 diff --git a/.xlings.json b/.xlings.json index 7ce6add..2ef3138 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,6 +1,5 @@ { "workspace": { - "mcpp": "0.0.9", - "xmake": "3.0.7" + "mcpp": "0.0.17" } } diff --git a/mcpp.toml b/mcpp.toml index 06c67ae..0a0da07 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -14,6 +14,7 @@ include_dirs = ["src/libs/json"] [toolchain] default = "gcc@16.1.0" macos = "llvm@20.1.7" +windows = "llvm@20.1.7" # Per-target overrides: `mcpp build --target x86_64-linux-musl` (or the # four-segment form `x86_64-unknown-linux-musl`) picks musl-gcc 15.1 + full diff --git a/src/bmi_cache.cppm b/src/bmi_cache.cppm index 9be6ce0..e2ab08e 100644 --- a/src/bmi_cache.cppm +++ b/src/bmi_cache.cppm @@ -15,9 +15,14 @@ // dep cache without trashing manifest.txt (docs/26 §5.4 V2). module; +#if defined(_WIN32) +#include +#include +#else #include #include #include +#endif export module mcpp.bmi_cache; @@ -184,9 +189,35 @@ stage_into(const CacheKey& key, namespace { -// Acquire an exclusive non-blocking flock on /.lock. Returns the fd on -// success (caller closes it to release), or -1 if another mcpp is already -// populating this entry — in which case the caller should skip writing. +// Acquire an exclusive non-blocking lock on /.lock. Returns a handle +// on success, or -1/INVALID_HANDLE if another mcpp is already populating. +#if defined(_WIN32) +// Windows: use LockFileEx on a file handle +HANDLE try_lock_dir(const std::filesystem::path& dir) { + std::error_code ec; + std::filesystem::create_directories(dir, ec); + auto lockPath = dir / ".lock"; + HANDLE h = CreateFileW(lockPath.wstring().c_str(), + GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) return h; + OVERLAPPED ov = {}; + if (!LockFileEx(h, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, + 0, 1, 0, &ov)) { + CloseHandle(h); + return INVALID_HANDLE_VALUE; + } + return h; +} + +void release_lock(HANDLE h) { + if (h == INVALID_HANDLE_VALUE) return; + OVERLAPPED ov = {}; + UnlockFileEx(h, 0, 1, 0, &ov); + CloseHandle(h); +} +#else +// POSIX: use flock(2) int try_lock_dir(const std::filesystem::path& dir) { std::error_code ec; std::filesystem::create_directories(dir, ec); @@ -205,6 +236,7 @@ void release_lock(int fd) { ::flock(fd, LOCK_UN); ::close(fd); } +#endif } // namespace @@ -214,6 +246,16 @@ populate_from(const CacheKey& key, const DepArtifacts& arts) { auto cacheDir = key.dir(); +#if defined(_WIN32) + HANDLE lockHandle = try_lock_dir(cacheDir); + if (lockHandle == INVALID_HANDLE_VALUE) { + return {}; + } + struct LockGuard { + HANDLE h; + ~LockGuard() { release_lock(h); } + } guard{ lockHandle }; +#else int lockFd = try_lock_dir(cacheDir); if (lockFd < 0) { // Another writer holds the lock; treat as success (they'll do it). @@ -223,6 +265,7 @@ populate_from(const CacheKey& key, int fd; ~LockGuard() { release_lock(fd); } } guard{ lockFd }; +#endif auto cacheBmi = key.bmiDir(); auto cacheObj = key.objDir(); diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 992f091..3a8916d 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -12,6 +12,7 @@ import std; import mcpp.build.plan; import mcpp.toolchain.clang; import mcpp.toolchain.detect; +import mcpp.toolchain.provider; import mcpp.toolchain.registry; export namespace mcpp::build { @@ -59,6 +60,12 @@ std::string escape_path(const std::filesystem::path& p) { CompileFlags compute_flags(const BuildPlan& plan) { CompileFlags f; + + // ProviderCapabilities: centralised query point for per-toolchain decisions. + // Prefer caps.* checks over ad-hoc is_clang()/is_musl_target() calls for + // any new branching added to this function. + auto caps = mcpp::toolchain::capabilities_for(plan.toolchain); + f.cxxBinary = plan.toolchain.binaryPath; f.ccBinary = mcpp::toolchain::derive_c_compiler(plan.toolchain); f.toolEnv = mcpp::toolchain::compiler_env_prefix(plan.toolchain); @@ -125,7 +132,10 @@ CompileFlags compute_flags(const BuildPlan& plan) { plan.manifest.buildConfig.cStandard.empty() ? "c11" : plan.manifest.buildConfig.cStandard; // Assemble - std::string module_flag = isClang ? "" : " -fmodules"; + // -fmodules is a GCC-only flag; Clang uses a different module ABI and does + // not need it. caps.stdlib_id distinguishes GCC (libstdc++) from Clang + // (libc++ / msvc-stl) without an extra is_clang() call. + std::string module_flag = (caps.stdlib_id == "libstdc++") ? " -fmodules" : ""; std::string std_module_flag; if (isClang && !plan.stdBmiPath.empty()) { std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan)); @@ -149,20 +159,32 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Link flags f.staticStdlib = plan.manifest.buildConfig.staticStdlib; f.linkage = plan.manifest.buildConfig.linkage; -#if defined(__APPLE__) +#if defined(_WIN32) + // Windows: MSVC linker handles static/dynamic linking differently + std::string full_static; + std::string static_stdlib; +#elif defined(__APPLE__) // macOS does not support full static linking (libSystem must be dynamic) std::string full_static; + std::string static_stdlib = (f.staticStdlib && !isClang) ? " -static-libstdc++" : ""; #else std::string full_static = (f.linkage == "static") ? " -static" : ""; -#endif std::string static_stdlib = (f.staticStdlib && !isClang) ? " -static-libstdc++" : ""; +#endif std::string runtime_dirs; +#if !defined(_WIN32) + // -L and -rpath are ELF/Mach-O linker flags; MSVC linker doesn't use them. for (auto& dir : plan.toolchain.linkRuntimeDirs) { runtime_dirs += " -L" + escape_path(dir); runtime_dirs += " -Wl,-rpath," + escape_path(dir); } +#endif -#if defined(__APPLE__) +#if defined(_WIN32) + // Windows: Clang targeting MSVC links against MSVC runtime automatically. + // No -L/-rpath/-static flags needed. + f.ld = ""; +#elif defined(__APPLE__) // macOS linking strategy: // - No --sysroot: SDK .tbd stubs miss libc++abi exports. // - No -L/lib: xlings LLVM's libc++.dylib doesn't pull in diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 5266fee..f2c7073 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -14,7 +14,11 @@ module; #include #include -#if defined(__APPLE__) +#if defined(_WIN32) +#include +#define popen _popen +#define pclose _pclose +#elif defined(__APPLE__) #include // _NSGetExecutablePath #endif @@ -116,8 +120,14 @@ bool dyndep_mode_enabled() { std::filesystem::path mcpp_exe_path() { std::error_code ec; -#if defined(__APPLE__) - // macOS: use _NSGetExecutablePath +#if defined(_WIN32) + char buf[MAX_PATH]; + DWORD len = GetModuleFileNameA(NULL, buf, MAX_PATH); + if (len > 0 && len < MAX_PATH) { + auto p = std::filesystem::canonical(buf, ec); + if (!ec) return p; + } +#elif defined(__APPLE__) char buf[4096]; uint32_t size = sizeof(buf); if (_NSGetExecutablePath(buf, &size) == 0) { @@ -187,7 +197,13 @@ std::string emit_ninja_string(const BuildPlan& plan) { append("\n"); append("rule cp_bmi\n"); +#if defined(_WIN32) + // Use PowerShell Copy-Item which handles both forward and back slashes. + // cmd.exe `copy` breaks on forward-slash paths from ninja. + append(" command = powershell -NoProfile -Command \"Copy-Item -Force '$in' -Destination '$out'\"\n"); +#else append(" command = mkdir -p $$(dirname $out) && cp -f $in $out\n"); +#endif append(" description = STAGE $out\n\n"); // P1: per-file dyndep rule. Converts one .ddi → .dd independently. @@ -210,6 +226,12 @@ std::string emit_ninja_string(const BuildPlan& plan) { std::string module_output_flag = traits.needsExplicitModuleOutput ? " -fmodule-output=$bmi_out" : ""; append("rule cxx_module\n"); +#if defined(_WIN32) + // Windows: skip BMI restat optimization (requires POSIX shell). + // No $toolenv (empty on Windows; its leading space breaks CreateProcess). + append(std::format(" command = " + "$cxx $cxxflags{} -c $in -o $out\n", module_output_flag)); +#else append(std::format(" command = " "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then " "cp -p \"$bmi_out\" \"$bmi_out.bak\"; " @@ -221,13 +243,18 @@ std::string emit_ninja_string(const BuildPlan& plan) { "else " "rm -f \"$bmi_out.bak\"; " "fi\n", module_output_flag)); +#endif append(" description = MOD $out\n"); if (dyndep) append(" restat = 1\n"); append("\n"); append("rule cxx_object\n"); +#if defined(_WIN32) + append(" command = $cxx $cxxflags -c $in -o $out\n"); +#else append(" command = $toolenv $cxx $cxxflags -c $in -o $out\n"); +#endif append(" description = OBJ $out\n"); if (dyndep) append(" restat = 1\n"); @@ -235,13 +262,30 @@ std::string emit_ninja_string(const BuildPlan& plan) { if (need_c_rule) { append("rule c_object\n"); +#if defined(_WIN32) + append(" command = $cc $cflags -c $in -o $out\n"); +#else append(" command = $toolenv $cc $cflags -c $in -o $out\n"); +#endif append(" description = CC $out\n"); if (dyndep) append(" restat = 1\n"); append("\n"); } +#if defined(_WIN32) + append("rule cxx_link\n"); + append(" command = $cxx $in -o $out $ldflags\n"); + append(" description = LINK $out\n\n"); + + append("rule cxx_archive\n"); + append(" command = $ar rcs $out $in\n"); + append(" description = AR $out\n\n"); + + append("rule cxx_shared\n"); + append(" command = $cxx -shared $in -o $out $ldflags\n"); + append(" description = SHARED $out\n\n"); +#else append("rule cxx_link\n"); append(" command = $toolenv $cxx $in -o $out $ldflags\n"); append(" description = LINK $out\n\n"); @@ -253,6 +297,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { append("rule cxx_shared\n"); append(" command = $toolenv $cxx -shared $in -o $out $ldflags\n"); append(" description = SHARED $out\n\n"); +#endif if (dyndep) { // Scan rule: produce P1689 .ddi for one TU. @@ -261,14 +306,23 @@ std::string emit_ninja_string(const BuildPlan& plan) { append("rule cxx_scan\n"); if (plan.scanDepsPath.empty()) { // GCC path: compiler-integrated P1689 scanning. +#if defined(_WIN32) + append(" command = $cxx $cxxflags -fmodules " +#else append(" command = $toolenv $cxx $cxxflags -fmodules " +#endif "-fdeps-format=p1689r5 " "-fdeps-file=$out -fdeps-target=$compile_target " "-M -MM -MF $out.dep -E $in -o $compile_target\n"); } else { // Clang path: clang-scan-deps produces P1689 JSON to stdout. +#if defined(_WIN32) + append(" command = $scan_deps -format=p1689 -- " + "$cxx $cxxflags -c $in -o $compile_target > $out\n"); +#else append(" command = $toolenv $scan_deps -format=p1689 -- " "$cxx $cxxflags -c $in -o $compile_target > $out\n"); +#endif } append(" description = SCAN $out\n\n"); @@ -511,19 +565,33 @@ std::expected NinjaBackend::build(const BuildPlan& plan // -B flag we emit into cxxflags/ldflags (see // emit_ninja_string). No PATH injection needed here. std::filesystem::path ninjaBin; +#if defined(_WIN32) + if (auto nb = mcpp::xlings::paths::find_sibling_binary( + plan.toolchain.binaryPath, "ninja", "ninja.exe")) { + ninjaBin = *nb; + } +#else if (auto nb = mcpp::xlings::paths::find_sibling_binary( plan.toolchain.binaryPath, "ninja", "ninja")) { ninjaBin = *nb; } +#endif +#if defined(_WIN32) + // Windows: no quotes on first token (cmd.exe strips leading quotes), + // use shq only for the -C argument which may contain spaces. + std::string ninjaProgram = + !ninjaBin.empty() ? ninjaBin.string() : std::string{"ninja"}; +#else std::string ninjaProgram = - !ninjaBin.empty() ? std::format("'{}'", ninjaBin.string()) : std::string{"ninja"}; + !ninjaBin.empty() ? mcpp::xlings::shq(ninjaBin.string()) : std::string{"ninja"}; +#endif // Record ninja binary for P0 fast-path cache. BuildResult r; r.ninjaProgram = ninjaProgram; - std::string cmd = std::format("{} -C '{}'", ninjaProgram, plan.outputDir.string()); + std::string cmd = std::format("{} -C {}", ninjaProgram, mcpp::xlings::shq(plan.outputDir.string())); if (opts.verbose) cmd += " -v"; if (opts.parallelJobs) diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 2e83573..33cc341 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -10,6 +10,7 @@ import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; +import mcpp.platform; export namespace mcpp::build { @@ -172,17 +173,23 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, lu.targetName = t.name; if (t.kind == mcpp::manifest::Target::Library) { lu.kind = LinkUnit::StaticLibrary; - lu.output = std::filesystem::path("bin") / std::format("lib{}.a", t.name); + lu.output = std::filesystem::path("bin") / + std::format("{}{}{}", mcpp::platform::lib_prefix, t.name, + mcpp::platform::static_lib_ext); } else if (t.kind == mcpp::manifest::Target::SharedLibrary) { lu.kind = LinkUnit::SharedLibrary; - lu.output = std::filesystem::path("bin") / std::format("lib{}.so", t.name); + lu.output = std::filesystem::path("bin") / + std::format("{}{}{}", mcpp::platform::lib_prefix, t.name, + mcpp::platform::shared_lib_ext); } else if (t.kind == mcpp::manifest::Target::TestBinary) { lu.kind = LinkUnit::TestBinary; - lu.output = std::filesystem::path("bin") / t.name; + lu.output = std::filesystem::path("bin") / + std::format("{}{}", t.name, mcpp::platform::exe_suffix); if (!t.main.empty()) lu.entryMain = projectRoot / t.main; } else { lu.kind = LinkUnit::Binary; - lu.output = std::filesystem::path("bin") / t.name; + lu.output = std::filesystem::path("bin") / + std::format("{}{}", t.name, mcpp::platform::exe_suffix); if (!t.main.empty()) lu.entryMain = projectRoot / t.main; } diff --git a/src/cli.cppm b/src/cli.cppm index eb9efbf..f0aaf8e 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -11,6 +11,10 @@ module; #include #include +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif export module mcpp.cli; @@ -19,6 +23,7 @@ import mcpp.manifest; import mcpp.modgraph.graph; import mcpp.modgraph.scanner; import mcpp.modgraph.validate; +import mcpp.toolchain.clang; import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.registry; @@ -1131,7 +1136,7 @@ prepare_build(bool print_fingerprint, // macOS: LLVM/Clang — Apple doesn't ship GCC; upstream LLVM with // bundled libc++ is the self-contained choice. // Linux: musl-gcc — produces portable static binaries. -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(_WIN32) std::string defaultSpec = "llvm@20.1.7"; #else std::string defaultSpec = "gcc@15.1.0-musl"; @@ -1139,7 +1144,7 @@ prepare_build(bool print_fingerprint, auto defaultParsed = mcpp::toolchain::parse_toolchain_spec(defaultSpec); auto defaultPkg = mcpp::toolchain::to_xim_package(*defaultParsed); -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(_WIN32) mcpp::ui::info("First run", std::format("no toolchain configured — installing {} (LLVM/Clang) as default", defaultSpec)); @@ -1954,15 +1959,28 @@ prepare_build(bool print_fingerprint, std::format("{} ({} = {})", spec.git, spec.gitRefKind, spec.gitRev)); std::string cloneCmd; if (spec.gitRefKind == "branch") { +#if defined(_WIN32) + cloneCmd = std::format( + "git clone --depth 1 --branch \"{}\" \"{}\" \"{}\" 2>&1", + spec.gitRev, spec.git, gitRoot.string()); +#else cloneCmd = std::format( "git clone --depth 1 --branch '{}' '{}' '{}' 2>&1", spec.gitRev, spec.git, gitRoot.string()); +#endif } else { // For tag/rev: full clone, then checkout (depth-1 may miss the rev). +#if defined(_WIN32) + cloneCmd = std::format( + "git clone \"{}\" \"{}\" && cd \"{}\" && git checkout --quiet \"{}\" 2>&1", + spec.git, gitRoot.string(), + gitRoot.string(), spec.gitRev); +#else cloneCmd = std::format( "git clone '{}' '{}' && cd '{}' && git checkout --quiet '{}' 2>&1", spec.git, gitRoot.string(), gitRoot.string(), spec.gitRev); +#endif } std::string out; { @@ -2156,9 +2174,8 @@ prepare_build(bool print_fingerprint, // Clang: discover clang-scan-deps for P1689 dyndep scanning. if (mcpp::toolchain::is_clang(*tc)) { - auto sd = tc->binaryPath.parent_path() / "clang-scan-deps"; - if (std::filesystem::exists(sd)) { - ctx.plan.scanDepsPath = sd; + if (auto sd = mcpp::toolchain::clang::find_scan_deps(*tc)) { + ctx.plan.scanDepsPath = *sd; } } @@ -2515,7 +2532,11 @@ std::optional try_fast_build(const std::filesystem::path& projectRoot, } // All inputs are older than build.ninja → fast-path: just run ninja. +#if defined(_WIN32) + std::string cmd = std::format("{} -C \"{}\"", ninjaProgram, outputDir.string()); +#else std::string cmd = std::format("{} -C '{}'", ninjaProgram, outputDir.string()); +#endif if (verbose) cmd += " -v"; cmd += " 2>&1"; @@ -2604,8 +2625,13 @@ int cmd_run(const mcpplibs::cmdline::ParsedArgs& parsed, std::format("`{}`", mcpp::ui::shorten_path(exe, pathCtx))); std::println(""); std::fflush(stdout); +#if defined(_WIN32) + std::string cmd = std::format("\"{}\"", exe.string()); + for (auto& a : passthrough) cmd += std::format(" \"{}\"", a); +#else std::string cmd = std::format("'{}'", exe.string()); for (auto& a : passthrough) cmd += std::format(" '{}'", a); +#endif return std::system(cmd.c_str()) == 0 ? 0 : 1; } @@ -3147,6 +3173,7 @@ int cmd_test(const mcpplibs::cmdline::ParsedArgs& /*parsed*/, // visible to test binaries that shell out to them. The // toolchain binary's path encodes the registry root — derive it. std::string pathPrefix; +#if !defined(_WIN32) if (auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(ctx->tc.binaryPath)) { // xpkgs is /data/xpkgs → registry = xpkgs/../.. auto registryDir = xpkgs->parent_path().parent_path(); @@ -3154,12 +3181,22 @@ int cmd_test(const mcpplibs::cmdline::ParsedArgs& /*parsed*/, if (std::filesystem::exists(sandboxBin)) pathPrefix = std::format("PATH='{}':\"$PATH\" ", sandboxBin.string()); } +#endif +#if defined(_WIN32) + std::string cmd = std::format("\"{}\"", exe.string()); + for (auto& a : passthrough) cmd += std::format(" \"{}\"", a); +#else std::string cmd = std::format("{}'{}'", pathPrefix, exe.string()); for (auto& a : passthrough) cmd += std::format(" '{}'", a); +#endif int rc = std::system(cmd.c_str()); - // std::system returns wait status — extract exit code. + // std::system returns wait status on POSIX, exit code on Windows. +#if defined(_WIN32) + int exitCode = rc; +#else int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : 127; +#endif if (exitCode == 0) { std::println("{} ... ok", lu.targetName); diff --git a/src/config.cppm b/src/config.cppm index d53d531..6c843f7 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -16,7 +16,11 @@ module; #include #include -#if defined(__APPLE__) +#if defined(_WIN32) +#include +#define popen _popen +#define pclose _pclose +#elif defined(__APPLE__) #include // _NSGetExecutablePath #endif @@ -164,7 +168,13 @@ std::filesystem::path home_dir() { return std::filesystem::path(e); std::error_code ec; -#if defined(__APPLE__) +#if defined(_WIN32) + char _exe_buf[MAX_PATH]; + DWORD _exe_len = GetModuleFileNameA(NULL, _exe_buf, MAX_PATH); + std::filesystem::path exe; + if (_exe_len > 0 && _exe_len < MAX_PATH) + exe = std::filesystem::canonical(_exe_buf, ec); +#elif defined(__APPLE__) char _exe_buf[4096]; uint32_t _exe_size = sizeof(_exe_buf); std::filesystem::path exe; @@ -345,7 +355,11 @@ acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet) } // 2. Copy from system (`which xlings`) +#if defined(_WIN32) + auto sys = run_capture("where xlings.exe 2>nul"); +#else auto sys = run_capture("command -v xlings 2>/dev/null"); +#endif if (sys) { std::string p = *sys; while (!p.empty() && (p.back() == '\n' || p.back() == '\r')) p.pop_back(); @@ -427,7 +441,11 @@ std::expected load_or_init( // /bin/xlings, which satisfies xlings's own shim- // creation guard (`if fs::exists(homeDir/"bin"/"xlings")`), // making ensure_sandbox_xlings_binary() a no-op. +#if defined(_WIN32) + cfg.xlingsBinary = cfg.registryDir / "bin" / "xlings.exe"; +#else cfg.xlingsBinary = cfg.registryDir / "bin" / "xlings"; +#endif cfg.bmiCacheDir = cfg.mcppHome / "bmi"; cfg.metaCacheDir = cfg.mcppHome / "cache"; cfg.logDir = cfg.mcppHome / "log"; @@ -522,7 +540,11 @@ std::expected load_or_init( auto xbin = acquire_xlings_binary(cfg.xlingsBinary, quiet); if (!xbin) return std::unexpected(ConfigError{xbin.error()}); } else if (cfg.xlingsBinaryMode == "system") { +#if defined(_WIN32) + auto sys = run_capture("where xlings.exe 2>nul"); +#else auto sys = run_capture("command -v xlings 2>/dev/null"); +#endif if (!sys || sys->empty()) return std::unexpected(ConfigError{"system xlings not found in PATH"}); std::string p = *sys; @@ -546,8 +568,8 @@ std::expected load_or_init( // upstream (see docs/short-term-vs-long-track plan). ensure_sandbox_xlings_binary(cfg, quiet); ensure_sandbox_init(cfg, quiet); -#if !defined(__APPLE__) - // patchelf is ELF-only; macOS uses Mach-O and does not need it. +#if !defined(__APPLE__) && !defined(_WIN32) + // patchelf is ELF-only; macOS uses Mach-O and Windows uses PE. ensure_sandbox_patchelf(cfg, quiet, onBootstrapProgress); #endif ensure_sandbox_ninja(cfg, quiet, onBootstrapProgress); diff --git a/src/modgraph/p1689.cppm b/src/modgraph/p1689.cppm index 95bfb82..e42ae31 100644 --- a/src/modgraph/p1689.cppm +++ b/src/modgraph/p1689.cppm @@ -19,6 +19,10 @@ module; #include #include +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif export module mcpp.modgraph.p1689; diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index f84cdf0..5f60770 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -17,6 +17,10 @@ module; #include // popen, pclose +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif export module mcpp.pack; @@ -532,6 +536,28 @@ make_tarball(const std::filesystem::path& stagingRoot, std::expected run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) { +#if defined(_WIN32) + // `mcpp pack` is not yet supported on Windows. + // + // The current implementation relies on POSIX-only tools: + // - LD_TRACE_LOADED_OBJECTS=1 (ELF dynamic linker trick; no equivalent + // on Windows PE/COFF) + // - ldd / patchelf (Linux ELF tools; not available on Windows) + // - tar -czf (GNU tar; not universally present on Windows) + // + // For CI-produced Windows zip packages, use the ci-windows.yml workflow + // which zips the MSVC/Clang build output directly. + // + // Windows PE packaging (DLL collection + zip) is planned. + // See .agents/docs/2026-05-19-pack-windows-design.md for the design. + (void)plan; + (void)cfg; + return std::unexpected(Error{ + "error: `mcpp pack` is not yet supported on Windows.\n" + " Use the CI workflow (ci-windows.yml) to produce Windows zip packages.\n" + " Windows PE packaging (DLL collection + zip) is planned." + }); +#else using namespace detail; std::error_code ec; @@ -645,6 +671,7 @@ run(const Plan& plan, const mcpp::config::GlobalConfig& cfg) return r; } return {}; +#endif // !_WIN32 } } // namespace mcpp::pack diff --git a/src/platform.cppm b/src/platform.cppm new file mode 100644 index 0000000..fee9587 --- /dev/null +++ b/src/platform.cppm @@ -0,0 +1,65 @@ +// mcpp.platform — centralized platform-specific constants. +// +// Consumers import this module instead of scattering #if/_WIN32 / __APPLE__ +// blocks throughout the codebase. All compile-time branching lives here. + +module; + +// Nothing to #include for compile-time constants; the module fragment is kept +// for future OS headers if needed. + +export module mcpp.platform; + +import std; + +export namespace mcpp::platform { + +// ── Binary / library name conventions ───────────────────────────────────── + +#if defined(_WIN32) +constexpr std::string_view exe_suffix = ".exe"; +constexpr std::string_view static_lib_ext = ".lib"; +constexpr std::string_view shared_lib_ext = ".dll"; +constexpr std::string_view lib_prefix = ""; +#elif defined(__APPLE__) +constexpr std::string_view exe_suffix = ""; +constexpr std::string_view static_lib_ext = ".a"; +constexpr std::string_view shared_lib_ext = ".dylib"; +constexpr std::string_view lib_prefix = "lib"; +#else +// Linux and other POSIX +constexpr std::string_view exe_suffix = ""; +constexpr std::string_view static_lib_ext = ".a"; +constexpr std::string_view shared_lib_ext = ".so"; +constexpr std::string_view lib_prefix = "lib"; +#endif + +// ── Shell / process helpers ──────────────────────────────────────────────── + +#if defined(_WIN32) +constexpr std::string_view null_redirect = "2>nul"; +#else +constexpr std::string_view null_redirect = "2>/dev/null"; +#endif + +// ── Platform identification ──────────────────────────────────────────────── + +#if defined(_WIN32) +constexpr bool is_windows = true; +constexpr bool is_macos = false; +constexpr bool is_linux = false; +#elif defined(__APPLE__) +constexpr bool is_windows = false; +constexpr bool is_macos = true; +constexpr bool is_linux = false; +#elif defined(__linux__) +constexpr bool is_windows = false; +constexpr bool is_macos = false; +constexpr bool is_linux = true; +#else +constexpr bool is_windows = false; +constexpr bool is_macos = false; +constexpr bool is_linux = false; +#endif + +} // namespace mcpp::platform diff --git a/src/pm/publisher.cppm b/src/pm/publisher.cppm index 42209bc..56b4217 100644 --- a/src/pm/publisher.cppm +++ b/src/pm/publisher.cppm @@ -4,6 +4,10 @@ module; #include // popen / pclose / fgets +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif export module mcpp.pm.publisher; diff --git a/src/process.cppm b/src/process.cppm new file mode 100644 index 0000000..43f8834 --- /dev/null +++ b/src/process.cppm @@ -0,0 +1,145 @@ +// mcpp.process — platform-aware process runner. +// +// Centralises all popen/system usage into a single module so callers do +// not need to scatter #if _WIN32 guards or duplicate the popen-read loop. +// +// Three entry points: +// run_capture — run a command, capture stdout (replaces the many inline +// popen loops in probe.cppm, xlings.cppm, pack.cppm, …) +// run_with_env — run a command with extra env vars (replaces scattered +// _putenv_s() calls on Windows) +// shell_quote — platform-aware shell quoting (delegates to mcpp.xlings::shq; +// kept here so new code imports mcpp.process, not mcpp.xlings) +// +// NOTE on Windows shell_quote: +// Do NOT use shell_quote() for the FIRST token in a popen/system command +// string on Windows — cmd.exe strips the leading double-quote pair and the +// binary name becomes unrecognised. Use the raw path string as the first +// token and shell_quote() only for arguments. See xlings.cppm::shq() for +// the full rationale. + +module; +#include +#include +#if defined(_WIN32) +#include // _putenv_s +#define popen _popen +#define pclose _pclose +#endif + +export module mcpp.process; + +import std; +import mcpp.xlings; // shq() — the authoritative shell-quoting implementation + +export namespace mcpp::process { + +// ─── Result type ───────────────────────────────────────────────────────────── + +struct RunResult { + int exit_code = 0; + std::string output; +}; + +// ─── run_capture ───────────────────────────────────────────────────────────── +// +// Run `command` via the platform shell (popen on both POSIX and Windows). +// Captures stdout. stderr is NOT captured unless the caller redirects it +// in the command string (e.g. "cmd 2>&1" on POSIX, "cmd 2>&1" on Windows). +// +// Returns RunResult with exit_code set and output containing all captured +// text. On popen failure, exit_code is -1 and output is empty. +RunResult run_capture(std::string_view command); + +// ─── run_with_env ──────────────────────────────────────────────────────────── +// +// Run `command` with extra environment variables (additive — existing vars +// not in `env` are preserved). +// +// On Windows: uses _putenv_s() to inject each var into the current process +// environment before spawning the child via popen(). _putenv_s() changes +// are inherited by child processes. IMPORTANT: this mutates the calling +// process's environment; callers should restore vars if needed. +// +// On POSIX: prefixes the command with "VAR=val " tokens so the vars are +// scoped to the child (the calling process's environment is unchanged). +// +// Returns the same RunResult as run_capture(). +RunResult run_with_env(std::string_view command, + const std::vector>& env); + +// ─── shell_quote ───────────────────────────────────────────────────────────── +// +// Quote `s` for safe embedding in a shell command string. +// POSIX: wraps in single quotes, escaping embedded single quotes. +// Windows: wraps in double quotes, escaping embedded double quotes. +// +// See the module-level NOTE about cmd.exe's first-token behaviour on Windows. +std::string shell_quote(std::string_view s); + +} // namespace mcpp::process + +// ─── Implementation ────────────────────────────────────────────────────────── + +namespace mcpp::process { + +RunResult run_capture(std::string_view command) { + std::string cmd_str(command); + RunResult result; + + std::FILE* fp = ::popen(cmd_str.c_str(), "r"); + if (!fp) { + result.exit_code = -1; + return result; + } + + std::array buf{}; + while (std::fgets(buf.data(), static_cast(buf.size()), fp) != nullptr) + result.output += buf.data(); + + int rc = ::pclose(fp); +#if defined(_WIN32) + // On Windows, pclose() returns the raw exit code from WaitForSingleObject / + // GetExitCodeProcess — it is already the process exit code, not a wait + // status word, so no WIFEXITED/WEXITSTATUS unwrapping needed. + result.exit_code = rc; +#else + // On POSIX, pclose() returns a wait-status word; extract the real exit code. + if (WIFEXITED(rc)) + result.exit_code = WEXITSTATUS(rc); + else + result.exit_code = rc; // signal / abnormal — surface raw value +#endif + return result; +} + +RunResult run_with_env(std::string_view command, + const std::vector>& env) +{ +#if defined(_WIN32) + // Inject vars into the current process environment. popen() inherits them. + for (auto& [k, v] : env) + _putenv_s(k.c_str(), v.c_str()); + return run_capture(command); +#else + // Build "KEY=val KEY2=val2 " prefix. + std::string prefixed; + for (auto& [k, v] : env) { + prefixed += k; + prefixed += '='; + prefixed += shell_quote(v); + prefixed += ' '; + } + prefixed += command; + return run_capture(prefixed); +#endif +} + +std::string shell_quote(std::string_view s) { + // Delegate to the canonical implementation in mcpp.xlings so the two + // stay in sync. If xlings.cppm's shq() is ever updated for edge-cases + // (e.g. NUL bytes, Unicode), this function inherits the fix automatically. + return mcpp::xlings::shq(s); +} + +} // namespace mcpp::process diff --git a/src/toolchain/clang.cppm b/src/toolchain/clang.cppm index 23cf617..9cea70b 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -6,6 +6,7 @@ import std; import mcpp.toolchain.model; import mcpp.toolchain.probe; import mcpp.xlings; +import mcpp.platform; export namespace mcpp::toolchain::clang { @@ -89,9 +90,10 @@ std::optional find_libcxx_std_module_source( const std::string& envPrefix) { auto manifest_r = mcpp::toolchain::run_capture(std::format( - "{}{} -print-library-module-manifest-path 2>/dev/null", + "{}{} -print-library-module-manifest-path {}", envPrefix, - mcpp::xlings::shq(cxx_binary.string()))); + mcpp::xlings::shq(cxx_binary.string()), + mcpp::platform::null_redirect)); if (manifest_r) { auto manifestPath = std::filesystem::path( mcpp::toolchain::trim_line(*manifest_r)); @@ -130,14 +132,44 @@ std::optional find_libcxx_std_module_source( } void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) { - tc.stdlibId = "libc++"; + // Clang targeting MSVC uses MSVC STL, not libc++. + bool msvTarget = tc.targetTriple.find("msvc") != std::string::npos; + tc.stdlibId = msvTarget ? "msvc-stl" : "libc++"; tc.stdlibVersion = tc.version.empty() ? "unknown" : tc.version; tc.linkRuntimeDirs = mcpp::toolchain::discover_link_runtime_dirs( tc.binaryPath, tc.targetTriple); + if (auto p = find_libcxx_std_module_source(tc.binaryPath, envPrefix)) { tc.stdModuleSource = *p; tc.hasImportStd = true; } + +#if defined(_WIN32) + // Fallback: if libc++ std.cppm not found, look for MSVC STL's std.ixx. + // This happens when Clang targets x86_64-pc-windows-msvc. + if (!tc.hasImportStd && msvTarget) { + // Search Visual Studio installations for std.ixx + // Typical path: C:\Program Files\Microsoft Visual Studio\2022\*\VC\Tools\MSVC\*\modules\std.ixx + std::error_code ec; + std::filesystem::path vsBase = "C:\\Program Files\\Microsoft Visual Studio\\2022"; + if (std::filesystem::exists(vsBase, ec)) { + for (auto& edition : std::filesystem::directory_iterator(vsBase, ec)) { + auto vcTools = edition.path() / "VC" / "Tools" / "MSVC"; + if (!std::filesystem::exists(vcTools, ec)) continue; + for (auto& ver : std::filesystem::directory_iterator(vcTools, ec)) { + auto stdIxx = ver.path() / "modules" / "std.ixx"; + if (std::filesystem::exists(stdIxx, ec)) { + tc.stdModuleSource = stdIxx; + tc.hasImportStd = true; + break; + } + } + if (tc.hasImportStd) break; + } + } + } +#endif + if (tc.hasImportStd) { if (auto p = find_libcxx_std_compat_source(tc.binaryPath, envPrefix)) { tc.stdCompatSource = *p; @@ -158,6 +190,37 @@ std::vector std_module_build_commands(const Toolchain& tc, const std::filesystem::path& bmiPath, std::string_view sysrootFlag) { auto relBmi = std::filesystem::relative(bmiPath, cacheDir).string(); +#if defined(_WIN32) + // Windows: use absolute paths, raw binary path as first token + // (cmd.exe strips leading quotes), shq for args with spaces. + // -x c++-module is needed for MSVC STL's .ixx files (Clang doesn't + // recognize the .ixx extension as a module source by default). + auto absBmi = (cacheDir / relBmi).string(); + auto ext = tc.stdModuleSource.extension().string(); + // MSVC STL's std.ixx needs -x c++-module (Clang doesn't recognize .ixx) + // and generates harmless warnings about #include in module purview and + // the reserved 'std' module name — suppress both. + std::string ixxFlags = (ext == ".ixx") + ? " -x c++-module -Wno-include-angled-in-module-purview -Wno-reserved-module-identifier" + : ""; + return { + std::format( + "{} -std=c++23{}{} " + "--precompile {} -o {}", + tc.binaryPath.string(), + ixxFlags, + sysrootFlag, + mcpp::xlings::shq(tc.stdModuleSource.string()), + mcpp::xlings::shq(absBmi)), + std::format( + "{} -std=c++23{} " + "{} -c -o {}", + tc.binaryPath.string(), + sysrootFlag, + mcpp::xlings::shq(absBmi), + mcpp::xlings::shq((cacheDir / "std.o").string())) + }; +#else return { std::format( "cd {} && {}{} -std=c++23 -Wno-reserved-module-identifier{} " @@ -177,16 +240,25 @@ std::vector std_module_build_commands(const Toolchain& tc, sysrootFlag, mcpp::xlings::shq(relBmi)) }; +#endif } std::filesystem::path archive_tool(const Toolchain& tc) { +#if defined(_WIN32) + auto llvmAr = tc.binaryPath.parent_path() / "llvm-ar.exe"; +#else auto llvmAr = tc.binaryPath.parent_path() / "llvm-ar"; +#endif if (std::filesystem::exists(llvmAr)) return llvmAr; return {}; } std::optional find_scan_deps(const Toolchain& tc) { +#if defined(_WIN32) + auto p = tc.binaryPath.parent_path() / "clang-scan-deps.exe"; +#else auto p = tc.binaryPath.parent_path() / "clang-scan-deps"; +#endif if (std::filesystem::exists(p)) return p; return std::nullopt; } diff --git a/src/toolchain/llvm.cppm b/src/toolchain/llvm.cppm index 8072c58..e56744b 100644 --- a/src/toolchain/llvm.cppm +++ b/src/toolchain/llvm.cppm @@ -3,6 +3,7 @@ export module mcpp.toolchain.llvm; import std; +import mcpp.platform; export namespace mcpp::toolchain::llvm { @@ -24,7 +25,7 @@ std::string package_name() { } std::vector frontend_candidates() { - return {"clang++"}; + return {std::format("clang++{}", mcpp::platform::exe_suffix)}; } std::vector list_aliases() { diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index 03c0fb9..8724b6f 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -1,14 +1,26 @@ // mcpp.toolchain.probe - common compiler probing helpers. +// +// NOTE: This file contains its own run_capture() helper that returns +// std::expected — a different signature from +// mcpp::process::run_capture() (which returns RunResult). Do NOT migrate +// existing callers here without care. For new process invocations that do +// not need DetectError propagation, prefer mcpp::process::run_capture from +// the mcpp.process module. module; #include // popen, pclose, fgets, FILE #include // getenv +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif export module mcpp.toolchain.probe; import std; import mcpp.toolchain.model; import mcpp.xlings; +import mcpp.platform; export namespace mcpp::toolchain { @@ -66,10 +78,15 @@ std::string join_colon_paths(const std::vector& dirs) { } std::string env_prefix_for_dirs(const std::vector& dirs) { +#if defined(_WIN32) + (void)dirs; + return ""; +#else if (dirs.empty()) return ""; auto joined = join_colon_paths(dirs); return std::format("env LD_LIBRARY_PATH={}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}} ", mcpp::xlings::shq(joined)); +#endif } } // namespace @@ -240,11 +257,18 @@ probe_compiler_binary(const std::filesystem::path& explicit_compiler) { cxx = "g++"; } - auto bin_path_r = run_capture(std::format("command -v '{}' 2>/dev/null", cxx)); +#if defined(_WIN32) + auto bin_path_r = run_capture(std::format("where {} {}", cxx, + mcpp::platform::null_redirect)); +#else + auto bin_path_r = run_capture(std::format("command -v '{}' {}", cxx, + mcpp::platform::null_redirect)); +#endif if (!bin_path_r) { return std::unexpected(DetectError{std::format("compiler '{}' not found in PATH", cxx)}); } - auto bin = trim_line(*bin_path_r); + // `where` on Windows may return multiple lines; take only the first. + auto bin = trim_line(first_line_of(*bin_path_r)); if (bin.empty()) { return std::unexpected(DetectError{std::format("compiler '{}' not found", cxx)}); } @@ -254,9 +278,10 @@ probe_compiler_binary(const std::filesystem::path& explicit_compiler) { std::expected probe_target_triple(const std::filesystem::path& compilerBin, const std::string& envPrefix) { - auto triple_r = run_capture(std::format("{}{} -dumpmachine 2>/dev/null", + auto triple_r = run_capture(std::format("{}{} -dumpmachine {}", envPrefix, - mcpp::xlings::shq(compilerBin.string()))); + mcpp::xlings::shq(compilerBin.string()), + mcpp::platform::null_redirect)); if (!triple_r) return std::unexpected(triple_r.error()); return trim_line(*triple_r); } @@ -264,16 +289,18 @@ probe_target_triple(const std::filesystem::path& compilerBin, std::filesystem::path probe_sysroot(const std::filesystem::path& compilerBin, const std::string& envPrefix) { - auto r = run_capture(std::format("{}{} -print-sysroot 2>/dev/null", + auto r = run_capture(std::format("{}{} -print-sysroot {}", envPrefix, - mcpp::xlings::shq(compilerBin.string()))); + mcpp::xlings::shq(compilerBin.string()), + mcpp::platform::null_redirect)); if (r) { auto s = trim_line(*r); if (!s.empty() && std::filesystem::exists(s)) return s; } #if defined(__APPLE__) // macOS fallback: use xcrun to discover the SDK path - auto xcrun_r = run_capture("xcrun --show-sdk-path 2>/dev/null"); + auto xcrun_r = run_capture(std::format("xcrun --show-sdk-path {}", + mcpp::platform::null_redirect)); if (xcrun_r) { auto sdk = trim_line(*xcrun_r); if (!sdk.empty() && std::filesystem::exists(sdk)) return sdk; diff --git a/src/toolchain/provider.cppm b/src/toolchain/provider.cppm new file mode 100644 index 0000000..985de48 --- /dev/null +++ b/src/toolchain/provider.cppm @@ -0,0 +1,111 @@ +// mcpp.toolchain.provider — provider capabilities dispatch. +// +// Documents the "provider concept": each toolchain variant (GCC/libstdc++, +// Clang/libc++, Clang/MSVC-STL) has a distinct set of capabilities. +// Previously these decisions were scattered as ad-hoc is_clang(tc) / +// is_gcc(tc) / targetTriple.find("msvc") checks. This module centralises +// them into a single query point. +// +// Usage: +// auto caps = mcpp::toolchain::capabilities_for(tc); +// if (caps.has_import_std) { ... } + +export module mcpp.toolchain.provider; + +import std; +import mcpp.toolchain.model; + +export namespace mcpp::toolchain { + +// ─── ProviderCapabilities ──────────────────────────────────────────────────── +// +// Describes what a particular toolchain instance can do. All fields have +// safe defaults (false / empty) so callers that only care about one flag +// do not need to guard the rest. + +struct ProviderCapabilities { + // True when the toolchain ships a prebuilt `std` module source + // (bits/std.cc for GCC, std.cppm / std.ixx for Clang variants) and + // Toolchain::stdModuleSource has been populated by enrich_toolchain(). + bool has_import_std = false; + + // True when clang-scan-deps (or an equivalent dep-scanner) is available + // alongside the compiler binary. Currently only Clang provides this. + bool has_scan_deps = false; + + // True when the compiler supports C++ named modules at all. + // All three supported compilers do; kept for future use when we add + // compilers that don't (e.g. old MSVC versions, ICC). + bool has_modules = true; + + // Canonical stdlib identifier: + // "libstdc++" — GCC, or Clang targeting a non-MSVC triple on Linux/macOS + // "libc++" — Clang with libc++ (xim:llvm toolchain, or Apple Clang) + // "msvc-stl" — Clang targeting x86_64-pc-windows-msvc + // "" — Unknown / not yet detected + std::string stdlib_id; + + // Archive tool name used for static libraries: + // "ar" — GCC / system binutils + // "llvm-ar" — Clang (llvm-ar is preferred; falls back to system ar) + // "lib.exe" — MSVC (future) + // "" — Unknown + std::string archive_format; +}; + +// Determine provider capabilities from an already-detected toolchain. +// All fields are derived from tc.compiler + tc.targetTriple + tc.hasImportStd +// so the result is deterministic and has no side-effects. +ProviderCapabilities capabilities_for(const Toolchain& tc); + +} // namespace mcpp::toolchain + +// ─── Implementation ────────────────────────────────────────────────────────── + +namespace mcpp::toolchain { + +ProviderCapabilities capabilities_for(const Toolchain& tc) { + ProviderCapabilities caps; + + caps.has_import_std = tc.hasImportStd; + caps.has_modules = true; // all supported compilers handle modules + + switch (tc.compiler) { + case CompilerId::GCC: { + caps.has_scan_deps = false; // GCC has no clang-scan-deps equivalent + caps.stdlib_id = "libstdc++"; + caps.archive_format = "ar"; + break; + } + + case CompilerId::Clang: { + // Clang targeting MSVC uses MSVC STL, not libc++. + // We detect this the same way clang.cppm's enrich_toolchain does: + // by checking the target triple for "msvc". + bool msvc_target = tc.targetTriple.find("msvc") != std::string::npos; + + caps.has_scan_deps = true; // clang-scan-deps lives beside clang++ + caps.stdlib_id = msvc_target ? "msvc-stl" : "libc++"; + caps.archive_format = "llvm-ar"; + break; + } + + 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; + caps.stdlib_id = "msvc-stl"; + caps.archive_format = "lib.exe"; + break; + } + + case CompilerId::Unknown: + default: + // Leave all caps at their safe defaults (false / ""). + break; + } + + return caps; +} + +} // namespace mcpp::toolchain diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 042f4d0..8701fed 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -1,6 +1,10 @@ module; #include // popen, pclose, fgets, FILE #include // getenv +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif // mcpp.toolchain.stdmod — pre-build the `import std` BMI and cache it. // diff --git a/src/xlings.cppm b/src/xlings.cppm index 4cfe950..39ebfb8 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -10,6 +10,11 @@ module; #include #include +#if defined(_WIN32) +#include // _putenv_s +#define popen _popen +#define pclose _pclose +#endif export module mcpp.xlings; @@ -315,12 +320,25 @@ std::expected run_capture(const std::string& cmd) { std::string shq(std::string_view s) { std::string out; out.reserve(s.size() + 2); +#if defined(_WIN32) + // Windows: wrap in double quotes, escape inner " as \". + // IMPORTANT: avoid placing a shq'd token as the FIRST token in a + // popen/system command — cmd.exe strips a leading " pair. For + // binary paths, use the raw string; shq is safe for arguments. + out.push_back('"'); + for (char c : s) { + if (c == '"') out += "\\\""; + else out.push_back(c); + } + out.push_back('"'); +#else out.push_back('\''); for (char c : s) { if (c == '\'') out += "'\\''"; else out.push_back(c); } out.push_back('\''); +#endif return out; } @@ -409,6 +427,21 @@ std::filesystem::path sandbox_init_marker(const Env& env) { std::string build_command_prefix(const Env& env) { auto xvmBin = paths::sandbox_bin(env).string(); +#if defined(_WIN32) + // Windows: set environment variables via the process environment + // (cmd.exe `set` in compound &&-chains is unreliable) then invoke + // xlings directly. _putenv_s is inherited by popen/system child. + _putenv_s("XLINGS_HOME", env.home.string().c_str()); + _putenv_s("XLINGS_PROJECT_DIR", + env.projectDir.empty() ? "" : env.projectDir.string().c_str()); + // Prepend sandbox bin to PATH + { + std::string newPath = xvmBin + ";" + (std::getenv("PATH") ? std::getenv("PATH") : ""); + _putenv_s("PATH", newPath.c_str()); + } + // Return raw path — no quoting to avoid cmd.exe double-quote parsing issues + return env.binary.string(); +#else if (env.projectDir.empty()) { // Global mode: unset XLINGS_PROJECT_DIR (existing behavior). return std::format( @@ -427,13 +460,19 @@ std::string build_command_prefix(const Env& env) { shq(env.home.string()), shq(env.projectDir.string()), shq(env.binary.string())); +#endif } std::string build_interface_command(const Env& env, std::string_view capability, std::string_view argsJson) { +#if defined(_WIN32) + return std::format("{} interface {} --args {} 2>nul", + build_command_prefix(env), capability, shq(argsJson)); +#else return std::format("{} interface {} --args {} 2>/dev/null", build_command_prefix(env), capability, shq(argsJson)); +#endif } // ─── JSON extraction helpers ──────────────────────────────────────── @@ -624,12 +663,22 @@ int install_with_progress(const Env& env, std::string_view target, auto argsJson = std::format( R"({{"targets":["{}"],"yes":true}})", target); +#if defined(_WIN32) + _putenv_s("XLINGS_HOME", env.home.string().c_str()); + _putenv_s("XLINGS_PROJECT_DIR", ""); + // Use raw path (no quoting) to avoid cmd.exe double-quote parsing issues. + // Wrap only the JSON arg in single-escaped quotes for the C runtime. + auto cmd = std::format("{} interface install_packages --args {} 2>nul", + env.binary.string(), + shq(argsJson)); +#else auto cmd = std::format( "cd {} && env -u XLINGS_PROJECT_DIR XLINGS_HOME={} {} interface install_packages --args {} 2>/dev/null", shq(env.home.string()), shq(env.home.string()), shq(env.binary.string()), shq(argsJson)); +#endif std::FILE* fp = ::popen(cmd.c_str(), "r"); if (!fp) return -1; @@ -732,7 +781,11 @@ int config_set_mirror(const Env& env, std::string_view mirror, bool quiet) { "{} config --mirror {} {}", build_command_prefix(env), shq(mirror), +#if defined(_WIN32) + quiet ? ">nul 2>&1" : ""); +#else quiet ? ">/dev/null 2>&1" : ""); +#endif return std::system(cmd.c_str()); } @@ -740,13 +793,23 @@ void ensure_init(const Env& env, bool quiet) { auto marker = paths::sandbox_init_marker(env); if (std::filesystem::exists(marker)) return; + // Ensure the home directory exists before cd'ing into it. + std::error_code ec; + std::filesystem::create_directories(env.home, ec); + if (!quiet) print_status("Initialize", "mcpp sandbox layout (one-time)"); +#if defined(_WIN32) + _putenv_s("XLINGS_HOME", env.home.string().c_str()); + _putenv_s("XLINGS_PROJECT_DIR", ""); + auto cmd = env.binary.string() + " self init"; +#else auto cmd = std::format( "cd {} && env -u XLINGS_PROJECT_DIR XLINGS_HOME={} {} self init >/dev/null 2>&1", shq(env.home.string()), shq(env.home.string()), shq(env.binary.string())); +#endif int rc = std::system(cmd.c_str()); if (rc != 0 && !quiet) { std::println(stderr, @@ -780,7 +843,11 @@ void ensure_ninja(const Env& env, bool quiet, if (std::filesystem::exists(root)) { std::error_code ec; for (auto& v : std::filesystem::directory_iterator(root, ec)) { +#if defined(_WIN32) + if (std::filesystem::exists(v.path() / "ninja.exe")) return; +#else if (std::filesystem::exists(v.path() / "ninja")) return; +#endif } } if (!quiet) diff --git a/tests/e2e/01_help_and_version.sh b/tests/e2e/01_help_and_version.sh index 66d9aa7..2a17056 100755 --- a/tests/e2e/01_help_and_version.sh +++ b/tests/e2e/01_help_and_version.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Verify --help and --version set -e diff --git a/tests/e2e/02_new_build_run.sh b/tests/e2e/02_new_build_run.sh index f15626d..4eedb21 100755 --- a/tests/e2e/02_new_build_run.sh +++ b/tests/e2e/02_new_build_run.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: fresh-sandbox # Single-module hello world: mcpp new → build → run set -e @@ -16,11 +17,25 @@ cd hello grep -q "import std" src/main.cpp || { echo "main.cpp missing 'import std'"; exit 1; } grep -q "std::println" src/main.cpp || { echo "main.cpp missing 'std::println'"; exit 1; } -# Build +# Build (capture output, show on failure) +set +e "$MCPP" build > build.log 2>&1 +build_rc=$? +set -e +if [[ $build_rc -ne 0 ]]; then + cat build.log + echo "build failed (rc=$build_rc)" + exit 1 +fi [[ -d target ]] || { cat build.log; echo "no target/ dir"; exit 1; } -binary="$(find target -name hello -type f | head -1)" -[[ -n "$binary" ]] || { echo "binary not produced"; exit 1; } +# On Windows (MINGW/MSYS) the binary has a .exe suffix +OS="$(uname -s)" +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + binary="$(find target -name hello.exe -type f | head -1)" +else + binary="$(find target -name hello -type f | head -1)" +fi +[[ -n "$binary" ]] || { echo "binary not produced"; find target -type f 2>/dev/null | head -10; exit 1; } [[ -x "$binary" ]] || { echo "binary not executable"; exit 1; } # Run via mcpp diff --git a/tests/e2e/03_multi_module.sh b/tests/e2e/03_multi_module.sh index dd21023..42b8d95 100755 --- a/tests/e2e/03_multi_module.sh +++ b/tests/e2e/03_multi_module.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf gcc # Multi-module: package with .cppm + main.cpp importing it set -e diff --git a/tests/e2e/04_incremental.sh b/tests/e2e/04_incremental.sh index c9214e9..199a5ea 100755 --- a/tests/e2e/04_incremental.sh +++ b/tests/e2e/04_incremental.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Incremental: a no-op rebuild does no work; touching main.cpp recompiles only it set -e diff --git a/tests/e2e/05_errors.sh b/tests/e2e/05_errors.sh index 9ffd970..b5ca5a2 100755 --- a/tests/e2e/05_errors.sh +++ b/tests/e2e/05_errors.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Error paths: missing manifest, missing version, conditional import, header unit, naming violation set -e diff --git a/tests/e2e/06_emit_xpkg.sh b/tests/e2e/06_emit_xpkg.sh index 9b3821d..e72c12f 100755 --- a/tests/e2e/06_emit_xpkg.sh +++ b/tests/e2e/06_emit_xpkg.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # emit xpkg: produce valid Lua entry from mcpp.toml set -e diff --git a/tests/e2e/07_static_library.sh b/tests/e2e/07_static_library.sh index 551d1eb..98af0de 100755 --- a/tests/e2e/07_static_library.sh +++ b/tests/e2e/07_static_library.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf # Static library: kind = "lib" → libNAME.a via `ar rcs` set -e diff --git a/tests/e2e/08_shared_library.sh b/tests/e2e/08_shared_library.sh index 37b9460..2b91755 100755 --- a/tests/e2e/08_shared_library.sh +++ b/tests/e2e/08_shared_library.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf # Shared library: kind = "shared" → libNAME.so via -shared -fPIC set -e diff --git a/tests/e2e/09_path_dependency.sh b/tests/e2e/09_path_dependency.sh index 059fe5f..3e67ae1 100755 --- a/tests/e2e/09_path_dependency.sh +++ b/tests/e2e/09_path_dependency.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf gcc # Path-based dependency: package B imports modules from package A via # [dependencies.A] path = "../A" # Verifies the multi-package scanner + linker pipeline. diff --git a/tests/e2e/10_env_command.sh b/tests/e2e/10_env_command.sh index ba36ac1..5a0eb0e 100755 --- a/tests/e2e/10_env_command.sh +++ b/tests/e2e/10_env_command.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # `mcpp env` initializes $MCPP_HOME and prints expected layout. set -e diff --git a/tests/e2e/11_index_list.sh b/tests/e2e/11_index_list.sh index 1e54176..33a6395 100755 --- a/tests/e2e/11_index_list.sh +++ b/tests/e2e/11_index_list.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # `mcpp index list` shows configured registries (after init). set -e diff --git a/tests/e2e/12_add_command.sh b/tests/e2e/12_add_command.sh index 579ceae..943cfc4 100755 --- a/tests/e2e/12_add_command.sh +++ b/tests/e2e/12_add_command.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # `mcpp add` modifies mcpp.toml [dependencies], including the namespaced form # `mcpp add :@` which lands under [dependencies.] without # any TOML key quoting. diff --git a/tests/e2e/13_toolchain_pin.sh b/tests/e2e/13_toolchain_pin.sh index baeab04..87af7ea 100755 --- a/tests/e2e/13_toolchain_pin.sh +++ b/tests/e2e/13_toolchain_pin.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Project pins [toolchain] → mcpp resolves to xpkg absolute path. # # This test verifies the resolve_xpkg_path branch is exercised when a diff --git a/tests/e2e/14_toolchain_fallback.sh b/tests/e2e/14_toolchain_fallback.sh index 51bd326..a6d2a0e 100755 --- a/tests/e2e/14_toolchain_fallback.sh +++ b/tests/e2e/14_toolchain_fallback.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # 14_toolchain_fallback.sh — M5.5: when no toolchain is configured at all # (no project [toolchain], no global default), `mcpp build` hard-errors with # a helpful message instead of falling back to system PATH. diff --git a/tests/e2e/15_test_passing.sh b/tests/e2e/15_test_passing.sh index e2c429f..72c8a1d 100755 --- a/tests/e2e/15_test_passing.sh +++ b/tests/e2e/15_test_passing.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # `mcpp test` discovers tests/**/*.cpp and runs each as a separate binary. # All passing → exit 0 + summary "ok. N passed". set -e diff --git a/tests/e2e/16_test_failing.sh b/tests/e2e/16_test_failing.sh index bd2ead8..9be4c7d 100755 --- a/tests/e2e/16_test_failing.sh +++ b/tests/e2e/16_test_failing.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: scan-deps # 1 ok + 1 fail → mcpp test exits 1, summary lists failures. set -e diff --git a/tests/e2e/17_test_no_tests.sh b/tests/e2e/17_test_no_tests.sh index 3084b3d..e339044 100755 --- a/tests/e2e/17_test_no_tests.sh +++ b/tests/e2e/17_test_no_tests.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Project with no tests/ → `mcpp test` says "no tests found" and exits 0. set -e diff --git a/tests/e2e/18_devdeps_isolation.sh b/tests/e2e/18_devdeps_isolation.sh index b5faa06..550a23d 100755 --- a/tests/e2e/18_devdeps_isolation.sh +++ b/tests/e2e/18_devdeps_isolation.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Verify dev-deps are NOT pulled by `mcpp build` but ARE pulled by `mcpp test`. # We don't actually fetch a real dev-dep (would need network); we just verify # that the dev-deps section in mcpp.toml does not appear in the build path's diff --git a/tests/e2e/19_bmi_cache_reuse.sh b/tests/e2e/19_bmi_cache_reuse.sh index 3827efd..2f49572 100755 --- a/tests/e2e/19_bmi_cache_reuse.sh +++ b/tests/e2e/19_bmi_cache_reuse.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # 19_bmi_cache_reuse.sh — verify M3.2 BMI persistent cache wiring. # # 1. Path deps don't populate the cache (correctness invariant from docs/26). @@ -70,7 +71,7 @@ EOF # bmi/ should exist (env init creates it) but no deps/ entry for path deps. [[ -d "$MCPP_HOME/bmi" ]] || { echo "missing $MCPP_HOME/bmi"; exit 1; } -if compgen -G "$MCPP_HOME/bmi/*/deps/*/mylibA*" > /dev/null; then +if find "$MCPP_HOME/bmi" -path "*/deps/*/mylibA*" 2>/dev/null | grep -q .; then echo "FAIL: path dep mylibA was populated into BMI cache (must be skipped)" find "$MCPP_HOME/bmi" -maxdepth 5 exit 1 diff --git a/tests/e2e/20_p1689_scanner.sh b/tests/e2e/20_p1689_scanner.sh index a5da97a..47d79a1 100755 --- a/tests/e2e/20_p1689_scanner.sh +++ b/tests/e2e/20_p1689_scanner.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf gcc # 20_p1689_scanner.sh — verify M3.3.a opt-in P1689 scanner end-to-end. # # Builds the same multi-module project twice — once under the default diff --git a/tests/e2e/21_ninja_dyndep.sh b/tests/e2e/21_ninja_dyndep.sh index 1c63584..3a290f5 100755 --- a/tests/e2e/21_ninja_dyndep.sh +++ b/tests/e2e/21_ninja_dyndep.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf gcc # 21_ninja_dyndep.sh — verify M3.3.b: ninja dyndep build mode produces # byte-identical runtime output to the static-deps mode. # diff --git a/tests/e2e/22_doctor_cache_publish.sh b/tests/e2e/22_doctor_cache_publish.sh index 868e7f2..6ec661d 100755 --- a/tests/e2e/22_doctor_cache_publish.sh +++ b/tests/e2e/22_doctor_cache_publish.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf gcc # 22_doctor_cache_publish.sh — M4 #1 #2 #4 smoke tests: # - mcpp doctor runs and reports # - mcpp cache list / clean diff --git a/tests/e2e/23_remove_update.sh b/tests/e2e/23_remove_update.sh index 0a62002..8fd0bca 100755 --- a/tests/e2e/23_remove_update.sh +++ b/tests/e2e/23_remove_update.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # 23_remove_update.sh — M4 #3: mcpp remove / mcpp update. set -e diff --git a/tests/e2e/24_git_dependency.sh b/tests/e2e/24_git_dependency.sh index 4b0f7eb..93e623f 100755 --- a/tests/e2e/24_git_dependency.sh +++ b/tests/e2e/24_git_dependency.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: fresh-sandbox # 24_git_dependency.sh — M4 #5: git-based dep clones to ~/.mcpp/git// # and is treated as a path dep. set -e diff --git a/tests/e2e/25_convention_mode.sh b/tests/e2e/25_convention_mode.sh index 1f9d724..8e45057 100755 --- a/tests/e2e/25_convention_mode.sh +++ b/tests/e2e/25_convention_mode.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # 25_convention_mode.sh — verify M5.0 convention-first schema: # - 3-line mcpp.toml builds + runs successfully # - Inferred banner shown for sources / target diff --git a/tests/e2e/26_c_language_support.sh b/tests/e2e/26_c_language_support.sh index 6b5b833..eb20a02 100755 --- a/tests/e2e/26_c_language_support.sh +++ b/tests/e2e/26_c_language_support.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: gcc # C-language compile rule: .c files routed to `c_object` with cc / cflags, # distinct from the .cppm/.cpp `cxx_object` rule. Verifies that a mixed # C + modular-C++23 project links and runs, and that build.ninja contains diff --git a/tests/e2e/26_toolchain_management.sh b/tests/e2e/26_toolchain_management.sh index 6dfe3cf..c6468b8 100755 --- a/tests/e2e/26_toolchain_management.sh +++ b/tests/e2e/26_toolchain_management.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: gcc # 26_toolchain_management.sh — verify M5.5 toolchain CLI + isolation: # - mcpp toolchain install / list / default / remove # - hard error when no toolchain configured diff --git a/tests/e2e/27_namespace_dependencies.sh b/tests/e2e/27_namespace_dependencies.sh index 9a1e1ff..20fb170 100755 --- a/tests/e2e/27_namespace_dependencies.sh +++ b/tests/e2e/27_namespace_dependencies.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: fresh-sandbox # Namespaced dependencies: `[dependencies.] name = { path = "..." }` # is parsed correctly and the dep is actually picked up by the build. # Also verifies that the legacy `"." = "..."` quoted form still diff --git a/tests/e2e/27_self_contained_home.sh b/tests/e2e/27_self_contained_home.sh index ff61d33..bd9c5e1 100755 --- a/tests/e2e/27_self_contained_home.sh +++ b/tests/e2e/27_self_contained_home.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf # 27_self_contained_home.sh — verifies mcpp's self-contained home behaviour. # # Without MCPP_HOME set, mcpp resolves its home from the binary's location: diff --git a/tests/e2e/28_target_static.sh b/tests/e2e/28_target_static.sh index dca96d5..8111c37 100755 --- a/tests/e2e/28_target_static.sh +++ b/tests/e2e/28_target_static.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: musl elf # 28_target_static.sh — `mcpp build --target ` produces a binary # matching the requested target, and `--target *-linux-musl` yields a # fully-static ELF (no PT_INTERP, no RUNPATH). diff --git a/tests/e2e/29_toolchain_partial_versions.sh b/tests/e2e/29_toolchain_partial_versions.sh index e9fce6a..5a8b5d3 100755 --- a/tests/e2e/29_toolchain_partial_versions.sh +++ b/tests/e2e/29_toolchain_partial_versions.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: gcc # 29_toolchain_partial_versions.sh — `mcpp toolchain default` accepts partial # versions in either positional or @-separated form, AND `mcpp build` # auto-installs the default toolchain on a first run with no toolchain diff --git a/tests/e2e/30_dev_binary_home.sh b/tests/e2e/30_dev_binary_home.sh index 98adc8f..4390f51 100644 --- a/tests/e2e/30_dev_binary_home.sh +++ b/tests/e2e/30_dev_binary_home.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: gcc # Dev binaries are built under build/.../release/mcpp, not /bin/mcpp. # With MCPP_HOME unset they should use the conventional ~/.mcpp sandbox; # only release-style /bin/mcpp should self-locate to . diff --git a/tests/e2e/30_pack_modes.sh b/tests/e2e/30_pack_modes.sh index 30870da..583a6e1 100755 --- a/tests/e2e/30_pack_modes.sh +++ b/tests/e2e/30_pack_modes.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: pack patchelf elf # 30_pack_modes.sh — `mcpp pack` smoke tests for all three modes. # # Verifies the contract of each mode by extracting the produced tarball diff --git a/tests/e2e/31_transitive_deps.sh b/tests/e2e/31_transitive_deps.sh index 98d1d7d..f11a7e0 100755 --- a/tests/e2e/31_transitive_deps.sh +++ b/tests/e2e/31_transitive_deps.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: musl # 31_transitive_deps.sh — transitive dependency walker: # * a path-dep that itself declares a path-dep is fully resolved # (consumer doesn't need to list the grandchild explicitly) diff --git a/tests/e2e/32_semver_merge.sh b/tests/e2e/32_semver_merge.sh index 377be5d..a7b7413 100755 --- a/tests/e2e/32_semver_merge.sh +++ b/tests/e2e/32_semver_merge.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: fresh-sandbox # 32_semver_merge.sh — SemVer merge in the transitive walker: # * Two consumers of the same package with overlapping constraints # (one exact, one range) merge to a single satisfying version @@ -18,13 +19,17 @@ mkdir -p "$MCPP_HOME/registry/data" for idx_name in mcpplibs mcpp-index; do if [[ -d "$HOME/.mcpp/registry/data/$idx_name" ]]; then ln -sf "$HOME/.mcpp/registry/data/$idx_name" \ - "$MCPP_HOME/registry/data/$idx_name" + "$MCPP_HOME/registry/data/$idx_name" 2>/dev/null \ + || cp -r "$HOME/.mcpp/registry/data/$idx_name" \ + "$MCPP_HOME/registry/data/$idx_name" fi done # Pre-cached xpkg downloads so the test doesn't re-fetch the world. if [[ -d "$HOME/.mcpp/registry/data/xpkgs" ]]; then [[ -e "$MCPP_HOME/registry/data/xpkgs" ]] \ || ln -sf "$HOME/.mcpp/registry/data/xpkgs" \ + "$MCPP_HOME/registry/data/xpkgs" 2>/dev/null \ + || cp -r "$HOME/.mcpp/registry/data/xpkgs" \ "$MCPP_HOME/registry/data/xpkgs" fi diff --git a/tests/e2e/33_multi_version_mangling.sh b/tests/e2e/33_multi_version_mangling.sh index 5f20fae..e6c00da 100755 --- a/tests/e2e/33_multi_version_mangling.sh +++ b/tests/e2e/33_multi_version_mangling.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: elf gcc # 33_multi_version_mangling.sh — Level 1 of dep resolution: when two # transitive consumers want incompatible (non-overlapping) versions of # the same package, the secondary copy is rewritten to use a mangled diff --git a/tests/e2e/35_workspace.sh b/tests/e2e/35_workspace.sh index 330a8c7..8bac59e 100755 --- a/tests/e2e/35_workspace.sh +++ b/tests/e2e/35_workspace.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: set -euo pipefail # Test: workspace with two library members and one binary member. @@ -94,7 +95,13 @@ echo "workspace build: ok" # ── Verify the binary runs correctly ──────────────────── # target/ is created in the member dir (apps/hello/target/), not workspace root. -BIN=$(find apps/hello/target -type f -name hello | head -1) +# On Windows (MINGW/MSYS) the binary has a .exe suffix +OS="$(uname -s)" +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + BIN=$(find apps/hello/target -type f -name hello.exe | head -1) +else + BIN=$(find apps/hello/target -type f -name hello | head -1) +fi test -n "$BIN" || { echo "FAIL: hello binary not found"; exit 1; } OUT=$("$BIN" 2>&1) echo "output: $OUT" diff --git a/tests/e2e/36_llvm_toolchain.sh b/tests/e2e/36_llvm_toolchain.sh index 6e92240..bba2d13 100755 --- a/tests/e2e/36_llvm_toolchain.sh +++ b/tests/e2e/36_llvm_toolchain.sh @@ -1,9 +1,18 @@ #!/usr/bin/env bash +# requires: # 36_llvm_toolchain.sh — build a non-module C/C++ package with xlings LLVM. set -e -LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" -if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then +OS="$(uname -s)" +# On Windows the clang++ binary has a .exe suffix +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + LLVM_ROOT="${USERPROFILE}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" + CLANGPP_BIN="$LLVM_ROOT/bin/clang++.exe" +else + LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" + CLANGPP_BIN="$LLVM_ROOT/bin/clang++" +fi +if [[ ! -x "$CLANGPP_BIN" ]]; then echo "SKIP: xlings llvm@20.1.7 is not installed" exit 0 fi @@ -16,7 +25,13 @@ source "$(dirname "$0")/_inherit_toolchain.sh" mkdir -p "$TMP/proj/src" cd "$TMP/proj" -cat > mcpp.toml <<'EOF' +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + TC_KEY="windows" +else + TC_KEY="linux" +fi + +cat > mcpp.toml < src/main.cpp <<'EOF' @@ -63,7 +78,11 @@ grep -q 'Finished' "$TMP/build.log" || { exit 1 } -binary=$(find target -type f -path '*/bin/hello_llvm' | head -1) +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + binary=$(find target -type f -path '*/bin/hello_llvm.exe' | head -1) +else + binary=$(find target -type f -path '*/bin/hello_llvm' | head -1) +fi [[ -n "$binary" && -x "$binary" ]] || { find target -maxdepth 5 -type f echo "FAIL: hello_llvm binary missing" diff --git a/tests/e2e/37_llvm_import_std.sh b/tests/e2e/37_llvm_import_std.sh index 6747b6a..51f5724 100755 --- a/tests/e2e/37_llvm_import_std.sh +++ b/tests/e2e/37_llvm_import_std.sh @@ -1,7 +1,17 @@ #!/usr/bin/env bash +# requires: import-std-libcxx # 37_llvm_import_std.sh — build an import-std package with xlings LLVM/libc++. set -e +OS="$(uname -s)" +# libc++ std.cppm is only available on Linux/macOS — on Windows there is no +# libc++ module distribution. Exit gracefully; the import-std-libcxx capability +# check in run_all.sh already gates this, but guard here too for direct runs. +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + echo "SKIP: libc++ std.cppm not available on Windows" + exit 0 +fi + LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@20.1.7 is not installed" diff --git a/tests/e2e/38_llvm_modules.sh b/tests/e2e/38_llvm_modules.sh index 31016a4..2ce8849 100755 --- a/tests/e2e/38_llvm_modules.sh +++ b/tests/e2e/38_llvm_modules.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: import-std-libcxx # 38_llvm_modules.sh — multi-module project with LLVM/Clang. # # Tests: module interface (.cppm) with `export module`, cross-module import, @@ -6,6 +7,15 @@ # -fmodule-output / -fprebuilt-module-path flags. set -e +OS="$(uname -s)" +# libc++ std.cppm is only available on Linux/macOS — on Windows there is no +# libc++ module distribution. Exit gracefully; the import-std-libcxx capability +# check in run_all.sh already gates this, but guard here too for direct runs. +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + echo "SKIP: libc++ std.cppm not available on Windows" + exit 0 +fi + LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@20.1.7 is not installed" diff --git a/tests/e2e/38_self_config_mirror.sh b/tests/e2e/38_self_config_mirror.sh index 2ec9433..333f82d 100755 --- a/tests/e2e/38_self_config_mirror.sh +++ b/tests/e2e/38_self_config_mirror.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # 38_self_config_mirror.sh — configure xlings mirror through mcpp self config. set -e diff --git a/tests/e2e/39_llvm_incremental.sh b/tests/e2e/39_llvm_incremental.sh index d54a4e4..9842661 100755 --- a/tests/e2e/39_llvm_incremental.sh +++ b/tests/e2e/39_llvm_incremental.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: import-std-libcxx scan-deps # 39_llvm_incremental.sh — Clang per-file incremental rebuild via clang-scan-deps dyndep. set -e diff --git a/tests/e2e/39_xlings_index_migration.sh b/tests/e2e/39_xlings_index_migration.sh index 895381a..41962ad 100755 --- a/tests/e2e/39_xlings_index_migration.sh +++ b/tests/e2e/39_xlings_index_migration.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # 39_xlings_index_migration.sh - legacy mcpp-index cache migrates to mcpplibs. set -e diff --git a/tests/e2e/40_llvm_bmi_cache.sh b/tests/e2e/40_llvm_bmi_cache.sh index 5aadb5f..16ce2f3 100755 --- a/tests/e2e/40_llvm_bmi_cache.sh +++ b/tests/e2e/40_llvm_bmi_cache.sh @@ -1,7 +1,17 @@ #!/usr/bin/env bash +# requires: import-std-libcxx # 40_llvm_bmi_cache.sh — Clang BMI cache reuse for dependency packages. set -e +OS="$(uname -s)" +# libc++ std.cppm is only available on Linux/macOS — on Windows there is no +# libc++ module distribution. Exit gracefully; the import-std-libcxx capability +# check in run_all.sh already gates this, but guard here too for direct runs. +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + echo "SKIP: libc++ std.cppm not available on Windows" + exit 0 +fi + LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@20.1.7 is not installed" @@ -23,7 +33,8 @@ USER_MCPP="${HOME}/.mcpp" if [[ -d "$USER_MCPP/registry/data/mcpplibs" ]]; then mkdir -p "$MCPP_HOME/registry/data" [[ -e "$MCPP_HOME/registry/data/mcpplibs" ]] \ - || ln -sf "$USER_MCPP/registry/data/mcpplibs" "$MCPP_HOME/registry/data/mcpplibs" + || ln -sf "$USER_MCPP/registry/data/mcpplibs" "$MCPP_HOME/registry/data/mcpplibs" 2>/dev/null \ + || cp -r "$USER_MCPP/registry/data/mcpplibs" "$MCPP_HOME/registry/data/mcpplibs" fi mkdir -p "$TMP/proj/src" diff --git a/tests/e2e/41_llvm_std_compat.sh b/tests/e2e/41_llvm_std_compat.sh index 9747c38..3f91d98 100755 --- a/tests/e2e/41_llvm_std_compat.sh +++ b/tests/e2e/41_llvm_std_compat.sh @@ -1,7 +1,17 @@ #!/usr/bin/env bash +# requires: import-std-libcxx # 41_llvm_std_compat.sh — build a project that uses import std.compat with Clang. set -e +OS="$(uname -s)" +# libc++ std.compat.cppm is only available on Linux/macOS — on Windows there +# is no libc++ module distribution. Exit gracefully; the import-std-libcxx +# capability check in run_all.sh already gates this, but guard here too. +if [[ "$OS" == MINGW* || "$OS" == MSYS* || "$OS" == CYGWIN* ]]; then + echo "SKIP: libc++ std.compat.cppm not available on Windows" + exit 0 +fi + LLVM_ROOT="${HOME}/.mcpp/registry/data/xpkgs/xim-x-llvm/20.1.7" if [[ ! -x "$LLVM_ROOT/bin/clang++" ]]; then echo "SKIP: xlings llvm@20.1.7 is not installed" diff --git a/tests/e2e/42_custom_local_index.sh b/tests/e2e/42_custom_local_index.sh index 8387afe..f8eeba5 100755 --- a/tests/e2e/42_custom_local_index.sh +++ b/tests/e2e/42_custom_local_index.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Custom [indices] parsing: a local path index is parsed from mcpp.toml # and visible in `mcpp index list`. Verifies the TOML parsing path for # short form, long form, and local path indices without requiring any diff --git a/tests/e2e/43_indices_lockfile.sh b/tests/e2e/43_indices_lockfile.sh index fe0b661..70dcb11 100755 --- a/tests/e2e/43_indices_lockfile.sh +++ b/tests/e2e/43_indices_lockfile.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # Lockfile v2 + index pin/unpin: verify lockfile format, v1 migration, # and `mcpp index pin` / `mcpp index unpin` CLI commands. # No network access required — uses local path indices and synthetic lockfiles. diff --git a/tests/e2e/44_indices_e2e_integration.sh b/tests/e2e/44_indices_e2e_integration.sh index 1755961..f4b7e1e 100755 --- a/tests/e2e/44_indices_e2e_integration.sh +++ b/tests/e2e/44_indices_e2e_integration.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: # E2E integration test for [indices] feature gaps: # 1. Local path index discovery via `mcpp index list` # 2. Workspace inherits [indices] from root diff --git a/tests/e2e/_inherit_toolchain.sh b/tests/e2e/_inherit_toolchain.sh index 2968963..e870433 100644 --- a/tests/e2e/_inherit_toolchain.sh +++ b/tests/e2e/_inherit_toolchain.sh @@ -13,26 +13,34 @@ if [[ -z "${MCPP_HOME:-}" ]]; then fi mkdir -p "$MCPP_HOME" +# On Windows, HOME may differ from USERPROFILE; try both USER_MCPP="${HOME}/.mcpp" +if [[ ! -d "$USER_MCPP" && -n "${USERPROFILE:-}" ]]; then + USER_MCPP="$USERPROFILE/.mcpp" +fi if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then mkdir -p "$MCPP_HOME/registry/data" [[ -e "$MCPP_HOME/registry/data/xpkgs" ]] \ - || ln -sf "$USER_MCPP/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs" + || ln -sf "$USER_MCPP/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs" 2>/dev/null \ + || cp -r "$USER_MCPP/registry/data/xpkgs" "$MCPP_HOME/registry/data/xpkgs" fi if [[ -d "$USER_MCPP/registry/data/xim-pkgindex" ]]; then mkdir -p "$MCPP_HOME/registry/data" [[ -e "$MCPP_HOME/registry/data/xim-pkgindex" ]] \ - || ln -sf "$USER_MCPP/registry/data/xim-pkgindex" "$MCPP_HOME/registry/data/xim-pkgindex" + || ln -sf "$USER_MCPP/registry/data/xim-pkgindex" "$MCPP_HOME/registry/data/xim-pkgindex" 2>/dev/null \ + || cp -r "$USER_MCPP/registry/data/xim-pkgindex" "$MCPP_HOME/registry/data/xim-pkgindex" fi if [[ -d "$USER_MCPP/registry/data/xim-index-repos" ]]; then mkdir -p "$MCPP_HOME/registry/data" [[ -e "$MCPP_HOME/registry/data/xim-index-repos" ]] \ - || ln -sf "$USER_MCPP/registry/data/xim-index-repos" "$MCPP_HOME/registry/data/xim-index-repos" + || ln -sf "$USER_MCPP/registry/data/xim-index-repos" "$MCPP_HOME/registry/data/xim-index-repos" 2>/dev/null \ + || cp -r "$USER_MCPP/registry/data/xim-index-repos" "$MCPP_HOME/registry/data/xim-index-repos" fi if [[ "${MCPP_INHERIT_SUBOS:-1}" != "0" && -d "$USER_MCPP/registry/subos" ]]; then mkdir -p "$MCPP_HOME/registry" [[ -e "$MCPP_HOME/registry/subos" ]] \ - || ln -sf "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos" + || ln -sf "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos" 2>/dev/null \ + || cp -r "$USER_MCPP/registry/subos" "$MCPP_HOME/registry/subos" fi if [[ "${MCPP_INHERIT_CONFIG:-1}" != "0" && -f "$USER_MCPP/config.toml" ]]; then cp -f "$USER_MCPP/config.toml" "$MCPP_HOME/config.toml" 2>/dev/null || true @@ -40,5 +48,6 @@ fi if [[ -d "$USER_MCPP/bin" ]]; then mkdir -p "$MCPP_HOME" [[ -e "$MCPP_HOME/bin" ]] \ - || ln -sf "$USER_MCPP/bin" "$MCPP_HOME/bin" + || ln -sf "$USER_MCPP/bin" "$MCPP_HOME/bin" 2>/dev/null \ + || cp -r "$USER_MCPP/bin" "$MCPP_HOME/bin" fi diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index ada5c7e..29cc389 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -31,43 +31,96 @@ if [[ -z "${MCPP_HOME:-}" ]]; then fi echo "MCPP_HOME: $MCPP_HOME" -# Platform detection: some tests are Linux-only (ELF patchelf, musl-static, -# GCC-specific BMI layout, etc.) +# --------------------------------------------------------------------------- +# Capability detection +# --------------------------------------------------------------------------- +# Build the set of capabilities available on this machine/platform. +# Each test declares its needs via a `# requires: cap1 cap2 ...` comment +# on line 2. Tests with no requirements run everywhere. + +CAPS=() OS="$(uname -s)" -MACOS_SKIP=( - # GCC-specific BMI assertions (gcm.cache/*.gcm) - 03_multi_module.sh - # Static library test checks ELF ar output format - 07_static_library.sh - # Shared library test hardcodes .so / ELF shared object - 08_shared_library.sh - # Path dependency checks .gcm BMI format (GCC-specific) - 09_path_dependency.sh - # Pack modes use patchelf (ELF-only) - 30_pack_modes.sh - # Toolchain management tests assume GCC availability - 26_toolchain_management.sh - 29_toolchain_partial_versions.sh - # P1689 scanner test hardcodes GCC ddi format - 20_p1689_scanner.sh - # Ninja dyndep test hardcodes GCC module format - 21_ninja_dyndep.sh - # Doctor/cache/publish uses GCC fingerprint - 22_doctor_cache_publish.sh - # Self-contained home test assumes Linux sandbox layout - 27_self_contained_home.sh - # Multi-version mangling test uses GCC module format - 33_multi_version_mangling.sh -) - -should_skip() { - local name="$1" - if [[ "$OS" == "Darwin" ]]; then - for skip in "${MACOS_SKIP[@]}"; do - [[ "$name" == "$skip" ]] && return 0 - done - fi - return 1 + +case "$OS" in + Linux) + CAPS+=(elf unix-shell fresh-sandbox) + command -v g++ &>/dev/null && CAPS+=(gcc) + command -v patchelf &>/dev/null && CAPS+=(patchelf) + # musl-gcc: check both system PATH and xlings-managed locations + if command -v x86_64-linux-musl-g++ &>/dev/null \ + || [[ -x "$HOME/.xlings/data/xpkgs/xim-x-musl-gcc/15.1.0/bin/x86_64-linux-musl-g++" ]] \ + || [[ -x "${MCPP_HOME}/registry/data/xpkgs/xim-x-musl-gcc/15.1.0/bin/x86_64-linux-musl-g++" ]]; then + CAPS+=(musl) + fi + # pack capability: ELF + patchelf both required + if [[ " ${CAPS[*]} " == *" patchelf "* ]]; then + CAPS+=(pack) + fi + ;; + Darwin) + CAPS+=(unix-shell fresh-sandbox) + # macOS g++ is Apple Clang, not real GCC — don't add gcc capability. + # Tests requiring gcc need actual GNU GCC (modules, gcm.cache, etc.) + ;; + MINGW* | MSYS* | CYGWIN*) + # Git Bash / MSYS2 on Windows: symlinks need admin or Developer Mode + if [[ "${MSYS:-}" == *winsymlinks* ]] || cmd.exe /c "mklink /?" &>/dev/null 2>&1; then + CAPS+=(symlink) + fi + # NOTE: Windows runners may have g++.exe (MinGW/Strawberry) in PATH + # but it's not a proper mcpp-compatible GCC. Don't add gcc capability. + # fresh-sandbox: not yet reliable on Windows — xlings LLVM auto-install + # into temp MCPP_HOME dirs has path/copy issues. Enable once resolved. + ;; +esac + +# symlink: ln -sf works properly on all non-Windows platforms +case "$OS" in + Linux|Darwin) CAPS+=(symlink) ;; +esac + +# scan-deps: clang-scan-deps available (needed for P1689 / Clang dyndep flows) +if command -v clang-scan-deps &>/dev/null \ + || ls "${MCPP_HOME}/registry/data/xpkgs/xim-x-llvm"/*/bin/clang-scan-deps 2>/dev/null | head -1 | grep -q . \ + || ls "${MCPP_HOME}/registry/data/xpkgs/xim-x-llvm"/*/bin/clang-scan-deps.exe 2>/dev/null | head -1 | grep -q .; then + CAPS+=(scan-deps) +fi + +# import-std-libcxx: libc++ std.cppm available (LLVM with libc++ modules) +if ls "${MCPP_HOME}/registry/data/xpkgs/xim-x-llvm"/*/share/libc++/v1/std.cppm 2>/dev/null | head -1 | grep -q .; then + CAPS+=(import-std-libcxx) +fi + +echo "Detected capabilities: ${CAPS[*]:-}" + +# --------------------------------------------------------------------------- +# Helper: check if a test's requirements are satisfied +# --------------------------------------------------------------------------- +# Returns 0 (true) if the test should be skipped, prints reason. +# Returns 1 (false) if all requirements are met. + +check_requires() { + local test_file="$1" + # Read the # requires: line (must be line 2 of the script) + local req_line + req_line="$(sed -n '2p' "$test_file")" + + # If there's no requires comment at all, run the test + [[ "$req_line" =~ ^#\ requires: ]] || return 1 + + local caps_needed="${req_line#\# requires:}" + caps_needed="${caps_needed# }" # strip leading space + + # Empty requirements → runs everywhere + [[ -z "$caps_needed" ]] && return 1 + + for cap in $caps_needed; do + if [[ " ${CAPS[*]} " != *" $cap "* ]]; then + echo "$cap" # return the missing capability name + return 0 # should skip + fi + done + return 1 # all satisfied → don't skip } PASS=0 @@ -78,8 +131,9 @@ FAILED_TESTS=() for test in "$HERE"/[0-9]*.sh; do name="$(basename "$test")" echo - if should_skip "$name"; then - echo "SKIP: $name (not applicable on $OS)" + missing_cap="$(check_requires "$test")" + if [[ -n "$missing_cap" ]]; then + echo "SKIP: $name (missing capability: $missing_cap)" ((SKIP++)) continue fi diff --git a/tests/unit/test_bmi_cache.cpp b/tests/unit/test_bmi_cache.cpp index cd39828..714bc25 100644 --- a/tests/unit/test_bmi_cache.cpp +++ b/tests/unit/test_bmi_cache.cpp @@ -1,7 +1,9 @@ #include #include +#if !defined(_WIN32) #include #include +#endif import std; import mcpp.bmi_cache; @@ -43,8 +45,8 @@ void writeFile(const std::filesystem::path& p, std::string_view body) { TEST(BmiCache, KeyDirLayoutMatchesDocs26) { auto k = makeKey("/home/u/.mcpp"); - EXPECT_EQ(k.dir().string(), - "/home/u/.mcpp/bmi/deadbeef0123abcd/deps/mcpplibs/mcpplibs.cmdline@0.0.1"); + auto expected = std::filesystem::path("/home/u/.mcpp/bmi/deadbeef0123abcd/deps/mcpplibs/mcpplibs.cmdline@0.0.1"); + EXPECT_EQ(k.dir(), expected); EXPECT_EQ(k.manifestFile().filename().string(), "manifest.txt"); EXPECT_EQ(k.bmiDir().filename().string(), "gcm.cache"); EXPECT_EQ(k.objDir().filename().string(), "obj"); @@ -200,8 +202,10 @@ TEST(BmiCache, PopulateFailsIfBuildOutputMissing) { EXPECT_NE(pop.error().find("expected build output missing"), std::string::npos); } +#if !defined(_WIN32) // M4 #9: when an external holder takes the .lock, populate_from must skip // (returns success but does NOT clobber the directory). +// Uses flock() which is POSIX-only. TEST(BmiCache, PopulateSkipsWhenLockHeld) { Tmp t; auto home = t.path / "home"; @@ -233,3 +237,4 @@ TEST(BmiCache, PopulateSkipsWhenLockHeld) { ASSERT_TRUE(pop2) << pop2.error(); EXPECT_TRUE(std::filesystem::exists(k.manifestFile())); } +#endif // !defined(_WIN32) diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 2b5452a..4c41243 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -398,7 +398,7 @@ kind = "lib" EXPECT_TRUE(m->lib.path.empty()); EXPECT_TRUE(mcpp::manifest::has_lib_target(*m)); auto root = mcpp::manifest::resolve_lib_root_path(*m); - EXPECT_EQ(root.string(), "src/tinyhttps.cppm"); + EXPECT_EQ(root, std::filesystem::path("src/tinyhttps.cppm")); } TEST(Manifest, LibRootBareNameNoNamespace) { @@ -412,7 +412,7 @@ kind = "lib" auto m = mcpp::manifest::parse_string(src); ASSERT_TRUE(m.has_value()) << m.error().format(); auto root = mcpp::manifest::resolve_lib_root_path(*m); - EXPECT_EQ(root.string(), "src/gtest.cppm"); + EXPECT_EQ(root, std::filesystem::path("src/gtest.cppm")); } TEST(Manifest, LibRootExplicitOverride) { diff --git a/tests/unit/test_modgraph.cpp b/tests/unit/test_modgraph.cpp index b5bc654..c1cc366 100644 --- a/tests/unit/test_modgraph.cpp +++ b/tests/unit/test_modgraph.cpp @@ -27,11 +27,13 @@ void write(const std::filesystem::path& p, std::string_view content) { TEST(Scanner, ProvidesAndRequires) { auto dir = make_tempdir("mcpp-scanner"); - write(dir / "src" / "foo.cppm", R"(export module foo; -import std; -import bar; -export int answer(); -)"); + // NOTE: avoid raw string literal for module source — clang-scan-deps + // on Windows may false-positive on `import bar;` inside R"(...)". + write(dir / "src" / "foo.cppm", + "export module foo;\n" + "import std;\n" + "import bar;\n" + "export int answer();\n"); auto u = scan_file(dir / "src" / "foo.cppm", "pkg"); ASSERT_TRUE(u.has_value()) << u.error().format(); @@ -48,9 +50,9 @@ TEST(Scanner, PartitionImportFromPrimaryInterface) { // Primary module interface: `export module foo;` → logicalName = "foo". // `import :tls;` resolves to "foo:tls". auto dir = make_tempdir("mcpp-scanner"); - write(dir / "src" / "foo.cppm", R"(export module foo; -import :tls; -)"); + write(dir / "src" / "foo.cppm", + "export module foo;\n" + "import :tls;\n"); auto u = scan_file(dir / "src" / "foo.cppm", "pkg"); ASSERT_TRUE(u.has_value()) << u.error().format(); ASSERT_EQ(u->requires_.size(), 1u); @@ -63,10 +65,10 @@ TEST(Scanner, PartitionImportFromAnotherPartition) { // `import :tls;` must resolve to "foo:tls" (the sibling partition), // NOT "foo:http:tls" (which is what a naive prepend produces). auto dir = make_tempdir("mcpp-scanner"); - write(dir / "src" / "http.cppm", R"(export module foo:http; -import :tls; -import :socket; -)"); + write(dir / "src" / "http.cppm", + "export module foo:http;\n" + "import :tls;\n" + "import :socket;\n"); auto u = scan_file(dir / "src" / "http.cppm", "pkg"); ASSERT_TRUE(u.has_value()) << u.error().format(); ASSERT_TRUE(u->provides.has_value()); @@ -81,9 +83,9 @@ TEST(Scanner, PartitionImportWithDottedModuleName) { // Dotted module names (xpkg-style, e.g. `mcpplibs.tinyhttps:http`) // — only the colon-prefixed partition suffix is what we strip. auto dir = make_tempdir("mcpp-scanner"); - write(dir / "src" / "http.cppm", R"(export module mcpplibs.tinyhttps:http; -import :tls; -)"); + write(dir / "src" / "http.cppm", + "export module mcpplibs.tinyhttps:http;\n" + "import :tls;\n"); auto u = scan_file(dir / "src" / "http.cppm", "pkg"); ASSERT_TRUE(u.has_value()) << u.error().format(); ASSERT_EQ(u->requires_.size(), 1u); @@ -93,11 +95,12 @@ import :tls; TEST(Scanner, RejectsConditionalImport) { auto dir = make_tempdir("mcpp-scanner"); - write(dir / "main.cpp", R"(import std; -#ifdef WANT_X -import x; -#endif -int main(){})"); + write(dir / "main.cpp", + "import std;\n" + "#ifdef WANT_X\n" + "import x;\n" + "#endif\n" + "int main(){}"); auto r = scan_file(dir / "main.cpp", "pkg"); EXPECT_FALSE(r.has_value()); EXPECT_NE(r.error().message.find("conditional"), std::string::npos); @@ -106,9 +109,10 @@ int main(){})"); TEST(Scanner, RejectsHeaderUnit) { auto dir = make_tempdir("mcpp-scanner"); - write(dir / "main.cpp", R"(import std; -import "x.h"; -int main(){})"); + write(dir / "main.cpp", + "import std;\n" + "import \"x.h\";\n" + "int main(){}"); auto r = scan_file(dir / "main.cpp", "pkg"); EXPECT_FALSE(r.has_value()); EXPECT_NE(r.error().message.find("header units"), std::string::npos); diff --git a/tests/unit/test_toolchain_detect.cpp b/tests/unit/test_toolchain_detect.cpp index a0dcfa5..1722281 100644 --- a/tests/unit/test_toolchain_detect.cpp +++ b/tests/unit/test_toolchain_detect.cpp @@ -54,6 +54,8 @@ esac } // namespace +#if !defined(_WIN32) +// Uses a fake shell script as a compiler — POSIX only. TEST(ToolchainDetect, ClangVersionOutputIsNotMisclassifiedByGccPaths) { auto clang = make_fake_clang(); TempDirGuard cleanup{clang.parent_path()}; @@ -66,6 +68,7 @@ TEST(ToolchainDetect, ClangVersionOutputIsNotMisclassifiedByGccPaths) { EXPECT_EQ(tc->stdlibId, "libc++"); EXPECT_FALSE(tc->hasImportStd); } +#endif // !defined(_WIN32) // ─── normalize_driver_output: path-free semantic identity ───────────── // @@ -126,6 +129,7 @@ TEST(NormalizeDriverOutput, EmptyInputProducesEmpty) { EXPECT_EQ(normalize_driver_output("\n\n\n"), ""); } +#if !defined(_WIN32) // ─── detect() populates driverIdent ───────────────────────────────── TEST(ToolchainDetect, PopulatesDriverIdentFromVersionOutput) { auto clang = make_fake_clang(); @@ -138,3 +142,4 @@ TEST(ToolchainDetect, PopulatesDriverIdentFromVersionOutput) { EXPECT_NE(tc->driverIdent.find("clang version 20.1.7"), std::string::npos) << "driverIdent should contain the --version header: " << tc->driverIdent; } +#endif // !defined(_WIN32) diff --git a/tests/unit/test_toolchain_registry.cpp b/tests/unit/test_toolchain_registry.cpp index 39e1e9a..fc41f89 100644 --- a/tests/unit/test_toolchain_registry.cpp +++ b/tests/unit/test_toolchain_registry.cpp @@ -48,7 +48,11 @@ TEST(ToolchainRegistry, MapsLlvmAndClangAliasesToLlvmPackage) { EXPECT_EQ(llvmPkg.display_spec(), "llvm@20.1.7"); EXPECT_EQ(clangPkg.display_spec(), "clang@20.1.7"); ASSERT_FALSE(clangPkg.frontendCandidates.empty()); +#if defined(_WIN32) + EXPECT_EQ(clangPkg.frontendCandidates.front(), "clang++.exe"); +#else EXPECT_EQ(clangPkg.frontendCandidates.front(), "clang++"); +#endif } TEST(ToolchainRegistry, ResolvesPartialMuslVersionForDisplayAndPackage) { diff --git a/tests/unit/test_xpkg_emit.cpp b/tests/unit/test_xpkg_emit.cpp index b630262..bf5b09f 100644 --- a/tests/unit/test_xpkg_emit.cpp +++ b/tests/unit/test_xpkg_emit.cpp @@ -96,6 +96,8 @@ TEST(XpkgEmit, ReleaseTarballUrl) { EXPECT_EQ(release_tarball_url("git@github.com:foo/bar.git", "bar", "0.1.0"), ""); } +#if !defined(_WIN32) +// sha256_of_file uses sha256sum which is not available on Windows TEST(XpkgEmit, Sha256OfFile) { using namespace mcpp::publish; @@ -116,6 +118,7 @@ TEST(XpkgEmit, Sha256OfFile) { // Non-existent file → empty string EXPECT_EQ(sha256_of_file("/no/such/path/zzz"), ""); } +#endif // !defined(_WIN32) TEST(XpkgEmit, LongBracketSequenceInValueIsHarmless) { // We emit `"..."` strings, not `[[...]]`, so a literal `]=]` in