diff --git a/.agents/docs/platform-abstraction-plan.md b/.agents/docs/platform-abstraction-plan.md new file mode 100644 index 0000000..c071b99 --- /dev/null +++ b/.agents/docs/platform-abstraction-plan.md @@ -0,0 +1,102 @@ +# Platform Abstraction Layer — Architecture & Implementation Plan + +## 1. Problem Statement + +mcpp has ~45+ `#if defined(...)` blocks scattered across 10+ source files for platform-specific logic. This causes: +- **stdin leakage**: subprocess calls on macOS don't close stdin → first-run hangs +- **Code duplication**: `self_exe_path()` duplicated in `config.cppm` and `ninja_backend.cppm` +- **Inconsistent abstractions**: `process.cppm` wraps some calls, but many files still use raw `std::system()`/`popen()` +- **Maintenance burden**: adding platform support requires editing 10+ files + +## 2. Target Architecture + +``` +src/platform/ +├── platform.cppm # Unified facade (re-exports all platform capabilities) +├── common.cppm # Cross-platform constants + compile-time detection +├── process.cppm # Unified process execution (auto-closes stdin on POSIX) +├── fs.cppm # Platform filesystem ops (exe path, file lock, which) +├── env.cppm # Environment variable operations +├── shell.cppm # Shell quoting + command building +├── macos.cppm # macOS: xcrun, SDK discovery, Xcode CLT detection +├── linux.cppm # Linux: /proc, LD_LIBRARY_PATH, patchelf +└── windows.cppm # Windows: vswhere, MSVC, _putenv_s, registry +``` + +## 3. Module Responsibilities + +### common.cppm — Compile-time constants & platform detection +- Platform booleans: `is_windows`, `is_macos`, `is_linux` +- Binary naming: `exe_suffix`, `static_lib_ext`, `shared_lib_ext`, `lib_prefix` +- Platform name string +- Null redirect string + +### process.cppm — Unified process execution (CORE FIX) +- `capture()` — run command, capture stdout (auto ` -#include -#else -#include -#include -#include -#endif export module mcpp.bmi_cache; import std; +import mcpp.platform; export namespace mcpp::bmi_cache { @@ -188,56 +181,6 @@ stage_into(const CacheKey& key, } namespace { - -// 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); - auto lockPath = dir / ".lock"; - int fd = ::open(lockPath.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0644); - if (fd < 0) return -1; - if (::flock(fd, LOCK_EX | LOCK_NB) != 0) { - ::close(fd); - return -1; - } - return fd; -} - -void release_lock(int fd) { - if (fd < 0) return; - ::flock(fd, LOCK_UN); - ::close(fd); -} -#endif - } // namespace std::expected @@ -246,26 +189,11 @@ 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) { + auto lock = mcpp::platform::fs::FileLock::try_acquire(cacheDir); + if (!lock) { // Another writer holds the lock; treat as success (they'll do it). return {}; } - struct LockGuard { - 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 3a8916d..6f0a445 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -10,6 +10,7 @@ export module mcpp.build.flags; import std; import mcpp.build.plan; +import mcpp.platform; import mcpp.toolchain.clang; import mcpp.toolchain.detect; import mcpp.toolchain.provider; @@ -159,46 +160,23 @@ CompileFlags compute_flags(const BuildPlan& plan) { // Link flags f.staticStdlib = plan.manifest.buildConfig.staticStdlib; f.linkage = plan.manifest.buildConfig.linkage; -#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" : ""; - std::string static_stdlib = (f.staticStdlib && !isClang) ? " -static-libstdc++" : ""; -#endif + std::string full_static = (mcpp::platform::supports_full_static && f.linkage == "static") ? " -static" : ""; + std::string static_stdlib = (f.staticStdlib && !isClang && !mcpp::platform::is_windows) ? " -static-libstdc++" : ""; 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); + if constexpr (mcpp::platform::supports_rpath) { + for (auto& dir : plan.toolchain.linkRuntimeDirs) { + runtime_dirs += " -L" + escape_path(dir); + runtime_dirs += " -Wl,-rpath," + escape_path(dir); + } + } + + if constexpr (mcpp::platform::is_windows) { + f.ld = ""; + } else if constexpr (mcpp::platform::needs_explicit_libcxx) { + f.ld = std::format("{}{}{} -lc++", full_static, static_stdlib, b_flag); + } else { + f.ld = std::format("{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag, runtime_dirs); } -#endif - -#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 - // libc++abi. System /usr/lib/libc++ does (and is ABI-compatible - // with LLVM 20 headers since macOS ships a recent libc++). - // - No -rpath for LLVM lib: binary should use system libc++ at runtime. - // - Explicit -lc++: clang++.cfg's -nostdinc++ suppresses implicit linkage. - // Result: compile with LLVM headers, link with system libc++ + libc++abi. - f.ld = std::format("{}{}{} -lc++", full_static, static_stdlib, b_flag); -#else - // Linux: sysroot + runtime dirs needed (glibc/libc++ live in sandbox) - f.ld = std::format("{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag, - runtime_dirs); -#endif return f; } diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index b5cccce..26f1fbc 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -14,13 +14,6 @@ module; #include #include -#if defined(_WIN32) -#include -#define popen _popen -#define pclose _pclose -#elif defined(__APPLE__) -#include // _NSGetExecutablePath -#endif export module mcpp.build.ninja; @@ -33,6 +26,7 @@ import mcpp.dyndep; import mcpp.toolchain.detect; import mcpp.toolchain.registry; import mcpp.xlings; +import mcpp.platform; export namespace mcpp::build { @@ -92,20 +86,14 @@ void write_file(const std::filesystem::path& p, std::string_view content) { os << content; } -bool run(const std::string& cmd, std::string& output_capture, bool capture = true) { - std::array buf{}; +bool run(const std::string& cmd, std::string& output_capture, bool capture_output = true) { output_capture.clear(); - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) - return false; - while (std::fgets(buf.data(), buf.size(), fp) != nullptr) { - if (capture) - output_capture += buf.data(); - else - std::fputs(buf.data(), stdout); + if (capture_output) { + auto r = mcpp::platform::process::capture(cmd); + output_capture = r.output; + return r.exit_code == 0; } - int rc = ::pclose(fp); - return rc == 0; + return mcpp::platform::process::run_passthrough(cmd) == 0; } bool dyndep_mode_enabled() { @@ -119,27 +107,7 @@ bool dyndep_mode_enabled() { } std::filesystem::path mcpp_exe_path() { - std::error_code ec; -#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) { - auto p = std::filesystem::canonical(buf, ec); - if (!ec) return p; - } -#else - auto p = std::filesystem::read_symlink("/proc/self/exe", ec); - if (!ec) - return p; -#endif - return "mcpp"; // fall back to PATH lookup + return mcpp::platform::fs::self_exe_path(); } bool is_c_source(const std::filesystem::path& src) { @@ -197,13 +165,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 + if constexpr (mcpp::platform::is_windows) { + // 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"); + } append(" description = STAGE $out\n\n"); // P1: per-file dyndep rule. Converts one .ddi → .dd independently. @@ -223,38 +191,38 @@ std::string emit_ninja_string(const BuildPlan& plan) { // // $bmi_out is set per build edge to the BMI path (gcm.cache/.gcm). // If $bmi_out is empty (no module provided), we just compile normally. + // On POSIX, $toolenv prefixes commands with LD_LIBRARY_PATH etc. + // On Windows, $toolenv is empty and its leading space breaks CreateProcess, + // so we omit it entirely. + constexpr std::string_view te = mcpp::platform::is_windows ? "" : "$toolenv "; + 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\"; " - "fi && " - "$toolenv $cxx $cxxflags{} -c $in -o $out && " - "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && " - "cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then " - "mv \"$bmi_out.bak\" \"$bmi_out\"; " - "else " - "rm -f \"$bmi_out.bak\"; " - "fi\n", module_output_flag)); -#endif + if constexpr (mcpp::platform::is_windows) { + // Windows: skip BMI restat optimization (requires POSIX shell). + 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\"; " + "fi && " + "{}$cxx $cxxflags{} -c $in -o $out && " + "if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && " + "cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then " + "mv \"$bmi_out.bak\" \"$bmi_out\"; " + "else " + "rm -f \"$bmi_out.bak\"; " + "fi\n", te, module_output_flag)); + } 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(std::format(" command = {}$cxx $cxxflags -c $in -o $out\n", te)); append(" description = OBJ $out\n"); if (dyndep) append(" restat = 1\n"); @@ -262,42 +230,24 @@ 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(std::format(" command = {}$cc $cflags -c $in -o $out\n", te)); 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(std::format(" command = {}$cxx $in -o $out $ldflags\n", te)); append(" description = LINK $out\n\n"); append("rule cxx_archive\n"); - append(" command = $toolenv $ar rcs $out $in\n"); + append(std::format(" command = {}$ar rcs $out $in\n", te)); append(" description = AR $out\n\n"); append("rule cxx_shared\n"); - append(" command = $toolenv $cxx -shared $in -o $out $ldflags\n"); + append(std::format(" command = {}$cxx -shared $in -o $out $ldflags\n", te)); append(" description = SHARED $out\n\n"); -#endif if (dyndep) { // Scan rule: produce P1689 .ddi for one TU. @@ -306,25 +256,21 @@ 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 + append(std::format(" command = {}$cxx $cxxflags -fmodules " "-fdeps-format=p1689r5 " "-fdeps-file=$out -fdeps-target=$compile_target " - "-M -MM -MF $out.dep -E $in -o $compile_target\n"); + "-M -MM -MF $out.dep -E $in -o $compile_target\n", te)); } else { // Clang path: clang-scan-deps produces P1689 JSON to stdout. -#if defined(_WIN32) - // Wrap in cmd /c for shell redirection (ninja on Windows uses - // CreateProcess which doesn't interpret > as redirect). - append(" command = cmd /c \"$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 + if constexpr (mcpp::platform::is_windows) { + // Wrap in cmd /c for shell redirection (ninja on Windows uses + // CreateProcess which doesn't interpret > as redirect). + append(" command = cmd /c \"$scan_deps -format=p1689 -- " + "$cxx $cxxflags -c $in -o $compile_target > $out\"\n"); + } else { + append(std::format(" command = {}$scan_deps -format=p1689 -- " + "$cxx $cxxflags -c $in -o $compile_target > $out\n", te)); + } } append(" description = SCAN $out\n\n"); @@ -567,27 +513,21 @@ 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) + auto ninja_name = std::string("ninja") + std::string(mcpp::platform::exe_suffix); if (auto nb = mcpp::xlings::paths::find_sibling_binary( - plan.toolchain.binaryPath, "ninja", "ninja.exe")) { + plan.toolchain.binaryPath, "ninja", ninja_name)) { ninjaBin = *nb; } -#else - if (auto nb = mcpp::xlings::paths::find_sibling_binary( - plan.toolchain.binaryPath, "ninja", "ninja")) { - ninjaBin = *nb; + + std::string ninjaProgram; + if (!ninjaBin.empty()) { + if constexpr (mcpp::platform::is_windows) + ninjaProgram = ninjaBin.string(); + else + ninjaProgram = mcpp::platform::shell::quote(ninjaBin.string()); + } else { + ninjaProgram = "ninja"; } -#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() ? mcpp::xlings::shq(ninjaBin.string()) : std::string{"ninja"}; -#endif // Record ninja binary for P0 fast-path cache. BuildResult r; diff --git a/src/cli.cppm b/src/cli.cppm index a8ba0f1..172e124 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -11,10 +11,6 @@ module; #include #include -#if defined(_WIN32) -#define popen _popen -#define pclose _pclose -#endif export module mcpp.cli; @@ -36,6 +32,7 @@ import mcpp.publish.xpkg_emit; import mcpp.pack; import mcpp.config; import mcpp.xlings; +import mcpp.platform; import mcpp.fetcher; import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now @@ -665,25 +662,25 @@ void patchelf_walk(const std::filesystem::path& dir, continue; is.close(); // Probe PT_INTERP — skip static binaries (no interp). - auto probe = std::format("'{}' --print-interpreter '{}' 2>/dev/null", - patchelfBin.string(), path.string()); - std::FILE* fp = ::popen(probe.c_str(), "r"); - bool hasInterp = false; - if (fp) { - char buf[1024]{}; - hasInterp = (std::fread(buf, 1, sizeof(buf) - 1, fp) > 0); - ::pclose(fp); - } + auto probe = std::format("{} --print-interpreter {} 2>/dev/null", + mcpp::platform::shell::quote(patchelfBin.string()), + mcpp::platform::shell::quote(path.string())); + auto probeResult = mcpp::platform::process::capture(probe); + bool hasInterp = (probeResult.exit_code == 0 && !probeResult.output.empty()); if (hasInterp) { - (void)std::system(std::format( - "'{}' --set-interpreter '{}' '{}' 2>/dev/null", - patchelfBin.string(), loader.string(), path.string()).c_str()); + (void)mcpp::platform::process::run_silent(std::format( + "{} --set-interpreter {} {} 2>/dev/null", + mcpp::platform::shell::quote(patchelfBin.string()), + mcpp::platform::shell::quote(loader.string()), + mcpp::platform::shell::quote(path.string()))); } // Always set RUNPATH (works on .so too — they need to find deps). if (!rpath.empty()) { - (void)std::system(std::format( - "'{}' --set-rpath '{}' '{}' 2>/dev/null", - patchelfBin.string(), rpath, path.string()).c_str()); + (void)mcpp::platform::process::run_silent(std::format( + "{} --set-rpath {} {} 2>/dev/null", + mcpp::platform::shell::quote(patchelfBin.string()), + mcpp::platform::shell::quote(rpath), + mcpp::platform::shell::quote(path.string()))); } } } @@ -1016,16 +1013,7 @@ prepare_build(bool print_fingerprint, return &*cfg_opt; }; - constexpr std::string_view kCurrentPlatform = -#if defined(__linux__) - "linux"; -#elif defined(__APPLE__) - "macos"; -#elif defined(_WIN32) - "windows"; -#else - "unknown"; -#endif + constexpr std::string_view kCurrentPlatform = mcpp::platform::name; // M5.5: toolchain resolution priority: // 0. --target X / --static, looked up in [target.] @@ -1110,21 +1098,21 @@ prepare_build(bool print_fingerprint, // CI / offline / test opt-out: hard-error instead of silently // pulling ~800 MB of toolchain. Preserves the original M5.5 // contract for environments that need it. -#if defined(__APPLE__) || defined(_WIN32) - return std::unexpected( - "no toolchain configured.\n" - " run one of:\n" - " mcpp toolchain install llvm 20.1.7\n" - " mcpp toolchain default llvm@20.1.7\n" - " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install."); -#else - return std::unexpected( - "no toolchain configured.\n" - " run one of:\n" - " mcpp toolchain install gcc 15.1.0-musl\n" - " mcpp toolchain default gcc@15.1.0-musl\n" - " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install."); -#endif + if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) { + return std::unexpected( + "no toolchain configured.\n" + " run one of:\n" + " mcpp toolchain install llvm 20.1.7\n" + " mcpp toolchain default llvm@20.1.7\n" + " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install."); + } else { + return std::unexpected( + "no toolchain configured.\n" + " run one of:\n" + " mcpp toolchain install gcc 15.1.0-musl\n" + " mcpp toolchain default gcc@15.1.0-musl\n" + " or unset MCPP_NO_AUTO_INSTALL to let mcpp auto-install."); + } } else { // First-run UX: no project-level [toolchain], no global default, // and the user just ran `mcpp build` (or similar). Auto-install @@ -1136,23 +1124,20 @@ 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__) || defined(_WIN32) - std::string defaultSpec = "llvm@20.1.7"; -#else - std::string defaultSpec = "gcc@15.1.0-musl"; -#endif + std::string defaultSpec = (mcpp::platform::is_macos || mcpp::platform::is_windows) + ? "llvm@20.1.7" : "gcc@15.1.0-musl"; auto defaultParsed = mcpp::toolchain::parse_toolchain_spec(defaultSpec); auto defaultPkg = mcpp::toolchain::to_xim_package(*defaultParsed); -#if defined(__APPLE__) || defined(_WIN32) - mcpp::ui::info("First run", - std::format("no toolchain configured — installing {} (LLVM/Clang) as default", - defaultSpec)); -#else - mcpp::ui::info("First run", - std::format("no toolchain configured — installing {} (musl, static) as default", - defaultSpec)); -#endif + if constexpr (mcpp::platform::is_macos || mcpp::platform::is_windows) { + mcpp::ui::info("First run", + std::format("no toolchain configured — installing {} (LLVM/Clang) as default", + defaultSpec)); + } else { + mcpp::ui::info("First run", + std::format("no toolchain configured — installing {} (musl, static) as default", + defaultSpec)); + } auto cfg = get_cfg(); if (!cfg) return std::unexpected(cfg.error()); @@ -1959,36 +1944,25 @@ prepare_build(bool print_fingerprint, std::format("{} ({} = {})", spec.git, spec.gitRefKind, spec.gitRev)); std::string cloneCmd; if (spec.gitRefKind == "branch") { -#if defined(_WIN32) - cloneCmd = std::format( - "git clone --depth 1 --branch \"{}\" \"{}\" \"{}\" 2>&1", - spec.gitRev, spec.git, gitRoot.string()); -#else cloneCmd = std::format( - "git clone --depth 1 --branch '{}' '{}' '{}' 2>&1", - spec.gitRev, spec.git, gitRoot.string()); -#endif + "git clone --depth 1 --branch {} {} {} 2>&1", + mcpp::platform::shell::quote(spec.gitRev), + mcpp::platform::shell::quote(spec.git), + mcpp::platform::shell::quote(gitRoot.string())); } else { // For tag/rev: full clone, then checkout (depth-1 may miss the rev). -#if defined(_WIN32) cloneCmd = std::format( - "git clone \"{}\" \"{}\" && cd \"{}\" && git checkout --quiet \"{}\" 2>&1", - spec.git, gitRoot.string(), - gitRoot.string(), spec.gitRev); -#else - cloneCmd = std::format( - "git clone '{}' '{}' && cd '{}' && git checkout --quiet '{}' 2>&1", - spec.git, gitRoot.string(), - gitRoot.string(), spec.gitRev); -#endif + "git clone {} {} && cd {} && git checkout --quiet {} 2>&1", + mcpp::platform::shell::quote(spec.git), + mcpp::platform::shell::quote(gitRoot.string()), + mcpp::platform::shell::quote(gitRoot.string()), + mcpp::platform::shell::quote(spec.gitRev)); } std::string out; { - std::array buf{}; - std::FILE* fp = ::popen(cloneCmd.c_str(), "r"); - if (!fp) return std::unexpected("popen failed for git clone"); - while (std::fgets(buf.data(), buf.size(), fp)) out += buf.data(); - int rc = ::pclose(fp); + auto r = mcpp::platform::process::capture(cloneCmd); + out = r.output; + int rc = r.exit_code; if (rc != 0) { std::filesystem::remove_all(gitRoot, ec); return std::unexpected(std::format( @@ -2532,24 +2506,16 @@ 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 + std::string cmd = std::format("{} -C {}", ninjaProgram, mcpp::platform::shell::quote(outputDir.string())); if (verbose) cmd += " -v"; cmd += " 2>&1"; auto t0 = std::chrono::steady_clock::now(); std::string out; - FILE* pipe = popen(cmd.c_str(), "r"); - if (!pipe) return std::nullopt; - char buf[4096]; - while (std::fgets(buf, sizeof(buf), pipe)) { - out += buf; - if (verbose) std::fputs(buf, stdout); - } - int status = pclose(pipe); + auto r = mcpp::platform::process::capture(cmd); + out = r.output; + if (verbose && !out.empty()) std::fputs(out.c_str(), stdout); + int status = r.exit_code; bool ok = (status == 0); if (!ok) { if (!verbose) std::fputs(out.c_str(), stdout); @@ -2625,14 +2591,10 @@ 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; + std::string cmd = mcpp::platform::shell::quote(exe.string()); + for (auto& a : passthrough) cmd += " " + mcpp::platform::shell::quote(a); + int rc = std::system(cmd.c_str()); + return mcpp::platform::process::extract_exit_code(rc) == 0 ? 0 : 1; } int cmd_env(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) { @@ -3173,30 +3135,20 @@ 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(); - auto sandboxBin = registryDir / "subos" / "default" / "bin"; - 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 on POSIX, exit code on Windows. -#if defined(_WIN32) - int exitCode = rc; -#else - int exitCode = WIFEXITED(rc) ? WEXITSTATUS(rc) : 127; -#endif + if constexpr (!mcpp::platform::is_windows) { + 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(); + auto sandboxBin = registryDir / "subos" / "default" / "bin"; + if (std::filesystem::exists(sandboxBin)) + pathPrefix = std::format("PATH={}:\"$PATH\" ", + mcpp::platform::shell::quote(sandboxBin.string())); + } + } + + std::string cmd = pathPrefix + mcpp::platform::shell::quote(exe.string()); + for (auto& a : passthrough) cmd += " " + mcpp::platform::shell::quote(a); + int exitCode = mcpp::platform::process::extract_exit_code(std::system(cmd.c_str())); if (exitCode == 0) { std::println("{} ... ok", lu.targetName); @@ -3824,14 +3776,10 @@ int cmd_publish(const mcpplibs::cmdline::ParsedArgs& parsed) { // Sanity: working tree clean (best-effort via git status). if (!allow_dirty && std::filesystem::exists(*root / ".git")) { - std::string out; - std::FILE* fp = ::popen( - std::format("git -C {} status --porcelain 2>&1", root->string()).c_str(), "r"); - if (fp) { - std::array buf{}; - while (std::fgets(buf.data(), buf.size(), fp)) out += buf.data(); - ::pclose(fp); - } + auto gitStatus = mcpp::platform::process::capture( + std::format("git -C {} status --porcelain 2>&1", + mcpp::platform::shell::quote(root->string()))); + std::string out = gitStatus.output; if (!out.empty()) { mcpp::ui::error("working tree has uncommitted changes; pass --allow-dirty to skip this check"); std::println(stderr, "{}", out); diff --git a/src/config.cppm b/src/config.cppm index 2c51951..93b27b2 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -14,15 +14,7 @@ // the directory tree exists and seeds .xlings.json if missing. module; -#include #include -#if defined(_WIN32) -#include -#define popen _popen -#define pclose _pclose -#elif defined(__APPLE__) -#include // _NSGetExecutablePath -#endif export module mcpp.config; @@ -30,6 +22,7 @@ import std; import mcpp.libs.toml; import mcpp.pm.index_spec; import mcpp.xlings; +import mcpp.platform; export namespace mcpp::config { @@ -180,22 +173,8 @@ std::filesystem::path home_dir() { return std::filesystem::path(e); std::error_code ec; -#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; - if (_NSGetExecutablePath(_exe_buf, &_exe_size) == 0) - exe = std::filesystem::canonical(_exe_buf, ec); -#else - auto exe = std::filesystem::canonical("/proc/self/exe", ec); -#endif - if (!ec && exe.parent_path().filename() == "bin") { + auto exe = mcpp::platform::fs::self_exe_path(); + if (exe.has_parent_path() && exe.parent_path().filename() == "bin") { // Dev builds emit binaries at target///bin/, // matching the bin/ shape. Any ancestor literally named // "target" disqualifies self-contained mode and falls through @@ -367,14 +346,10 @@ 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(); + auto xlings_name = std::string("xlings") + std::string(mcpp::platform::exe_suffix); + auto sysXlings = mcpp::platform::fs::which(xlings_name); + if (sysXlings) { + std::string p = sysXlings->string(); if (!p.empty() && std::filesystem::exists(p)) { std::filesystem::copy_file(p, destBin, std::filesystem::copy_options::overwrite_existing, ec); @@ -453,11 +428,8 @@ 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.xlingsBinary = cfg.registryDir / "bin" / + (std::string("xlings") + std::string(mcpp::platform::exe_suffix)); cfg.bmiCacheDir = cfg.mcppHome / "bmi"; cfg.metaCacheDir = cfg.mcppHome / "cache"; cfg.logDir = cfg.mcppHome / "log"; @@ -552,16 +524,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()) + auto sysPath = mcpp::platform::fs::which( + std::string("xlings") + std::string(mcpp::platform::exe_suffix)); + if (!sysPath) return std::unexpected(ConfigError{"system xlings not found in PATH"}); - std::string p = *sys; - while (!p.empty() && (p.back() == '\n' || p.back() == '\r')) p.pop_back(); - cfg.xlingsBinary = p; + cfg.xlingsBinary = *sysPath; } else { cfg.xlingsBinary = cfg.xlingsBinaryMode; if (!std::filesystem::exists(cfg.xlingsBinary)) diff --git a/src/modgraph/p1689.cppm b/src/modgraph/p1689.cppm index e42ae31..7e8a386 100644 --- a/src/modgraph/p1689.cppm +++ b/src/modgraph/p1689.cppm @@ -17,17 +17,12 @@ // Spec: docs/27-p1689-dyndep.md. module; -#include -#include -#if defined(_WIN32) -#define popen _popen -#define pclose _pclose -#endif export module mcpp.modgraph.p1689; import std; import mcpp.modgraph.graph; +import mcpp.platform; import mcpp.toolchain.detect; export namespace mcpp::modgraph::p1689 { @@ -300,12 +295,9 @@ std::expected parse_ddi(std::string_view body) { namespace { bool run_capturing(const std::string& cmd, std::string& out) { - std::array buf{}; - out.clear(); - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return false; - while (std::fgets(buf.data(), buf.size(), fp)) out += buf.data(); - return ::pclose(fp) == 0; + auto r = mcpp::platform::process::capture(cmd); + out = r.output; + return r.exit_code == 0; } std::string shell_escape(const std::filesystem::path& p) { diff --git a/src/pack/pack.cppm b/src/pack/pack.cppm index 5f60770..1342d78 100644 --- a/src/pack/pack.cppm +++ b/src/pack/pack.cppm @@ -16,16 +16,12 @@ // ⤷ mode suffix omitted for the default (`bundle-project`). module; -#include // popen, pclose -#if defined(_WIN32) -#define popen _popen -#define pclose _pclose -#endif export module mcpp.pack; import std; import mcpp.config; +import mcpp.platform; import mcpp.xlings; import mcpp.manifest; @@ -188,22 +184,16 @@ namespace detail { // success, or an error message on non-zero exit. std::expected run_capture(const std::string& cmd) { - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return std::unexpected("popen failed: " + cmd); - std::string out; - std::array buf{}; - while (std::fgets(buf.data(), buf.size(), fp)) - out += buf.data(); - int rc = ::pclose(fp); - if (rc != 0) return std::unexpected(std::format( - "command exited with {}: {}", rc, cmd)); - return out; + auto r = mcpp::platform::process::capture(cmd); + if (r.exit_code != 0) return std::unexpected(std::format( + "command exited with {}: {}", r.exit_code, cmd)); + return r.output; } // Run a shell command and discard stdout/stderr; return exit code. int run_silent(const std::string& cmd) { - auto silent = cmd + " >/dev/null 2>&1"; - return std::system(silent.c_str()); + auto silent = cmd + " " + std::string(mcpp::platform::shell::silent_redirect); + return mcpp::platform::process::run_silent(silent); } // ─── ldd parsing + manylinux skip-list ────────────────────────────── diff --git a/src/platform.cppm b/src/platform/common.cppm similarity index 57% rename from src/platform.cppm rename to src/platform/common.cppm index fee9587..855ccfd 100644 --- a/src/platform.cppm +++ b/src/platform/common.cppm @@ -1,14 +1,12 @@ -// mcpp.platform — centralized platform-specific constants. +// mcpp.platform.common — centralized platform-specific constants. // -// Consumers import this module instead of scattering #if/_WIN32 / __APPLE__ -// blocks throughout the codebase. All compile-time branching lives here. +// Consumers import this module (via mcpp.platform) instead of scattering +// #if/_WIN32 / __APPLE__ blocks throughout the codebase. All compile-time +// branching for platform constants 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; +export module mcpp.platform.common; import std; @@ -62,4 +60,37 @@ constexpr bool is_macos = false; constexpr bool is_linux = false; #endif +// ── Platform name string ─────────────────────────────────────────────────── + +constexpr std::string_view name = +#if defined(_WIN32) + "windows"; +#elif defined(__APPLE__) + "macos"; +#elif defined(__linux__) + "linux"; +#else + "unknown"; +#endif + +// xpkg platform key (used by resolver for xpkg.lua lookups). +// Note: macOS uses "macosx" (not "macos") for xpkg compatibility. +constexpr std::string_view xpkg_platform = +#if defined(_WIN32) + "windows"; +#elif defined(__APPLE__) + "macosx"; +#elif defined(__linux__) + "linux"; +#else + "linux"; +#endif + +// ── Link strategy capabilities ───────────────────────────────────────── +// Used by build/flags.cppm to avoid #ifdef blocks in linker flag logic. + +constexpr bool supports_full_static = is_linux; // macOS/Windows cannot +constexpr bool supports_rpath = !is_windows; // ELF + Mach-O only +constexpr bool needs_explicit_libcxx = is_macos; // macOS: -lc++ required + } // namespace mcpp::platform diff --git a/src/platform/env.cppm b/src/platform/env.cppm new file mode 100644 index 0000000..525caa4 --- /dev/null +++ b/src/platform/env.cppm @@ -0,0 +1,79 @@ +// mcpp.platform.env — platform-aware environment variable operations. +// +// Windows: uses _putenv_s to mutate the calling process environment. +// POSIX: builds "KEY=val" shell prefix strings (no process mutation). + +module; +#include +#if defined(_WIN32) +#include // _putenv_s +#endif + +export module mcpp.platform.env; + +import std; + +export namespace mcpp::platform::env { + +// Get an environment variable. Returns nullopt if not set. +std::optional get(std::string_view key); + +// Set an environment variable in the current process. +// On POSIX this is a no-op by design — use build_env_prefix() instead +// to scope vars to a child process via command-line prefixing. +void set(const std::string& key, const std::string& value); + +// Build a shell command prefix that injects the given env vars. +// Windows: calls set() for each var and returns "". +// POSIX: returns "KEY1='val1' KEY2='val2' " (caller prepends to command). +std::string build_env_prefix( + const std::vector>& vars); + +} // namespace mcpp::platform::env + +// ─── Implementation ────────────────────────────────────────────────────── + +namespace mcpp::platform::env { + +std::optional get(std::string_view key) { + std::string k(key); + auto* v = std::getenv(k.c_str()); + if (!v || !*v) return std::nullopt; + return std::string(v); +} + +void set(const std::string& key, const std::string& value) { +#if defined(_WIN32) + _putenv_s(key.c_str(), value.c_str()); +#else + // POSIX: intentional no-op. Use build_env_prefix() instead. + (void)key; + (void)value; +#endif +} + +std::string build_env_prefix( + const std::vector>& vars) +{ +#if defined(_WIN32) + for (auto& [k, v] : vars) + _putenv_s(k.c_str(), v.c_str()); + return ""; +#else + std::string prefix; + for (auto& [k, v] : vars) { + prefix += k; + prefix += '='; + prefix += '\''; + for (char c : v) { + if (c == '\'') prefix += "'\\''"; + else prefix += c; + } + prefix += '\''; + prefix += ' '; + } + return prefix; +#endif +} + +} // namespace mcpp::platform::env diff --git a/src/platform/fs.cppm b/src/platform/fs.cppm new file mode 100644 index 0000000..c0afa2d --- /dev/null +++ b/src/platform/fs.cppm @@ -0,0 +1,225 @@ +// mcpp.platform.fs — platform-specific filesystem operations. +// +// Consolidates three patterns that were duplicated across the codebase: +// 1. self_exe_path() — current executable path (was in config.cppm & +// ninja_backend.cppm) +// 2. which() — find an executable in PATH (was in config.cppm & +// probe.cppm) +// 3. FileLock — RAII exclusive file lock (was in bmi_cache.cppm) + +module; +#include +#include +#if defined(_WIN32) +#include +#include +#define popen _popen +#define pclose _pclose +#elif defined(__APPLE__) +#include // _NSGetExecutablePath +#else +#include +#include +#include +#endif + +// POSIX file locking (shared between macOS and Linux) +#if !defined(_WIN32) +#include +#include +#include +#endif + +export module mcpp.platform.fs; + +import std; +import mcpp.platform.common; + +export namespace mcpp::platform::fs { + +// ── self_exe_path ───────────────────────────────────────────────────────── +// +// Returns the absolute path of the currently running executable. +// Windows: GetModuleFileNameA +// macOS: _NSGetExecutablePath +// Linux: /proc/self/exe +std::filesystem::path self_exe_path(); + +// ── which ───────────────────────────────────────────────────────────────── +// +// Find an executable by name in PATH. +// Windows: `where ` +// POSIX: `command -v ` +std::optional which(std::string_view binary_name); + +// ── FileLock ────────────────────────────────────────────────────────────── +// +// RAII exclusive non-blocking file lock. +// Windows: LockFileEx on a HANDLE +// POSIX: flock(2) on a file descriptor +class FileLock { +public: + // Try to acquire an exclusive lock on /.lock. + // Returns nullopt if another process holds the lock. + static std::optional try_acquire(const std::filesystem::path& dir); + + ~FileLock(); + FileLock(FileLock&& o) noexcept; + FileLock& operator=(FileLock&& o) noexcept; + + FileLock(const FileLock&) = delete; + FileLock& operator=(const FileLock&) = delete; + +private: +#if defined(_WIN32) + void* handle_ = reinterpret_cast(static_cast(-1)); // INVALID_HANDLE_VALUE + explicit FileLock(void* h) : handle_(h) {} +#else + int fd_ = -1; + explicit FileLock(int fd) : fd_(fd) {} +#endif +}; + +} // namespace mcpp::platform::fs + +// ─── Implementation ────────────────────────────────────────────────────── + +namespace mcpp::platform::fs { + +std::filesystem::path self_exe_path() { + std::error_code ec; +#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) { + auto p = std::filesystem::canonical(buf, ec); + if (!ec) return p; + } +#else + auto p = std::filesystem::read_symlink("/proc/self/exe", ec); + if (!ec) return p; +#endif + // Fallback: hope the binary is in PATH + return "mcpp"; +} + +std::optional which(std::string_view binary_name) { + std::string name(binary_name); +#if defined(_WIN32) + std::string cmd = "where " + name + " 2>nul"; +#else + std::string cmd = "command -v '" + name + "' 2>/dev/null buf{}; + std::string out; + std::FILE* fp = ::popen(cmd.c_str(), "r"); + if (!fp) return std::nullopt; + while (std::fgets(buf.data(), static_cast(buf.size()), fp) != nullptr) + out += buf.data(); + int rc = ::pclose(fp); + + // Trim and take first line + while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) + out.pop_back(); + auto nl = out.find('\n'); + if (nl != std::string::npos) out.resize(nl); + + if (rc != 0 || out.empty()) return std::nullopt; + if (!std::filesystem::exists(out)) return std::nullopt; + return std::filesystem::path(out); +} + +// ── FileLock ────────────────────────────────────────────────────────────── + +#if defined(_WIN32) + +std::optional FileLock::try_acquire(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 std::nullopt; + OVERLAPPED ov = {}; + if (!LockFileEx(h, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, + 0, 1, 0, &ov)) { + CloseHandle(h); + return std::nullopt; + } + return FileLock{h}; +} + +FileLock::~FileLock() { + if (handle_ != INVALID_HANDLE_VALUE) { + OVERLAPPED ov = {}; + UnlockFileEx(static_cast(handle_), 0, 1, 0, &ov); + CloseHandle(static_cast(handle_)); + } +} + +FileLock::FileLock(FileLock&& o) noexcept : handle_(o.handle_) { + o.handle_ = INVALID_HANDLE_VALUE; +} + +FileLock& FileLock::operator=(FileLock&& o) noexcept { + if (this != &o) { + if (handle_ != INVALID_HANDLE_VALUE) { + OVERLAPPED ov = {}; + UnlockFileEx(static_cast(handle_), 0, 1, 0, &ov); + CloseHandle(static_cast(handle_)); + } + handle_ = o.handle_; + o.handle_ = INVALID_HANDLE_VALUE; + } + return *this; +} + +#else // POSIX + +std::optional FileLock::try_acquire(const std::filesystem::path& dir) { + std::error_code ec; + std::filesystem::create_directories(dir, ec); + auto lockPath = dir / ".lock"; + int fd = ::open(lockPath.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0644); + if (fd < 0) return std::nullopt; + if (::flock(fd, LOCK_EX | LOCK_NB) != 0) { + ::close(fd); + return std::nullopt; + } + return FileLock{fd}; +} + +FileLock::~FileLock() { + if (fd_ >= 0) { + ::flock(fd_, LOCK_UN); + ::close(fd_); + } +} + +FileLock::FileLock(FileLock&& o) noexcept : fd_(o.fd_) { + o.fd_ = -1; +} + +FileLock& FileLock::operator=(FileLock&& o) noexcept { + if (this != &o) { + if (fd_ >= 0) { + ::flock(fd_, LOCK_UN); + ::close(fd_); + } + fd_ = o.fd_; + o.fd_ = -1; + } + return *this; +} + +#endif + +} // namespace mcpp::platform::fs diff --git a/src/platform/linux.cppm b/src/platform/linux.cppm new file mode 100644 index 0000000..f132a76 --- /dev/null +++ b/src/platform/linux.cppm @@ -0,0 +1,68 @@ +// mcpp.platform.linux — Linux-specific platform capabilities. +// +// Provides: +// build_ld_library_path() — construct LD_LIBRARY_PATH prefix +// runtime_lib_dirs() — Linux-specific library search paths + +module; + +export module mcpp.platform.linux; + +import std; +import mcpp.platform.shell; + +export namespace mcpp::platform::linux_ { + +// Build an LD_LIBRARY_PATH shell prefix from a list of directories. +// Returns "env LD_LIBRARY_PATH=:${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} " +// or "" if dirs is empty. +std::string build_ld_library_path_prefix( + const std::vector& dirs); + +// Return Linux-specific runtime library directories for LLVM toolchains. +std::vector +runtime_lib_dirs(const std::filesystem::path& toolchain_root); + +} // namespace mcpp::platform::linux_ + +// ─── Implementation ────────────────────────────────────────────────────── + +namespace mcpp::platform::linux_ { + +std::string build_ld_library_path_prefix( + const std::vector& dirs) +{ +#if defined(__linux__) + if (dirs.empty()) return ""; + std::string joined; + for (auto& d : dirs) { + if (!joined.empty()) joined += ':'; + joined += d.string(); + } + return std::format("env LD_LIBRARY_PATH={}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}} ", + mcpp::platform::shell::quote(joined)); +#else + (void)dirs; + return ""; +#endif +} + +std::vector +runtime_lib_dirs(const std::filesystem::path& toolchain_root) { + std::vector dirs; +#if defined(__linux__) + auto add = [&](const std::filesystem::path& p) { + if (std::filesystem::exists(p)) + dirs.push_back(p); + }; + add(toolchain_root / "lib" / "x86_64-unknown-linux-gnu"); + add("/lib/x86_64-linux-gnu"); + add("/usr/lib/x86_64-linux-gnu"); + add("/usr/lib64"); +#else + (void)toolchain_root; +#endif + return dirs; +} + +} // namespace mcpp::platform::linux_ diff --git a/src/platform/macos.cppm b/src/platform/macos.cppm new file mode 100644 index 0000000..32b7d19 --- /dev/null +++ b/src/platform/macos.cppm @@ -0,0 +1,102 @@ +// mcpp.platform.macos — macOS-specific platform capabilities. +// +// Provides: +// has_xcode_clt() — detect Xcode Command Line Tools +// sdk_path() — discover macOS SDK via xcrun +// runtime_lib_dirs() — macOS-specific library search paths +// supports_full_static — macOS cannot fully static-link (libSystem) + +module; +#include +#include +#if defined(_WIN32) +#define popen _popen +#define pclose _pclose +#endif + +export module mcpp.platform.macos; + +import std; + +export namespace mcpp::platform::macos { + +// Whether macOS supports full static linking (it does not — libSystem +// must be dynamically linked). +constexpr bool supports_full_static = false; + +// Check whether Xcode Command Line Tools are installed. +// Returns true if `xcode-select -p` succeeds. +bool has_xcode_clt(); + +// Discover the macOS SDK path via `xcrun --show-sdk-path`. +// Returns the SDK path if found, or nullopt. +std::optional sdk_path(); + +// Return macOS-specific runtime library directories for LLVM toolchains. +std::vector +runtime_lib_dirs(const std::filesystem::path& toolchain_root); + +} // namespace mcpp::platform::macos + +// ─── Implementation ────────────────────────────────────────────────────── + +namespace mcpp::platform::macos { + +namespace { + +std::string run_capture_trimmed(const std::string& cmd) { + std::array buf{}; + std::string out; +#if defined(__APPLE__) + std::string full = cmd + " (buf.size()), fp) != nullptr) + out += buf.data(); + ::pclose(fp); + while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' ')) + out.pop_back(); +#else + (void)cmd; + (void)buf; +#endif + return out; +} + +} // namespace + +bool has_xcode_clt() { +#if defined(__APPLE__) + int rc = std::system("xcode-select -p /dev/null 2>&1"); + return rc == 0; +#else + return false; +#endif +} + +std::optional sdk_path() { +#if defined(__APPLE__) + auto result = run_capture_trimmed("xcrun --show-sdk-path 2>/dev/null"); + if (!result.empty() && std::filesystem::exists(result)) + return std::filesystem::path(result); +#endif + return std::nullopt; +} + +std::vector +runtime_lib_dirs(const std::filesystem::path& toolchain_root) { + std::vector dirs; +#if defined(__APPLE__) + auto add = [&](const std::filesystem::path& p) { + if (std::filesystem::exists(p)) + dirs.push_back(p); + }; + add(toolchain_root / "lib" / "aarch64-apple-darwin"); + add(toolchain_root / "lib" / "darwin"); +#else + (void)toolchain_root; +#endif + return dirs; +} + +} // namespace mcpp::platform::macos diff --git a/src/platform/platform.cppm b/src/platform/platform.cppm new file mode 100644 index 0000000..9dc40c2 --- /dev/null +++ b/src/platform/platform.cppm @@ -0,0 +1,23 @@ +// mcpp.platform — unified platform abstraction facade. +// +// Import this single module to get access to all platform capabilities. +// Re-exports every sub-module so consumers can write: +// +// import mcpp.platform; +// // then use mcpp::platform::fs::self_exe_path(), etc. +// +// Platform-specific modules (macos, linux, windows) are always compiled +// on all platforms but their functions are no-ops on non-matching +// platforms, so consumers can call them without #ifdef guards. + +export module mcpp.platform; + +export import mcpp.platform.common; +export import mcpp.platform.shell; +export import mcpp.platform.process; +export import mcpp.platform.fs; +export import mcpp.platform.env; +export import mcpp.platform.macos; +export import mcpp.platform.linux; +export import mcpp.platform.windows; +export import mcpp.platform.terminal; diff --git a/src/platform/process.cppm b/src/platform/process.cppm new file mode 100644 index 0000000..23323e5 --- /dev/null +++ b/src/platform/process.cppm @@ -0,0 +1,197 @@ +// mcpp.platform.process — platform-aware process runner. +// +// Centralises all popen/system usage so callers do not scatter #if _WIN32 +// guards or duplicate the popen-read loop. On POSIX, all functions +// automatically redirect stdin from /dev/null to prevent interactive +// prompts from child processes (fixes macOS first-run hangs where xcrun +// or xcode-select would block waiting for user input). +// +// Entry points: +// capture — run a command, capture stdout +// run_silent — run a command, discard output +// run_streaming — run a command, stream stdout line by line +// +// NOTE: These functions run commands through the platform shell (sh/cmd.exe). +// Callers are responsible for shell-quoting arguments (see platform.shell). + +module; +#include +#include +#if defined(_WIN32) +#include // _putenv_s +#define popen _popen +#define pclose _pclose +#endif + +export module mcpp.platform.process; + +import std; + +export namespace mcpp::platform::process { + +struct RunResult { + int exit_code = 0; + std::string output; +}; + +// Run `command` via the platform shell, capture stdout. +// On POSIX, stdin is automatically redirected from /dev/null. +RunResult capture(std::string_view command); + +// Run `command` with extra environment variables (additive). +// Windows: _putenv_s (mutates calling process env). +// POSIX: prefixes command with VAR=val tokens (no mutation). +RunResult capture_with_env( + std::string_view command, + const std::vector>& env); + +// Run `command` silently (discard stdout/stderr). +// On POSIX, stdin is automatically redirected from /dev/null. +int run_silent(std::string_view command); + +// Run `command`, stream stdout line-by-line via callback. +// On POSIX, stdin is automatically redirected from /dev/null. +int run_streaming(std::string_view command, + std::function on_line); + +// Run `command`, passing stdout/stderr through to the terminal. +// Optionally captures stdout into `output` if non-null. +// Returns a platform-normalized exit code (WEXITSTATUS on POSIX). +int run_passthrough(std::string_view command, + std::string* output = nullptr); + +// Extract a platform-normalized exit code from a raw system()/pclose() +// return value. Windows returns the exit code directly; POSIX returns +// a wait-status word requiring WIFEXITED/WEXITSTATUS unwrapping. +int extract_exit_code(int raw_status); + +} // namespace mcpp::platform::process + +// ─── Implementation ────────────────────────────────────────────────────── + +namespace mcpp::platform::process { + +namespace { + +// On POSIX, append "< /dev/null" to prevent child processes from reading +// stdin. This fixes macOS first-run hangs where tools like xcrun or +// xcode-select block waiting for user input. +std::string seal_stdin(std::string_view cmd) { +#if defined(_WIN32) + return std::string(cmd); +#else + return std::string(cmd) + " buf{}; + while (std::fgets(buf.data(), static_cast(buf.size()), fp) != nullptr) + result.output += buf.data(); + + result.exit_code = normalize_exit_code(::pclose(fp)); + return result; +} + +RunResult capture_with_env( + std::string_view command, + const std::vector>& env) +{ +#if defined(_WIN32) + for (auto& [k, v] : env) + _putenv_s(k.c_str(), v.c_str()); + return capture(command); +#else + std::string prefixed; + for (auto& [k, v] : env) { + prefixed += k; + prefixed += '='; + // Simple quoting for env values + prefixed += '\''; + for (char c : v) { + if (c == '\'') prefixed += "'\\''"; + else prefixed += c; + } + prefixed += '\''; + prefixed += ' '; + } + prefixed += command; + return capture(prefixed); +#endif +} + +int run_silent(std::string_view command) { + auto cmd = seal_stdin(command); + return normalize_exit_code(std::system(cmd.c_str())); +} + +int run_streaming(std::string_view command, + std::function on_line) +{ + auto cmd = seal_stdin(command); + std::FILE* fp = ::popen(cmd.c_str(), "r"); + if (!fp) return -1; + + std::array buf{}; + std::string acc; + while (std::fgets(buf.data(), static_cast(buf.size()), fp) != nullptr) { + acc += buf.data(); + std::size_t pos; + while ((pos = acc.find('\n')) != std::string::npos) { + if (on_line) { + auto line = std::string_view{acc}.substr(0, pos); + while (!line.empty() && line.back() == '\r') + line.remove_suffix(1); + on_line(line); + } + acc.erase(0, pos + 1); + } + } + if (!acc.empty() && on_line) { + std::string_view line{acc}; + while (!line.empty() && line.back() == '\r') + line.remove_suffix(1); + if (!line.empty()) on_line(line); + } + return normalize_exit_code(::pclose(fp)); +} + +int run_passthrough(std::string_view command, std::string* output) { + auto cmd = seal_stdin(command); + std::FILE* fp = ::popen(cmd.c_str(), "r"); + if (!fp) return -1; + + std::array buf{}; + while (std::fgets(buf.data(), static_cast(buf.size()), fp) != nullptr) { + if (output) *output += buf.data(); + std::fputs(buf.data(), stdout); + } + return normalize_exit_code(::pclose(fp)); +} + +} // namespace mcpp::platform::process diff --git a/src/platform/shell.cppm b/src/platform/shell.cppm new file mode 100644 index 0000000..ef54624 --- /dev/null +++ b/src/platform/shell.cppm @@ -0,0 +1,57 @@ +// mcpp.platform.shell — platform-aware shell quoting and redirect helpers. +// +// Provides shell-safe argument quoting for command construction: +// POSIX: single-quote wrapping ('arg') +// Windows: double-quote wrapping ("arg") +// +// NOTE on Windows: do NOT use quote() for the FIRST token in a +// popen/system command string — cmd.exe strips a leading " pair. +// Use the raw path string as the first token; quote() is safe for +// arguments only. + +module; + +export module mcpp.platform.shell; + +import std; + +export namespace mcpp::platform::shell { + +// Platform-aware shell argument quoting. +std::string quote(std::string_view s); + +// Full silent redirect (stdin + stdout + stderr → /dev/null). +#if defined(_WIN32) +constexpr std::string_view silent_redirect = ">nul 2>&1"; +#else +constexpr std::string_view silent_redirect = ">/dev/null 2>&1"; +#endif + +} // namespace mcpp::platform::shell + +// ─── Implementation ────────────────────────────────────────────────────── + +namespace mcpp::platform::shell { + +std::string quote(std::string_view s) { + std::string out; + out.reserve(s.size() + 2); +#if defined(_WIN32) + 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; +} + +} // namespace mcpp::platform::shell diff --git a/src/platform/terminal.cppm b/src/platform/terminal.cppm new file mode 100644 index 0000000..0459afc --- /dev/null +++ b/src/platform/terminal.cppm @@ -0,0 +1,52 @@ +// mcpp.platform.terminal — terminal capability detection. +// +// Provides: +// is_tty() — whether stdout is a terminal +// terminal_cols() — terminal width in columns + +module; +#include +#include +#ifdef __unix__ +#include +#include +#endif + +export module mcpp.platform.terminal; + +import std; + +export namespace mcpp::platform::terminal { + +// Returns true if stdout is connected to a terminal (TTY). +bool is_tty(); + +// Returns the terminal width in columns. Tries TIOCGWINSZ on Unix, +// falls back to $COLUMNS, then defaults to 80. +std::size_t cols(); + +} // namespace mcpp::platform::terminal + +namespace mcpp::platform::terminal { + +bool is_tty() { +#ifdef __unix__ + return ::isatty(::fileno(stdout)) != 0; +#else + return false; +#endif +} + +std::size_t cols() { +#ifdef __unix__ + struct winsize w{}; + if (::ioctl(::fileno(stdout), TIOCGWINSZ, &w) == 0 && w.ws_col > 0) + return w.ws_col; +#endif + if (auto* e = std::getenv("COLUMNS"); e && *e) { + try { auto n = std::stoul(e); if (n > 0) return n; } catch (...) {} + } + return 80; +} + +} // namespace mcpp::platform::terminal diff --git a/src/platform/windows.cppm b/src/platform/windows.cppm new file mode 100644 index 0000000..265e727 --- /dev/null +++ b/src/platform/windows.cppm @@ -0,0 +1,41 @@ +// mcpp.platform.windows — Windows-specific platform capabilities. +// +// Provides: +// prepend_path() — add a directory to the front of %PATH% +// +// Note: Visual Studio / MSVC discovery is in mcpp.toolchain.msvc, which is +// the authoritative module for MSVC toolchain detection. This module +// provides general-purpose Windows platform utilities only. + +module; +#include +#if defined(_WIN32) +#include // _putenv_s +#endif + +export module mcpp.platform.windows; + +import std; + +export namespace mcpp::platform::windows { + +// Prepend a directory to the %PATH% environment variable. +void prepend_path(const std::filesystem::path& dir); + +} // namespace mcpp::platform::windows + +// ─── Implementation ────────────────────────────────────────────────────── + +namespace mcpp::platform::windows { + +void prepend_path(const std::filesystem::path& dir) { +#if defined(_WIN32) + std::string newPath = dir.string() + ";" + + (std::getenv("PATH") ? std::getenv("PATH") : ""); + _putenv_s("PATH", newPath.c_str()); +#else + (void)dir; +#endif +} + +} // namespace mcpp::platform::windows diff --git a/src/pm/publisher.cppm b/src/pm/publisher.cppm index 56b4217..6eee15d 100644 --- a/src/pm/publisher.cppm +++ b/src/pm/publisher.cppm @@ -3,17 +3,13 @@ // See docs/04-schema-xpkg-extension.md for the produced layout. module; -#include // popen / pclose / fgets -#if defined(_WIN32) -#define popen _popen -#define pclose _pclose -#endif export module mcpp.pm.publisher; import std; import mcpp.manifest; import mcpp.modgraph.graph; +import mcpp.platform; export namespace mcpp::pm { @@ -208,19 +204,14 @@ std::string release_tarball_url(std::string_view repo, std::string sha256_of_file(const std::filesystem::path& file) { if (!std::filesystem::exists(file)) return {}; - auto cmd = std::format("sha256sum '{}' 2>/dev/null", file.string()); - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return {}; - std::array buf{}; - std::string out; - while (std::fgets(buf.data(), buf.size(), fp)) - out += buf.data(); - int rc = ::pclose(fp); - if (rc != 0) return {}; + auto cmd = std::format("sha256sum {} 2>/dev/null", + mcpp::platform::shell::quote(file.string())); + auto r = mcpp::platform::process::capture(cmd); + if (r.exit_code != 0) return {}; // sha256sum format: "<64-hex> \n" - auto sp = out.find(' '); + auto sp = r.output.find(' '); if (sp == std::string::npos || sp != 64) return {}; - return out.substr(0, 64); + return r.output.substr(0, 64); } std::string make_release_tarball(const std::filesystem::path& root, @@ -231,21 +222,17 @@ std::string make_release_tarball(const std::filesystem::path& root, std::error_code ec; std::filesystem::create_directories(output.parent_path(), ec); + auto prefix = std::format("{}-{}/", name, version); auto cmd = std::format( - "git -C '{}' archive --format=tar.gz " - "--prefix='{}-{}/' " - "-o '{}' HEAD 2>&1", - root.string(), name, version, output.string()); - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return std::format("popen failed for git archive: {}", cmd); - - std::array buf{}; - std::string err; - while (std::fgets(buf.data(), buf.size(), fp)) - err += buf.data(); - int rc = ::pclose(fp); - if (rc != 0) { - return std::format("git archive failed (rc={}): {}", rc, err); + "git -C {} archive --format=tar.gz " + "--prefix={} " + "-o {} HEAD 2>&1", + mcpp::platform::shell::quote(root.string()), + mcpp::platform::shell::quote(prefix), + mcpp::platform::shell::quote(output.string())); + auto r = mcpp::platform::process::capture(cmd); + if (r.exit_code != 0) { + return std::format("git archive failed (rc={}): {}", r.exit_code, r.output); } if (!std::filesystem::exists(output)) { return std::format("git archive exited 0 but no tarball at '{}'", diff --git a/src/pm/resolver.cppm b/src/pm/resolver.cppm index bc92bc7..6fbe466 100644 --- a/src/pm/resolver.cppm +++ b/src/pm/resolver.cppm @@ -17,6 +17,7 @@ export module mcpp.pm.resolver; import std; import mcpp.manifest; +import mcpp.platform; import mcpp.pm.compat; import mcpp.pm.package_fetcher; import mcpp.version_req; @@ -26,16 +27,7 @@ export namespace mcpp::pm { // xpkg.lua's `xpm.` uses these names. (Distinct from // `kCurrentPlatform` in cli.cppm, which is the [toolchain] table key — // "macos" vs "macosx".) -inline constexpr std::string_view kXpkgPlatform = -#if defined(__linux__) - "linux"; -#elif defined(__APPLE__) - "macosx"; -#elif defined(_WIN32) - "windows"; -#else - "linux"; -#endif +inline constexpr std::string_view kXpkgPlatform = mcpp::platform::xpkg_platform; // Returns true if `v` is a SemVer constraint (caret, tilde, range, glob, // `*`, or empty) rather than a literal exact version. Empty counts as diff --git a/src/process.cppm b/src/process.cppm deleted file mode 100644 index 43f8834..0000000 --- a/src/process.cppm +++ /dev/null @@ -1,145 +0,0 @@ -// 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 837475f..52d325f 100644 --- a/src/toolchain/clang.cppm +++ b/src/toolchain/clang.cppm @@ -230,21 +230,15 @@ std::vector std_module_build_commands(const Toolchain& tc, } 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 + auto llvmAr = tc.binaryPath.parent_path() / + (std::string("llvm-ar") + std::string(mcpp::platform::exe_suffix)); 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 + auto p = tc.binaryPath.parent_path() / + (std::string("clang-scan-deps") + std::string(mcpp::platform::exe_suffix)); if (std::filesystem::exists(p)) return p; return std::nullopt; } diff --git a/src/toolchain/msvc.cppm b/src/toolchain/msvc.cppm index 967acb3..ffe786c 100644 --- a/src/toolchain/msvc.cppm +++ b/src/toolchain/msvc.cppm @@ -12,16 +12,12 @@ // foundation for future native MSVC (cl.exe) toolchain support. module; -#include #include -#if defined(_WIN32) -#define popen _popen -#define pclose _pclose -#endif export module mcpp.toolchain.msvc; import std; +import mcpp.platform; export namespace mcpp::toolchain::msvc { @@ -47,13 +43,8 @@ namespace { // Run a command and capture stdout (first line, trimmed). std::string run_capture_line(const std::string& cmd) { - std::array buf{}; - std::string out; - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return {}; - while (std::fgets(buf.data(), buf.size(), fp) != nullptr) - out += buf.data(); - ::pclose(fp); + auto r = mcpp::platform::process::capture(cmd); + auto& out = r.output; // Trim trailing whitespace/newlines while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' ')) out.pop_back(); diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index 8724b6f..b356775 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -8,12 +8,7 @@ // the mcpp.process module. module; -#include // popen, pclose, fgets, FILE #include // getenv -#if defined(_WIN32) -#define popen _popen -#define pclose _pclose -#endif export module mcpp.toolchain.probe; @@ -78,33 +73,21 @@ 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 + return mcpp::platform::linux_::build_ld_library_path_prefix(dirs); } } // namespace std::expected run_capture(const std::string& cmd) { - std::array buf{}; - std::string out; - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) { + auto r = mcpp::platform::process::capture(cmd); + if (r.exit_code != 0 && r.output.empty()) { return std::unexpected(DetectError{std::format("failed to execute: {}", cmd)}); } - while (std::fgets(buf.data(), buf.size(), fp) != nullptr) out += buf.data(); - int rc = ::pclose(fp); - if (rc != 0) { + if (r.exit_code != 0) { return std::unexpected(DetectError{ - std::format("'{}' exited with status {}", cmd, rc)}); + std::format("'{}' exited with status {}", cmd, r.exit_code)}); } - return out; + return r.output; } std::string extract_version(std::string_view s) { @@ -193,15 +176,10 @@ discover_compiler_runtime_dirs(const std::filesystem::path& compilerBin) { || exe.find("clang") != std::string::npos; if (looksLikeLlvm) { append_existing_unique(dirs, root / "lib"); -#if defined(__linux__) - append_existing_unique(dirs, root / "lib" / "x86_64-unknown-linux-gnu"); - append_existing_unique(dirs, "/lib/x86_64-linux-gnu"); - append_existing_unique(dirs, "/usr/lib/x86_64-linux-gnu"); - append_existing_unique(dirs, "/usr/lib64"); -#elif defined(__APPLE__) - append_existing_unique(dirs, root / "lib" / "aarch64-apple-darwin"); - append_existing_unique(dirs, root / "lib" / "darwin"); -#endif + for (auto& d : mcpp::platform::linux_::runtime_lib_dirs(root)) + append_existing_unique(dirs, d); + for (auto& d : mcpp::platform::macos::runtime_lib_dirs(root)) + append_existing_unique(dirs, d); } if (auto rt = mcpp::xlings::paths::find_sibling_tool(compilerBin, "gcc-runtime")) { @@ -218,20 +196,18 @@ discover_link_runtime_dirs(const std::filesystem::path& compilerBin, auto root = compilerBin.parent_path().parent_path(); if (!targetTriple.empty()) append_existing_unique(dirs, root / "lib" / std::string(targetTriple)); -#if defined(__linux__) - append_existing_unique(dirs, root / "lib" / "x86_64-unknown-linux-gnu"); -#elif defined(__APPLE__) - append_existing_unique(dirs, root / "lib" / "aarch64-apple-darwin"); - append_existing_unique(dirs, root / "lib" / "darwin"); -#endif + for (auto& d : mcpp::platform::linux_::runtime_lib_dirs(root)) + append_existing_unique(dirs, d); + for (auto& d : mcpp::platform::macos::runtime_lib_dirs(root)) + append_existing_unique(dirs, d); append_existing_unique(dirs, root / "lib"); -#if defined(__linux__) - if (auto rt = mcpp::xlings::paths::find_sibling_tool(compilerBin, "gcc-runtime")) { - append_existing_unique(dirs, *rt / "lib64"); - append_existing_unique(dirs, *rt / "lib"); + if constexpr (mcpp::platform::is_linux) { + if (auto rt = mcpp::xlings::paths::find_sibling_tool(compilerBin, "gcc-runtime")) { + append_existing_unique(dirs, *rt / "lib64"); + append_existing_unique(dirs, *rt / "lib"); + } } -#endif return dirs; } @@ -257,22 +233,11 @@ probe_compiler_binary(const std::filesystem::path& explicit_compiler) { cxx = "g++"; } -#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) { + auto found = mcpp::platform::fs::which(cxx); + if (!found) { return std::unexpected(DetectError{std::format("compiler '{}' not found in PATH", cxx)}); } - // `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)}); - } - return std::filesystem::path(bin); + return *found; } std::expected @@ -297,15 +262,9 @@ probe_sysroot(const std::filesystem::path& compilerBin, 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(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; - } -#endif + if (auto sdk = mcpp::platform::macos::sdk_path()) + return *sdk; return {}; } diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 8701fed..34a1d06 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -1,10 +1,5 @@ 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. // @@ -28,6 +23,7 @@ module; export module mcpp.toolchain.stdmod; import std; +import mcpp.platform; import mcpp.toolchain.clang; import mcpp.toolchain.detect; import mcpp.toolchain.gcc; @@ -59,20 +55,12 @@ namespace mcpp::toolchain { namespace { std::expected run_capture_command(const std::string& cmd) { - std::array buf{}; - std::string out; - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) { + auto r = mcpp::platform::process::capture(cmd); + if (r.exit_code != 0) { return std::unexpected(StdModError{ - std::format("failed to spawn compiler: {}", cmd)}); + std::format("std module precompile failed (rc={}):\n{}", r.exit_code, r.output)}); } - while (std::fgets(buf.data(), buf.size(), fp) != nullptr) out += buf.data(); - int rc = ::pclose(fp); - if (rc != 0) { - return std::unexpected(StdModError{ - std::format("std module precompile failed (rc={}):\n{}", rc, out)}); - } - return out; + return r.output; } } // namespace diff --git a/src/ui.cppm b/src/ui.cppm index 8389c09..0c2a997 100644 --- a/src/ui.cppm +++ b/src/ui.cppm @@ -4,15 +4,12 @@ // here. TTY auto-detect; MCPP_NO_COLOR / --no-color disables colors. module; -#include // isatty, fileno, stdout -#ifdef __unix__ -#include -#include -#endif +#include // fileno, stdout export module mcpp.ui; import std; +import mcpp.platform; export namespace mcpp::ui { @@ -146,11 +143,7 @@ constexpr std::string_view kBrightRed = "\033[91m"; bool detect_color() { if (auto* e = std::getenv("MCPP_NO_COLOR"); e && *e == '1') return false; if (auto* e = std::getenv("NO_COLOR"); e && *e) return false; -#ifdef __unix__ - return ::isatty(::fileno(stdout)) != 0; -#else - return false; -#endif + return mcpp::platform::terminal::is_tty(); } std::string with_color(std::string_view code, std::string_view text) { @@ -326,15 +319,7 @@ std::string fmt_bytes(std::size_t b) { // rather collapse the bar than wrap into a second row that `\r\033[2K` // can't clean up later. std::size_t terminal_cols() { -#ifdef __unix__ - struct winsize w{}; - if (::ioctl(::fileno(stdout), TIOCGWINSZ, &w) == 0 && w.ws_col > 0) - return w.ws_col; -#endif - if (auto* e = std::getenv("COLUMNS"); e && *e) { - try { auto n = std::stoul(e); if (n > 0) return n; } catch (...) {} - } - return 80; + return mcpp::platform::terminal::cols(); } // Truncate a "visible" string (no ANSI codes inside) to `max` chars, replacing diff --git a/src/xlings.cppm b/src/xlings.cppm index 7d5c8af..b7faf06 100644 --- a/src/xlings.cppm +++ b/src/xlings.cppm @@ -8,18 +8,14 @@ // `mcpp.pm.compat`. It must NOT import mcpp.config or any other mcpp module. module; -#include +#include // stderr #include -#if defined(_WIN32) -#include // _putenv_s -#define popen _popen -#define pclose _pclose -#endif export module mcpp.xlings; import std; import mcpp.pm.compat; +import mcpp.platform; export namespace mcpp::xlings { @@ -305,41 +301,16 @@ struct LineScan { // ─── run_capture ──────────────────────────────────────────────────── std::expected run_capture(const std::string& cmd) { - std::array buf{}; - std::string out; - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return std::unexpected("popen failed: " + cmd); - while (std::fgets(buf.data(), buf.size(), fp) != nullptr) out += buf.data(); - int rc = ::pclose(fp); - if (rc != 0 && out.empty()) return std::unexpected("command failed: " + cmd); - return out; + auto r = mcpp::platform::process::capture(cmd); + if (r.exit_code != 0 && r.output.empty()) + return std::unexpected("command failed: " + cmd); + return r.output; } // ─── Shell quoting ────────────────────────────────────────────────── 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; + return mcpp::platform::shell::quote(s); } // ─── Path helpers ─────────────────────────────────────────────────── @@ -427,47 +398,40 @@ 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) - _putenv_s("XLINGS_HOME", env.home.string().c_str()); - _putenv_s("XLINGS_PROJECT_DIR", - env.projectDir.empty() ? "" : env.projectDir.string().c_str()); - { - std::string newPath = xvmBin + ";" + (std::getenv("PATH") ? std::getenv("PATH") : ""); - _putenv_s("PATH", newPath.c_str()); - } - return env.binary.string(); -#else - if (env.projectDir.empty()) { - // Global mode: unset XLINGS_PROJECT_DIR (existing behavior). + if constexpr (mcpp::platform::is_windows) { + mcpp::platform::env::set("XLINGS_HOME", env.home.string()); + mcpp::platform::env::set("XLINGS_PROJECT_DIR", + env.projectDir.empty() ? "" : env.projectDir.string()); + mcpp::platform::windows::prepend_path(xvmBin); + return env.binary.string(); + } else { + if (env.projectDir.empty()) { + // Global mode: unset XLINGS_PROJECT_DIR (existing behavior). + return std::format( + "cd {} && env -u XLINGS_PROJECT_DIR PATH={}:\"$PATH\" XLINGS_HOME={} {}", + shq(env.home.string()), + shq(xvmBin), + shq(env.home.string()), + shq(env.binary.string())); + } + // Project-level mode: set XLINGS_PROJECT_DIR so xlings uses + // additive project repos alongside global repos. return std::format( - "cd {} && env -u XLINGS_PROJECT_DIR PATH={}:\"$PATH\" XLINGS_HOME={} {}", + "cd {} && env PATH={}:\"$PATH\" XLINGS_HOME={} XLINGS_PROJECT_DIR={} {}", shq(env.home.string()), shq(xvmBin), shq(env.home.string()), + shq(env.projectDir.string()), shq(env.binary.string())); } - // Project-level mode: set XLINGS_PROJECT_DIR so xlings uses - // additive project repos alongside global repos. - return std::format( - "cd {} && env PATH={}:\"$PATH\" XLINGS_HOME={} XLINGS_PROJECT_DIR={} {}", - shq(env.home.string()), - shq(xvmBin), - shq(env.home.string()), - shq(env.projectDir.string()), - shq(env.binary.string())); -#endif } std::string build_interface_command(const Env& env, std::string_view capability, std::string_view argsJson) { -#if defined(_WIN32) - return std::format("{} interface {} --args {} 2>nul", - build_command_prefix(env), capability, shq(argsJson)); -#else - return std::format("{} interface {} --args {} 2>/dev/null", - build_command_prefix(env), capability, shq(argsJson)); -#endif + return std::format("{} interface {} --args {} {}", + build_command_prefix(env), capability, shq(argsJson), + mcpp::platform::null_redirect); } // ─── JSON extraction helpers ──────────────────────────────────────── @@ -606,24 +570,13 @@ call(const Env& env, std::string_view capability, { auto cmd = build_interface_command(env, capability, argsJson); - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return std::unexpected( - std::format("failed to spawn xlings: {}", cmd)); - CallResult result; - std::array buf{}; - std::string acc; - while (std::fgets(buf.data(), buf.size(), fp) != nullptr) { - acc += buf.data(); - std::size_t pos; - while ((pos = acc.find('\n')) != std::string::npos) { - auto line = acc.substr(0, pos); - acc.erase(0, pos + 1); - while (!line.empty() && line.back() == '\r') line.pop_back(); - if (line.empty()) continue; + int rc = mcpp::platform::process::run_streaming(cmd, + [&](std::string_view line) { + if (line.empty()) return; auto ev = parse_event_line(line); - if (!ev) continue; + if (!ev) return; std::visit([&](auto&& e) { using T = std::decay_t; @@ -643,9 +596,7 @@ call(const Env& env, std::string_view capability, if (handler) handler->on_result(e); } }, *ev); - } - } - int rc = ::pclose(fp); + }); if (rc != 0 && result.exitCode == 0) result.exitCode = rc; return result; } @@ -658,36 +609,37 @@ 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", ""); - std::error_code ec_mkdir; - std::filesystem::create_directories(env.home, ec_mkdir); - // Use direct `install` command instead of `interface install_packages` - // on Windows. The NDJSON interface may have issues with large packages - // where the extraction subprocess doesn't respect XLINGS_HOME. - auto directCmd = std::format("{} install {} -y", - env.binary.string(), target); - int directRc = std::system(directCmd.c_str()); - if (directRc == 0) return 0; - // Fallback to interface path if direct install fails - 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; - - std::array buf{}; - std::string acc; + if constexpr (mcpp::platform::is_windows) { + mcpp::platform::env::set("XLINGS_HOME", env.home.string()); + mcpp::platform::env::set("XLINGS_PROJECT_DIR", ""); + std::error_code ec_mkdir; + std::filesystem::create_directories(env.home, ec_mkdir); + // Use direct `install` command instead of `interface install_packages` + // on Windows. The NDJSON interface may have issues with large packages + // where the extraction subprocess doesn't respect XLINGS_HOME. + auto directCmd = std::format("{} install {} -y", + env.binary.string(), target); + int directRc = mcpp::platform::process::run_silent(directCmd); + if (directRc == 0) return 0; + } + auto cmd = [&]() -> std::string { + if constexpr (mcpp::platform::is_windows) { + // Fallback to interface path if direct install fails + return std::format("{} interface install_packages --args {} {}", + env.binary.string(), + shq(argsJson), + mcpp::platform::null_redirect); + } else { + return std::format( + "cd {} && env -u XLINGS_PROJECT_DIR XLINGS_HOME={} {} interface install_packages --args {} {}", + shq(env.home.string()), + shq(env.home.string()), + shq(env.binary.string()), + shq(argsJson), + mcpp::platform::null_redirect); + } + }(); + int resultExitCode = -1; auto handle_line = [&](std::string_view line) { @@ -739,16 +691,7 @@ int install_with_progress(const Env& env, std::string_view target, if (!prog.files.empty()) cb(prog); }; - while (std::fgets(buf.data(), buf.size(), fp)) { - acc += buf.data(); - std::size_t pos; - while ((pos = acc.find('\n')) != std::string::npos) { - handle_line(std::string_view{acc}.substr(0, pos)); - acc.erase(0, pos + 1); - } - } - if (!acc.empty()) handle_line(acc); - int closeRc = ::pclose(fp); + int closeRc = mcpp::platform::process::run_streaming(cmd, handle_line); return (resultExitCode != -1) ? resultExitCode : closeRc; } @@ -775,7 +718,7 @@ void seed_xlings_json(const Env& env, int config_show(const Env& env) { auto cmd = std::format("{} config", build_command_prefix(env)); - return std::system(cmd.c_str()); + return mcpp::platform::process::run_silent(cmd); } int config_set_mirror(const Env& env, std::string_view mirror, bool quiet) { @@ -784,12 +727,8 @@ int config_set_mirror(const Env& env, std::string_view mirror, bool quiet) { "{} config --mirror {} {}", build_command_prefix(env), shq(mirror), -#if defined(_WIN32) - quiet ? ">nul 2>&1" : ""); -#else - quiet ? ">/dev/null 2>&1" : ""); -#endif - return std::system(cmd.c_str()); + quiet ? mcpp::platform::shell::silent_redirect : ""); + return mcpp::platform::process::run_silent(cmd); } void ensure_init(const Env& env, bool quiet) { @@ -802,18 +741,21 @@ void ensure_init(const Env& env, bool quiet) { 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()); + std::string cmd; + if constexpr (mcpp::platform::is_windows) { + mcpp::platform::env::set("XLINGS_HOME", env.home.string()); + mcpp::platform::env::set("XLINGS_PROJECT_DIR", ""); + cmd = env.binary.string() + " self init " + + std::string(mcpp::platform::shell::silent_redirect); + } else { + cmd = std::format( + "cd {} && env -u XLINGS_PROJECT_DIR XLINGS_HOME={} {} self init {}", + shq(env.home.string()), + shq(env.home.string()), + shq(env.binary.string()), + mcpp::platform::shell::silent_redirect); + } + int rc = mcpp::platform::process::run_silent(cmd); if (rc != 0 && !quiet) { std::println(stderr, "warning: `xlings self init` failed for sandbox at '{}'", @@ -845,12 +787,9 @@ void ensure_ninja(const Env& env, bool quiet, auto root = paths::xim_tool_root(env, "ninja"); if (std::filesystem::exists(root)) { std::error_code ec; + auto ninja_name = std::string("ninja") + std::string(mcpp::platform::exe_suffix); 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 (std::filesystem::exists(v.path() / ninja_name)) return; } } if (!quiet) @@ -893,13 +832,10 @@ bool is_index_fresh(const Env& env, std::int64_t ttlSeconds) { int update_index(const Env& env, bool quiet) { std::string cmd = build_command_prefix(env) + " update 2>&1"; - std::array buf{}; - std::FILE* fp = ::popen(cmd.c_str(), "r"); - if (!fp) return -1; - while (std::fgets(buf.data(), buf.size(), fp)) { - if (!quiet) std::fputs(buf.data(), stdout); - } - return ::pclose(fp); + return mcpp::platform::process::run_streaming(cmd, + [quiet](std::string_view line) { + if (!quiet) std::println("{}", line); + }); } void ensure_index_fresh(const Env& env, std::int64_t ttlSeconds, bool quiet) {