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-platform-maturity-plan.md b/.agents/docs/2026-05-19-windows-platform-maturity-plan.md new file mode 100644 index 0000000..f00fae5 --- /dev/null +++ b/.agents/docs/2026-05-19-windows-platform-maturity-plan.md @@ -0,0 +1,240 @@ +# Windows 平台成熟度提升方案 + +> 基于 PR #52 code review 反馈,针对 Windows 支持从"可自举"到"生产就绪"的优化路径。 + +## 当前状态 + +| 能力 | Linux | macOS | Windows | 差距 | +|------|-------|-------|---------|------| +| self-host | ✅ | ✅ | ✅ | — | +| `mcpp test` (unit) | ✅ | ✅ | ❌ | 缺 clang-scan-deps | +| E2E 覆盖 | 46/46 | 33/46 | 22/49 | 27 项 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 + +**目标:** Windows 二进制进入正式 release 发布流程。 + +#### 1. release.yml 加 build-windows job + +参照 `build-macos` 结构,在 `release.yml` 中增加 `build-windows` job: + +```yaml +build-windows: + name: build (Windows / x64) + runs-on: windows-latest + needs: build-release + # xlings install mcpp → mcpp build → package zip → upload +``` + +产出 `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 抽象 + +**目标:** 减少散落的 `#if defined(_WIN32)` / `#if defined(__APPLE__)`。 + +新建 `src/platform.cppm`,集中平台差异: + +```cpp +export module mcpp.platform; +import std; + +export namespace mcpp::platform { + +constexpr std::string_view exe_suffix = +#if defined(_WIN32) + ".exe"; +#else + ""; +#endif + +constexpr std::string_view static_lib_ext = +#if defined(_WIN32) + ".lib"; +#else + ".a"; +#endif + +constexpr std::string_view shared_lib_ext = +#if defined(_WIN32) + ".dll"; +#elif defined(__APPLE__) + ".dylib"; +#else + ".so"; +#endif + +constexpr std::string_view null_redirect = +#if defined(_WIN32) + "2>nul"; +#else + "2>/dev/null"; +#endif + +constexpr std::string_view lib_prefix = +#if defined(_WIN32) + ""; +#else + "lib"; +#endif + +std::string shell_quote(std::string_view s); // 取代散落的 shq + +} // namespace mcpp::platform +``` + +**受益文件:** `plan.cppm`、`flags.cppm`、`ninja_backend.cppm`、`probe.cppm`、`clang.cppm`、`config.cppm` + +### P2: ToolchainProvider 重构 + +**目标:** 把工具链行为从散落的 `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 + +### P3: 跨平台 Process Runner + +**目标:** 消除 shell 字符串拼接,统一子进程执行。 + +当前问题: +- `popen` + cmd.exe 字符串拼接(路径空格、引号转义脆弱) +- `shq()` 在 Windows 上有 cmd.exe 首 token 引号剥离问题 +- `_putenv_s` 污染全局进程环境 + +建议新建 `src/process.cppm`: + +```cpp +struct ProcessOptions { + std::vector argv; + std::map env; // 进程级环境变量 + std::filesystem::path cwd; + bool capture_stdout = true; + bool capture_stderr = false; +}; + +struct ProcessResult { + int exit_code; + std::string stdout_output; + std::string stderr_output; +}; + +ProcessResult run(const ProcessOptions& opts); +``` + +POSIX: `fork/exec` + `pipe` +Windows: `CreateProcessW` + `STARTUPINFOW` + +**受益范围:** `probe.cppm`、`xlings.cppm`、`stdmod.cppm`、`ninja_backend.cppm`、`config.cppm` + +### 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 能力标签化 + +**目标:** 从"平台 skip 列表"升级为"能力标签"。 + +在每个 E2E 脚本头部声明需求: + +```bash +# requires: elf — 需要 ELF 工具链 +# requires: gcc — 需要 GCC +# requires: symlink — 需要 ln -sf +# requires: scan-deps — 需要 clang-scan-deps +# requires: import-std — 需要 import std (std.cppm/std.ixx) +# requires: pack — 需要 mcpp pack +``` + +`run_all.sh` 读取标签,根据当前平台的能力集决定 skip,不再维护平台 skip 列表。 + +## 实施顺序 + +``` +P0 release + E2E 修复 ← 立即可做,产出最大 + ↓ +P1 PlatformTraits ← 减少 #if 散落,降低后续维护成本 + ↓ +P2 ToolchainProvider ← 为 MSVC 支持打基础 + ↓ +P3 Process Runner ← 消除 shell 拼接风险 + ↓ +P4 mcpp pack Windows ← 产品化打包 + ↓ +P5 E2E 标签化 ← 测试治理 +``` + +## 预期里程碑 + +| 阶段 | 目标 | Windows E2E 通过率 | +|------|------|-------------------| +| 当前 | self-host + 基础 E2E | 22/49 (45%) | +| P0 完成 | release + 高价值 E2E | ~30/49 (61%) | +| P1+P2 完成 | 平台抽象 + provider | ~35/49 (71%) | +| P3+P4 完成 | process runner + pack | ~40/49 (82%) | +| P5 完成 | 能力标签 | 动态评估 | diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml new file mode 100644 index 0000000..83720f6 --- /dev/null +++ b/.github/workflows/ci-windows.yml @@ -0,0 +1,161 @@ +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" + + # NOTE: `mcpp test` requires clang-scan-deps for module dependency + # scanning. The xlings LLVM package does not yet ship it on Windows. + # Enable once available. + + - 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..4dd0ab5 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -149,20 +149,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..4ca491f 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)); @@ -2156,9 +2161,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 +2519,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 +2612,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 +3160,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 +3168,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..5db0203 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -3,12 +3,17 @@ 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 +71,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 +250,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 +271,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 +282,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..2e01efd 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 {}", + 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 {}", + 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; @@ -740,13 +789,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 +839,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..d0b9b61 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 @@ -17,9 +18,15 @@ grep -q "import std" src/main.cpp || { echo "main.cpp missing 'import grep -q "std::println" src/main.cpp || { echo "main.cpp missing 'std::println'"; exit 1; } # Build -"$MCPP" build > build.log 2>&1 +"$MCPP" build > build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; } [[ -d target ]] || { cat build.log; echo "no target/ dir"; exit 1; } -binary="$(find target -name hello -type f | head -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"; exit 1; } [[ -x "$binary" ]] || { echo "binary not executable"; exit 1; } 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..565165b 100755 --- a/tests/e2e/10_env_command.sh +++ b/tests/e2e/10_env_command.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# requires: symlink # `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..e8409dc 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: unix-shell # 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..a09d851 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -31,43 +31,94 @@ 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. + ;; +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 +129,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