Skip to content

Commit e2eea3d

Browse files
committed
feat: Windows self-host — POSIX compat fixes + LLVM toolchain config
- mcpp.toml: add windows = "llvm@20.1.7" so mcpp uses LLVM on Windows - probe.cppm: guard LD_LIBRARY_PATH env prefix, command -v, and /dev/null redirects behind #if !defined(_WIN32) - xlings.cppm: Windows-specific build_command_prefix using cmd.exe set/cd semantics instead of env -u / POSIX PATH prepend; fix shq() to use double quotes on Windows; fix 2>/dev/null → 2>/dev/null - config.cppm: use "where xlings.exe" instead of "command -v xlings" - clang.cppm: fix /dev/null redirect in module manifest probe - CI: re-enable self-host step (mcpp builds itself using LLVM)
1 parent f0760e1 commit e2eea3d

6 files changed

Lines changed: 129 additions & 16 deletions

File tree

.github/workflows/ci-windows.yml

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ name: ci-windows
33
# Windows validation CI for mcpp.
44
# Step 1: Verify xlings LLVM toolchain capabilities on Windows.
55
# Step 2: xmake bootstrap to produce first mcpp.exe.
6-
# Step 3: Package into a distributable zip (same layout as Linux/macOS).
7-
# NOTE: self-host (mcpp building itself) not yet possible — needs MSVC toolchain support.
6+
# Step 3: Self-host — use the bootstrapped mcpp.exe to build itself (via LLVM).
7+
# Step 4: Package into a distributable zip (same layout as Linux/macOS).
88

99
on:
1010
push:
@@ -233,12 +233,38 @@ jobs:
233233
exit 1
234234
}
235235
236-
# NOTE: full self-host (`mcpp build` building itself) is not yet
237-
# possible on Windows — mcpp's build system defaults to gcc which
238-
# is unavailable here. Once mcpp.toml gains a `windows = "msvc"`
239-
# toolchain override (and detect.cppm handles MSVC output), the
240-
# self-host step can be re-enabled. For now we package the xmake-
241-
# bootstrapped binary, which is functionally identical.
236+
- name: Self-host — mcpp builds itself
237+
shell: bash
238+
run: |
239+
echo "=== Self-host: using bootstrapped mcpp.exe to build mcpp ==="
240+
# Save bootstrap binary before cleaning xmake artifacts
241+
mkdir -p /tmp/mcpp-bootstrap
242+
cp "$MCPP_BOOTSTRAP" /tmp/mcpp-bootstrap/mcpp.exe
243+
MCPP_EXE="/tmp/mcpp-bootstrap/mcpp.exe"
244+
245+
# Clean xmake artifacts so mcpp starts fresh
246+
rm -rf build xmake.lua .xmake
247+
248+
echo "Bootstrap binary: $MCPP_EXE"
249+
"$MCPP_EXE" --version
250+
251+
# mcpp build uses its own build system; mcpp.toml now has
252+
# windows = "llvm@20.1.7" so it will use the xlings LLVM.
253+
export MCPP_VENDORED_XLINGS="$USERPROFILE/.xlings/subos/default/bin/xlings.exe"
254+
"$MCPP_EXE" build
255+
256+
# Find the self-hosted binary
257+
SELF_MCPP=$(find target -name "mcpp.exe" -path "*/bin/*" | head -1)
258+
test -n "$SELF_MCPP" || {
259+
echo "FAIL: self-host build did not produce mcpp.exe"
260+
find target -name "*.exe" 2>/dev/null
261+
exit 1
262+
}
263+
SELF_MCPP=$(cd "$(dirname "$SELF_MCPP")" && pwd)/$(basename "$SELF_MCPP")
264+
echo "Self-hosted binary: $SELF_MCPP"
265+
"$SELF_MCPP" --version
266+
267+
echo "MCPP_SELF=$SELF_MCPP" >> "$GITHUB_ENV"
242268
243269
- name: Package Windows release zip
244270
id: package
@@ -255,8 +281,8 @@ jobs:
255281
mkdir -p "$STAGING/$WRAPPER/bin"
256282
mkdir -p "$STAGING/$WRAPPER/registry/bin"
257283
258-
# Binary
259-
cp "$MCPP_BOOTSTRAP" "$STAGING/$WRAPPER/bin/mcpp.exe"
284+
# Binary (use self-hosted build if available, fall back to bootstrap)
285+
cp "${MCPP_SELF:-$MCPP_BOOTSTRAP}" "$STAGING/$WRAPPER/bin/mcpp.exe"
260286
261287
# Launcher batch script (equivalent to the shell wrapper on Linux/macOS)
262288
printf '@echo off\r\n"%%~dp0bin\\mcpp.exe" %%*\r\n' > "$STAGING/$WRAPPER/mcpp.bat"

mcpp.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ include_dirs = ["src/libs/json"]
1414
[toolchain]
1515
default = "gcc@16.1.0"
1616
macos = "llvm@20.1.7"
17+
windows = "llvm@20.1.7"
1718

1819
# Per-target overrides: `mcpp build --target x86_64-linux-musl` (or the
1920
# four-segment form `x86_64-unknown-linux-musl`) picks musl-gcc 15.1 + full

src/config.cppm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,11 @@ acquire_xlings_binary(const std::filesystem::path& destBin, bool quiet)
355355
}
356356

357357
// 2. Copy from system (`which xlings`)
358+
#if defined(_WIN32)
359+
auto sys = run_capture("where xlings.exe 2>nul");
360+
#else
358361
auto sys = run_capture("command -v xlings 2>/dev/null");
362+
#endif
359363
if (sys) {
360364
std::string p = *sys;
361365
while (!p.empty() && (p.back() == '\n' || p.back() == '\r')) p.pop_back();
@@ -532,7 +536,11 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
532536
auto xbin = acquire_xlings_binary(cfg.xlingsBinary, quiet);
533537
if (!xbin) return std::unexpected(ConfigError{xbin.error()});
534538
} else if (cfg.xlingsBinaryMode == "system") {
539+
#if defined(_WIN32)
540+
auto sys = run_capture("where xlings.exe 2>nul");
541+
#else
535542
auto sys = run_capture("command -v xlings 2>/dev/null");
543+
#endif
536544
if (!sys || sys->empty())
537545
return std::unexpected(ConfigError{"system xlings not found in PATH"});
538546
std::string p = *sys;

src/toolchain/clang.cppm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,16 @@ std::optional<std::filesystem::path> find_libcxx_std_module_source(
8888
const std::filesystem::path& cxx_binary,
8989
const std::string& envPrefix)
9090
{
91+
#if defined(_WIN32)
92+
constexpr auto kDevNull = "2>nul";
93+
#else
94+
constexpr auto kDevNull = "2>/dev/null";
95+
#endif
9196
auto manifest_r = mcpp::toolchain::run_capture(std::format(
92-
"{}{} -print-library-module-manifest-path 2>/dev/null",
97+
"{}{} -print-library-module-manifest-path {}",
9398
envPrefix,
94-
mcpp::xlings::shq(cxx_binary.string())));
99+
mcpp::xlings::shq(cxx_binary.string()),
100+
kDevNull));
95101
if (manifest_r) {
96102
auto manifestPath = std::filesystem::path(
97103
mcpp::toolchain::trim_line(*manifest_r));

src/toolchain/probe.cppm

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,15 @@ std::string join_colon_paths(const std::vector<std::filesystem::path>& dirs) {
7070
}
7171

7272
std::string env_prefix_for_dirs(const std::vector<std::filesystem::path>& dirs) {
73+
#if defined(_WIN32)
74+
(void)dirs;
75+
return "";
76+
#else
7377
if (dirs.empty()) return "";
7478
auto joined = join_colon_paths(dirs);
7579
return std::format("env LD_LIBRARY_PATH={}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}} ",
7680
mcpp::xlings::shq(joined));
81+
#endif
7782
}
7883

7984
} // namespace
@@ -244,7 +249,11 @@ probe_compiler_binary(const std::filesystem::path& explicit_compiler) {
244249
cxx = "g++";
245250
}
246251

252+
#if defined(_WIN32)
253+
auto bin_path_r = run_capture(std::format("where {} 2>nul", cxx));
254+
#else
247255
auto bin_path_r = run_capture(std::format("command -v '{}' 2>/dev/null", cxx));
256+
#endif
248257
if (!bin_path_r) {
249258
return std::unexpected(DetectError{std::format("compiler '{}' not found in PATH", cxx)});
250259
}
@@ -258,19 +267,31 @@ probe_compiler_binary(const std::filesystem::path& explicit_compiler) {
258267
std::expected<std::string, DetectError>
259268
probe_target_triple(const std::filesystem::path& compilerBin,
260269
const std::string& envPrefix) {
261-
auto triple_r = run_capture(std::format("{}{} -dumpmachine 2>/dev/null",
270+
#if defined(_WIN32)
271+
constexpr auto kNullRedirect = "2>nul";
272+
#else
273+
constexpr auto kNullRedirect = "2>/dev/null";
274+
#endif
275+
auto triple_r = run_capture(std::format("{}{} -dumpmachine {}",
262276
envPrefix,
263-
mcpp::xlings::shq(compilerBin.string())));
277+
mcpp::xlings::shq(compilerBin.string()),
278+
kNullRedirect));
264279
if (!triple_r) return std::unexpected(triple_r.error());
265280
return trim_line(*triple_r);
266281
}
267282

268283
std::filesystem::path
269284
probe_sysroot(const std::filesystem::path& compilerBin,
270285
const std::string& envPrefix) {
271-
auto r = run_capture(std::format("{}{} -print-sysroot 2>/dev/null",
286+
#if defined(_WIN32)
287+
constexpr auto kNullRedir = "2>nul";
288+
#else
289+
constexpr auto kNullRedir = "2>/dev/null";
290+
#endif
291+
auto r = run_capture(std::format("{}{} -print-sysroot {}",
272292
envPrefix,
273-
mcpp::xlings::shq(compilerBin.string())));
293+
mcpp::xlings::shq(compilerBin.string()),
294+
kNullRedir));
274295
if (r) {
275296
auto s = trim_line(*r);
276297
if (!s.empty() && std::filesystem::exists(s)) return s;

src/xlings.cppm

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,22 @@ std::expected<std::string, std::string> run_capture(const std::string& cmd) {
319319
std::string shq(std::string_view s) {
320320
std::string out;
321321
out.reserve(s.size() + 2);
322+
#if defined(_WIN32)
323+
// cmd.exe uses double quotes; escape inner double quotes with backslash
324+
out.push_back('"');
325+
for (char c : s) {
326+
if (c == '"') out += "\\\"";
327+
else out.push_back(c);
328+
}
329+
out.push_back('"');
330+
#else
322331
out.push_back('\'');
323332
for (char c : s) {
324333
if (c == '\'') out += "'\\''";
325334
else out.push_back(c);
326335
}
327336
out.push_back('\'');
337+
#endif
328338
return out;
329339
}
330340

@@ -413,6 +423,24 @@ std::filesystem::path sandbox_init_marker(const Env& env) {
413423

414424
std::string build_command_prefix(const Env& env) {
415425
auto xvmBin = paths::sandbox_bin(env).string();
426+
#if defined(_WIN32)
427+
// Windows: use cmd.exe set + call semantics.
428+
if (env.projectDir.empty()) {
429+
return std::format(
430+
"cd /d {} && set \"PATH={};%PATH%\" && set \"XLINGS_HOME={}\" && set \"XLINGS_PROJECT_DIR=\" && {}",
431+
shq(env.home.string()),
432+
xvmBin,
433+
env.home.string(),
434+
shq(env.binary.string()));
435+
}
436+
return std::format(
437+
"cd /d {} && set \"PATH={};%PATH%\" && set \"XLINGS_HOME={}\" && set \"XLINGS_PROJECT_DIR={}\" && {}",
438+
shq(env.home.string()),
439+
xvmBin,
440+
env.home.string(),
441+
env.projectDir.string(),
442+
shq(env.binary.string()));
443+
#else
416444
if (env.projectDir.empty()) {
417445
// Global mode: unset XLINGS_PROJECT_DIR (existing behavior).
418446
return std::format(
@@ -431,13 +459,19 @@ std::string build_command_prefix(const Env& env) {
431459
shq(env.home.string()),
432460
shq(env.projectDir.string()),
433461
shq(env.binary.string()));
462+
#endif
434463
}
435464

436465
std::string build_interface_command(const Env& env,
437466
std::string_view capability,
438467
std::string_view argsJson) {
468+
#if defined(_WIN32)
469+
return std::format("{} interface {} --args {} 2>nul",
470+
build_command_prefix(env), capability, shq(argsJson));
471+
#else
439472
return std::format("{} interface {} --args {} 2>/dev/null",
440473
build_command_prefix(env), capability, shq(argsJson));
474+
#endif
441475
}
442476

443477
// ─── JSON extraction helpers ────────────────────────────────────────
@@ -628,12 +662,21 @@ int install_with_progress(const Env& env, std::string_view target,
628662
auto argsJson = std::format(
629663
R"({{"targets":["{}"],"yes":true}})", target);
630664

665+
#if defined(_WIN32)
666+
auto cmd = std::format(
667+
"cd /d {} && set \"XLINGS_PROJECT_DIR=\" && set \"XLINGS_HOME={}\" && {} interface install_packages --args {} 2>nul",
668+
shq(env.home.string()),
669+
env.home.string(),
670+
shq(env.binary.string()),
671+
shq(argsJson));
672+
#else
631673
auto cmd = std::format(
632674
"cd {} && env -u XLINGS_PROJECT_DIR XLINGS_HOME={} {} interface install_packages --args {} 2>/dev/null",
633675
shq(env.home.string()),
634676
shq(env.home.string()),
635677
shq(env.binary.string()),
636678
shq(argsJson));
679+
#endif
637680

638681
std::FILE* fp = ::popen(cmd.c_str(), "r");
639682
if (!fp) return -1;
@@ -746,11 +789,19 @@ void ensure_init(const Env& env, bool quiet) {
746789

747790
if (!quiet)
748791
print_status("Initialize", "mcpp sandbox layout (one-time)");
792+
#if defined(_WIN32)
793+
auto cmd = std::format(
794+
"cd /d {} && set \"XLINGS_PROJECT_DIR=\" && set \"XLINGS_HOME={}\" && {} self init >nul 2>&1",
795+
shq(env.home.string()),
796+
env.home.string(),
797+
shq(env.binary.string()));
798+
#else
749799
auto cmd = std::format(
750800
"cd {} && env -u XLINGS_PROJECT_DIR XLINGS_HOME={} {} self init >/dev/null 2>&1",
751801
shq(env.home.string()),
752802
shq(env.home.string()),
753803
shq(env.binary.string()));
804+
#endif
754805
int rc = std::system(cmd.c_str());
755806
if (rc != 0 && !quiet) {
756807
std::println(stderr,

0 commit comments

Comments
 (0)