Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
072c25a
release: v0.0.17 — macOS self-host via xlings mcpp@0.0.16
Sunrisepeak May 16, 2026
f9d8238
fix: macOS link strategy — no sysroot for linker + 3-step self-host CI
Sunrisepeak May 16, 2026
df2c214
fix: ensure mcpp is on PATH for self-host dyndep calls
Sunrisepeak May 16, 2026
fcc851a
fix: use absolute path for bootstrap mcpp binary
Sunrisepeak May 16, 2026
579815b
fix: macOS link needs both no-sysroot AND explicit -lc++
Sunrisepeak May 16, 2026
f487838
fix: macOS link uses system libc++ (not xlings LLVM's)
Sunrisepeak May 16, 2026
e1deb37
fix: preserve v2 binary before rm -rf target in step 3
Sunrisepeak May 16, 2026
c981cb6
fix: revert .xlings.json mcpp version to 0.0.9
Sunrisepeak May 16, 2026
3a50d90
fix: pass MCPP_VENDORED_XLINGS in Linux CI build step
Sunrisepeak May 16, 2026
89a968a
refactor: macOS CI/release uses xlings mcpp (same pattern as Linux)
Sunrisepeak May 17, 2026
385bfab
fix: bust xlings cache (v3) to pick up new mcpp macosx pkg
Sunrisepeak May 17, 2026
a3acd03
fix: set GLOBAL mirror before mcpp test (gtest download on GH runners)
Sunrisepeak May 17, 2026
d71b427
fix: bust Linux xlings cache to pick up mcpp@0.0.16
Sunrisepeak May 17, 2026
a213d9f
fix: use freshly-built mcpp for test step (bootstrap lacks --mirror)
Sunrisepeak May 17, 2026
82fa56d
feat: macOS CI aligned with Linux — adds mcpp test + E2E suite
Sunrisepeak May 17, 2026
d650695
fix: use _NSGetExecutablePath on macOS (no /proc/self/exe)
Sunrisepeak May 17, 2026
2b8f674
fix: macOS E2E — skip Linux-specific tests + fix portability issues
Sunrisepeak May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions .github/workflows/ci-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
15 changes: 11 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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: |
Expand Down
59 changes: 27 additions & 32 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -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/
Expand All @@ -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
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
14 changes: 14 additions & 0 deletions src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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<llvm>/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;
}
Expand Down
13 changes: 13 additions & 0 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
module;
#include <cstdio>
#include <cstdlib>
#if defined(__APPLE__)
#include <mach-o/dyld.h> // _NSGetExecutablePath
#endif

export module mcpp.build.ninja;

Expand Down Expand Up @@ -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
}

Expand Down
11 changes: 11 additions & 0 deletions src/config.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
module;
#include <cstdio>
#include <cstdlib>
#if defined(__APPLE__)
#include <mach-o/dyld.h> // _NSGetExecutablePath
#endif

export module mcpp.config;

Expand Down Expand Up @@ -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/<triple>/<fp>/bin/<exe>,
// matching the bin/ shape. Any ancestor literally named
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/05_errors.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/09_path_dependency.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
[package]
name = "myappB"
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/22_doctor_cache_publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ out=$("$MCPP" cache list 2>&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]
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/24_git_dependency.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"* ]] || {
Expand Down
6 changes: 3 additions & 3 deletions tests/e2e/25_convention_mode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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; }
Expand Down Expand Up @@ -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; }
Expand Down
Loading
Loading