diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 55c57e4..5dcec6a 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -33,9 +33,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.xlings - key: xlings-macos15-arm64-${{ hashFiles('.xlings.json') }} + key: xlings-macos15-arm64-v3-${{ hashFiles('.xlings.json') }} restore-keys: | - xlings-macos15-arm64- + xlings-macos15-arm64-v3- - name: Bootstrap xlings env: @@ -268,23 +268,50 @@ jobs: *) echo "FAIL: unexpected platform"; exit 1 ;; esac - - name: Install xmake (for bootstrap) + - name: Bootstrap mcpp via xlings run: | - brew install xmake - xmake --version + # Same pattern as Linux CI: xlings install mcpp + xlings install mcpp -y + MCPP="$HOME/.xlings/subos/default/bin/mcpp" + test -x "$MCPP" + "$MCPP" --version + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" + + - name: Build mcpp from source (self-host) + run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP" build - - name: Bootstrap mcpp from source (xmake) + - name: Unit + integration tests via `mcpp test` run: | - export LLVM_ROOT="$LLVM_ROOT" - bash scripts/bootstrap-macos.sh "$LLVM_ROOT" - ./target/bootstrap/bin/mcpp --version + # Use freshly-built mcpp (has --mirror support) + MCPP=$(find target -path "*/bin/mcpp" | head -1) + MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") + "$MCPP" self config --mirror GLOBAL + "$MCPP" test - - name: Self-host (mcpp builds mcpp) + - name: E2E suite + run: | + MCPP=$(find target -path "*/bin/mcpp" | head -1) + MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") + test -x "$MCPP" + export MCPP + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + test -x "$MCPP_VENDORED_XLINGS" + export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL + "$MCPP" self config --mirror "$MCPP_E2E_TOOLCHAIN_MIRROR" + "$MCPP" self config + # macOS default toolchain is LLVM + "$MCPP" toolchain default llvm@20.1.7 + bash tests/e2e/run_all.sh + + - name: Self-host smoke (freshly-built mcpp builds itself again) run: | - # Put bootstrapped mcpp on PATH so build.ninja can find it for dyndep - export PATH="$PWD/target/bootstrap/bin:$PATH" - mcpp build - SELFHOST=$(find target -path "*/bin/mcpp" -not -path "*/bootstrap/*" -not -path "*/build/*" | head -1) - test -x "$SELFHOST" - "$SELFHOST" --version - echo ":: Self-host successful!" + MCPP=$(find target -path "*/bin/mcpp" | head -1) + MCPP=$(cd "$(dirname "$MCPP")" && pwd)/$(basename "$MCPP") + test -x "$MCPP" + export PATH="$(dirname "$MCPP"):$PATH" + "$MCPP" build + "$MCPP" --version + echo ":: Self-host smoke PASS" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc881f5..89fcf68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,9 +49,9 @@ jobs: uses: actions/cache@v4 with: path: ~/.xlings - key: xlings-${{ runner.os }}-${{ hashFiles('.xlings.json') }} + key: xlings-${{ runner.os }}-v2-${{ hashFiles('.xlings.json') }} restore-keys: | - xlings-${{ runner.os }}- + xlings-${{ runner.os }}-v2- - name: Bootstrap mcpp via xlings env: @@ -88,13 +88,20 @@ jobs: restore-keys: | mcpp-target-${{ runner.os }}- - - name: Build mcpp from source (self-host) + - name: Configure mirror + Build mcpp from source (self-host) run: | + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + # Set GLOBAL mirror via xlings directly (bootstrap mcpp may lack --mirror flag) + "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true + "$MCPP" self config --mirror GLOBAL 2>/dev/null || true "$MCPP" build - name: Unit + integration tests via `mcpp test` run: | - "$MCPP" test + # Use freshly-built mcpp for test (it has --mirror support) + MCPP_FRESH=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)") + "$MCPP_FRESH" self config --mirror GLOBAL + "$MCPP_FRESH" test - name: E2E suite run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7cce14..41055f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -295,7 +295,7 @@ jobs: xlings-macos15-release- xlings-macos15-arm64- - - name: Bootstrap xlings + LLVM + - name: Bootstrap mcpp via xlings env: XLINGS_NON_INTERACTIVE: '1' XLINGS_VERSION: '0.4.30' @@ -310,42 +310,37 @@ jobs: fi export PATH="$HOME/.xlings/subos/default/bin:$PATH" xlings --version - # Install LLVM - xlings install llvm -y || xlings install llvm@20.1.7 -y - LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname) - echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV" - - - name: Install xmake (for bootstrap) - run: brew install xmake - - - name: Bootstrap-compile mcpp (xmake + LLVM) - run: | - export LLVM_ROOT="$LLVM_ROOT" - bash scripts/bootstrap-macos.sh "$LLVM_ROOT" - ./target/bootstrap/bin/mcpp --version + xlings install mcpp -y + MCPP="$HOME/.xlings/subos/default/bin/mcpp" + test -x "$MCPP" + "$MCPP" --version + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + echo "XLINGS_BIN=$HOME/.xlings/subos/default/bin/xlings" >> "$GITHUB_ENV" - - name: Self-host rebuild (mcpp builds mcpp) + - name: Build mcpp from source (self-host) run: | - # Put bootstrapped mcpp on PATH so build.ninja can find it for dyndep - export PATH="$PWD/target/bootstrap/bin:$PATH" - mcpp build - # Find the self-hosted binary - SELFHOST=$(find target -path "*/bin/mcpp" -not -path "*/bootstrap/*" -not -path "*/build/*" | head -1) - test -x "$SELFHOST" - "$SELFHOST" --version - echo "SELFHOST=$SELFHOST" >> "$GITHUB_ENV" + export PATH="$HOME/.xlings/subos/default/bin:$PATH" + export MCPP_VENDORED_XLINGS="$XLINGS_BIN" + "$MCPP" build + MCPP_BIN=$(find target -path "*/bin/mcpp" | head -1) + MCPP_BIN=$(cd "$(dirname "$MCPP_BIN")" && pwd)/$(basename "$MCPP_BIN") + test -x "$MCPP_BIN" + file "$MCPP_BIN" + otool -L "$MCPP_BIN" + "$MCPP_BIN" --version + echo "MCPP_BIN=$MCPP_BIN" >> "$GITHUB_ENV" - name: Package macOS release id: stage run: | VERSION="${{ steps.resolve.outputs.version }}" - TARBALL_NAME="mcpp-${VERSION}-darwin-arm64.tar.gz" - WRAPPER="mcpp-${VERSION}-darwin-arm64" + TARBALL_NAME="mcpp-${VERSION}-macosx-arm64.tar.gz" + WRAPPER="mcpp-${VERSION}-macosx-arm64" # Create release layout STAGING=$(mktemp -d) mkdir -p "$STAGING/$WRAPPER/bin" - cp "$SELFHOST" "$STAGING/$WRAPPER/bin/mcpp" + cp "$MCPP_BIN" "$STAGING/$WRAPPER/bin/mcpp" # Strip (Mach-O) strip "$STAGING/$WRAPPER/bin/mcpp" 2>/dev/null || true # Copy metadata @@ -371,10 +366,10 @@ jobs: mkdir -p dist (cd "$STAGING" && tar -czf "$GITHUB_WORKSPACE/dist/${TARBALL_NAME}" "$WRAPPER") # Versionless alias - cp "dist/${TARBALL_NAME}" "dist/mcpp-darwin-arm64.tar.gz" + cp "dist/${TARBALL_NAME}" "dist/mcpp-macosx-arm64.tar.gz" # SHA256 (cd dist && shasum -a 256 "${TARBALL_NAME}" > "${TARBALL_NAME}.sha256") - (cd dist && shasum -a 256 "mcpp-darwin-arm64.tar.gz" > "mcpp-darwin-arm64.tar.gz.sha256") + (cd dist && shasum -a 256 "mcpp-macosx-arm64.tar.gz" > "mcpp-macosx-arm64.tar.gz.sha256") echo "tarball=${TARBALL_NAME}" >> "$GITHUB_OUTPUT" ls -la dist/ @@ -394,7 +389,7 @@ jobs: with: tag_name: ${{ steps.resolve.outputs.tag }} files: | - dist/mcpp-${{ steps.resolve.outputs.version }}-darwin-arm64.tar.gz - dist/mcpp-${{ steps.resolve.outputs.version }}-darwin-arm64.tar.gz.sha256 - dist/mcpp-darwin-arm64.tar.gz - dist/mcpp-darwin-arm64.tar.gz.sha256 + dist/mcpp-${{ steps.resolve.outputs.version }}-macosx-arm64.tar.gz + dist/mcpp-${{ steps.resolve.outputs.version }}-macosx-arm64.tar.gz.sha256 + dist/mcpp-macosx-arm64.tar.gz + dist/mcpp-macosx-arm64.tar.gz.sha256 diff --git a/mcpp.toml b/mcpp.toml index 5985952..06c67ae 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.16" +version = "0.0.17" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 72066e5..992f091 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -161,8 +161,22 @@ CompileFlags compute_flags(const BuildPlan& plan) { runtime_dirs += " -L" + escape_path(dir); runtime_dirs += " -Wl,-rpath," + escape_path(dir); } + +#if 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 de886b2..5266fee 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -14,6 +14,9 @@ module; #include #include +#if defined(__APPLE__) +#include // _NSGetExecutablePath +#endif export module mcpp.build.ninja; @@ -113,9 +116,19 @@ bool dyndep_mode_enabled() { std::filesystem::path mcpp_exe_path() { std::error_code ec; +#if defined(__APPLE__) + // macOS: use _NSGetExecutablePath + 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 } diff --git a/src/config.cppm b/src/config.cppm index 347cffd..d53d531 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -16,6 +16,9 @@ module; #include #include +#if defined(__APPLE__) +#include // _NSGetExecutablePath +#endif export module mcpp.config; @@ -161,7 +164,15 @@ std::filesystem::path home_dir() { return std::filesystem::path(e); std::error_code ec; +#if 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") { // Dev builds emit binaries at target///bin/, // matching the bin/ shape. Any ancestor literally named diff --git a/tests/e2e/05_errors.sh b/tests/e2e/05_errors.sh index db65eca..9ffd970 100755 --- a/tests/e2e/05_errors.sh +++ b/tests/e2e/05_errors.sh @@ -60,7 +60,7 @@ out=$("$MCPP" build 2>&1) && { echo "expected failure"; exit 1; } cd "$TMP" "$MCPP" new naming-ok > /dev/null cd naming-ok -sed -i 's/name = "naming-ok"/name = "myorg.something"/' mcpp.toml +sed -i.bak 's/name = "naming-ok"/name = "myorg.something"/' mcpp.toml && rm -f mcpp.toml.bak cat > src/foo.cppm <<'EOF' export module differentprefix; import std; diff --git a/tests/e2e/09_path_dependency.sh b/tests/e2e/09_path_dependency.sh index 9d542a8..059fe5f 100755 --- a/tests/e2e/09_path_dependency.sh +++ b/tests/e2e/09_path_dependency.sh @@ -81,7 +81,7 @@ grep -q 'mcpp.cache/mylibA.greet.gcm\|gcm.cache/mylibA.greet.gcm' "$ninja_file" # Path-resolution error reporting: declared name mismatch TMP2=$(mktemp -d) cp -r "$TMP/mylibA" "$TMP2/wrongname" -sed -i 's/name = "mylibA"/name = "differentname"/' "$TMP2/wrongname/mcpp.toml" +sed -i.bak 's/name = "mylibA"/name = "differentname"/' "$TMP2/wrongname/mcpp.toml" && rm -f "$TMP2/wrongname/mcpp.toml.bak" cat > mcpp.toml <&1) cd "$TMP" "$MCPP" new myapp >/dev/null cd myapp -sed -i 's|^repo[[:space:]]*=.*|repo = "https://github.com/example/myapp"|' mcpp.toml +sed -i.bak 's|^repo[[:space:]]*=.*|repo = "https://github.com/example/myapp"|' mcpp.toml && rm -f mcpp.toml.bak grep -q '^repo' mcpp.toml || cat >> mcpp.toml <<'EOF' [package] diff --git a/tests/e2e/24_git_dependency.sh b/tests/e2e/24_git_dependency.sh index 9ce3521..4b0f7eb 100755 --- a/tests/e2e/24_git_dependency.sh +++ b/tests/e2e/24_git_dependency.sh @@ -72,7 +72,7 @@ rev = "$HEAD_REV" EOF "$MCPP" build > build.log 2>&1 -triple=$(ls -d target/x86_64-linux-*/ | head -1) +triple=$(ls -d target/*/ | head -1) fp_dir=$(ls "$triple") out=$(${triple}${fp_dir}/bin/myapp) [[ "$out" == *"hello from git dep"* ]] || { diff --git a/tests/e2e/25_convention_mode.sh b/tests/e2e/25_convention_mode.sh index 1fee3de..1f9d724 100755 --- a/tests/e2e/25_convention_mode.sh +++ b/tests/e2e/25_convention_mode.sh @@ -27,7 +27,7 @@ grep -q 'Inferred sources \[src/\*\*' build.log || { cat build.lo grep -q 'Inferred target tinyapp (bin from src/main.cpp)' build.log || { cat build.log; echo "FAIL: no Inferred target line"; exit 1; } grep -q 'Compiling tinyapp v0.1.0' build.log || { cat build.log; echo "FAIL: not compiling"; exit 1; } -triple=$(ls -d target/x86_64-linux-*/ | head -1) +triple=$(ls -d target/*/ | head -1) fp_dir=$(ls "$triple") out=$("${triple}${fp_dir}/bin/tinyapp") [[ "$out" == "convention-mode bin OK" ]] || { echo "FAIL: runtime out='$out'"; exit 1; } @@ -52,7 +52,7 @@ EOF "$MCPP" build > build.log 2>&1 grep -q 'Inferred include_dirs \[include\]' build.log || { cat build.log; echo "FAIL: include/ not auto-picked"; exit 1; } -triple=$(ls -d target/x86_64-linux-*/ | head -1) +triple=$(ls -d target/*/ | head -1) fp_dir=$(ls "$triple") out=$("${triple}${fp_dir}/bin/inc") [[ "$out" == "answer = 42" ]] || { echo "FAIL: include resolution: $out"; exit 1; } @@ -101,7 +101,7 @@ EOF if grep -q 'Inferred' build.log; then cat build.log; echo "FAIL: legacy schema fired Inferred banner unexpectedly"; exit 1 fi -triple=$(ls -d target/x86_64-linux-*/ | head -1) +triple=$(ls -d target/*/ | head -1) fp_dir=$(ls "$triple") out=$("${triple}${fp_dir}/bin/legacy") [[ "$out" == "legacy schema OK" ]] || { echo "FAIL: legacy runtime: $out"; exit 1; } diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index 337e0c1..ada5c7e 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -31,13 +31,58 @@ if [[ -z "${MCPP_HOME:-}" ]]; then fi echo "MCPP_HOME: $MCPP_HOME" +# Platform detection: some tests are Linux-only (ELF patchelf, musl-static, +# GCC-specific BMI layout, etc.) +OS="$(uname -s)" +MACOS_SKIP=( + # GCC-specific BMI assertions (gcm.cache/*.gcm) + 03_multi_module.sh + # Static library test checks ELF ar output format + 07_static_library.sh + # Shared library test hardcodes .so / ELF shared object + 08_shared_library.sh + # Path dependency checks .gcm BMI format (GCC-specific) + 09_path_dependency.sh + # Pack modes use patchelf (ELF-only) + 30_pack_modes.sh + # Toolchain management tests assume GCC availability + 26_toolchain_management.sh + 29_toolchain_partial_versions.sh + # P1689 scanner test hardcodes GCC ddi format + 20_p1689_scanner.sh + # Ninja dyndep test hardcodes GCC module format + 21_ninja_dyndep.sh + # Doctor/cache/publish uses GCC fingerprint + 22_doctor_cache_publish.sh + # Self-contained home test assumes Linux sandbox layout + 27_self_contained_home.sh + # Multi-version mangling test uses GCC module format + 33_multi_version_mangling.sh +) + +should_skip() { + local name="$1" + if [[ "$OS" == "Darwin" ]]; then + for skip in "${MACOS_SKIP[@]}"; do + [[ "$name" == "$skip" ]] && return 0 + done + fi + return 1 +} + PASS=0 FAIL=0 +SKIP=0 FAILED_TESTS=() for test in "$HERE"/[0-9]*.sh; do name="$(basename "$test")" echo + if should_skip "$name"; then + echo "SKIP: $name (not applicable on $OS)" + ((SKIP++)) + continue + fi echo "=== $name ===" if MCPP="$MCPP" bash "$test"; then echo "PASS: $name" @@ -51,7 +96,7 @@ done echo echo "===============================================" -echo "E2E Summary: $PASS passed, $FAIL failed" +echo "E2E Summary: $PASS passed, $FAIL failed, $SKIP skipped" if [[ $FAIL -gt 0 ]]; then echo "Failed: ${FAILED_TESTS[@]}" exit 1