diff --git a/.github/workflows/bootstrap-macos.yml b/.github/workflows/bootstrap-macos.yml new file mode 100644 index 0000000..18c13c9 --- /dev/null +++ b/.github/workflows/bootstrap-macos.yml @@ -0,0 +1,138 @@ +name: bootstrap-macos + +# One-shot workflow to produce the first macOS mcpp binary. +# Uses xmake + xlings LLVM to compile mcpp from source. +# Once a macOS binary exists, mcpp can self-host for future releases. + +on: + workflow_dispatch: + +jobs: + bootstrap: + name: Bootstrap mcpp (macOS ARM64) + runs-on: macos-15 + timeout-minutes: 30 + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '0.4.30' + steps: + - uses: actions/checkout@v4 + + - name: System info + run: | + uname -a + sw_vers + xcrun --show-sdk-path + + - name: Install xlings + run: | + WORK=$(mktemp -d) + tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" + curl -fsSL -o "${WORK}/${tarball}" \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" + tar -xzf "${WORK}/${tarball}" -C "${WORK}" + "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install + echo "$HOME/.xlings/subos/default/bin" >> "$GITHUB_PATH" + echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" + + - name: Install LLVM + xmake + run: | + xlings install llvm -y || xlings install llvm@20.1.7 -y + brew install xmake + 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" + "$LLVM_ROOT/bin/clang++" --version + xmake --version + + - name: Build mcpp with xmake + run: | + # Generate xmake.lua if not present + if [ ! -f xmake.lua ]; then + cat > xmake.lua << 'EOF' + add_rules("mode.release") + set_languages("c++23") + + package("cmdline") + set_homepage("https://github.com/mcpplibs/cmdline") + set_description("Modern C++ command-line parsing library") + set_license("Apache-2.0") + add_urls("https://github.com/mcpplibs/cmdline/archive/refs/tags/$(version).tar.gz") + add_versions("0.0.1", "3fb2f5495c1a144485b3cbb2e43e27059151633460f702af0f3851cbff387ef0") + on_install(function (package) + import("package.tools.xmake").install(package) + end) + package_end() + + add_requires("cmdline 0.0.1") + + target("mcpp") + set_kind("binary") + add_files("src/main.cpp") + add_files("src/**.cppm") + add_packages("cmdline") + add_includedirs("src/libs/json") + set_policy("build.c++.modules", true) + EOF + fi + + # Configure with xlings LLVM + xmake f -y -m release --toolchain=llvm --sdk="$LLVM_ROOT" + # Build + xmake build -y mcpp + + - name: Verify built binary + run: | + MCPP=$(find build -name mcpp -type f -perm +111 | head -1) + test -x "$MCPP" + file "$MCPP" + "$MCPP" --version + echo "MCPP=$MCPP" >> "$GITHUB_ENV" + + - name: Package + id: package + run: | + VERSION=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) + TARBALL="mcpp-${VERSION}-darwin-arm64.tar.gz" + WRAPPER="mcpp-${VERSION}-darwin-arm64" + + mkdir -p "dist/$WRAPPER/bin" + cp "$MCPP" "dist/$WRAPPER/bin/mcpp" + strip "dist/$WRAPPER/bin/mcpp" 2>/dev/null || true + cp LICENSE "dist/$WRAPPER/" 2>/dev/null || true + cp README.md "dist/$WRAPPER/" 2>/dev/null || true + + cat > "dist/$WRAPPER/mcpp" << 'LAUNCHER' + #!/bin/sh + exec "$(dirname "$0")/bin/mcpp" "$@" + LAUNCHER + chmod +x "dist/$WRAPPER/mcpp" + + # Bundle xlings + XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" + if [ -x "$XLINGS_BIN" ]; then + mkdir -p "dist/$WRAPPER/registry/bin" + cp "$XLINGS_BIN" "dist/$WRAPPER/registry/bin/xlings" + fi + + (cd dist && tar -czf "$TARBALL" "$WRAPPER") + (cd dist && shasum -a 256 "$TARBALL" > "$TARBALL.sha256") + + echo "tarball=dist/$TARBALL" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + ls -la dist/ + + - name: Smoke test + run: | + SMOKE=$(mktemp -d) + tar -xzf "${{ steps.package.outputs.tarball }}" -C "$SMOKE" + VERSION="${{ steps.package.outputs.version }}" + "$SMOKE/mcpp-${VERSION}-darwin-arm64/bin/mcpp" --version + "$SMOKE/mcpp-${VERSION}-darwin-arm64/mcpp" --version + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: mcpp-darwin-arm64 + path: | + dist/mcpp-*-darwin-arm64.tar.gz + dist/mcpp-*-darwin-arm64.tar.gz.sha256 diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 8be8c15..55c57e4 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -267,3 +267,24 @@ jobs: Darwin-x86_64) echo "PASS: would select darwin-x86_64" ;; *) echo "FAIL: unexpected platform"; exit 1 ;; esac + + - name: Install xmake (for bootstrap) + run: | + brew install xmake + xmake --version + + - name: Bootstrap mcpp from source (xmake) + run: | + export LLVM_ROOT="$LLVM_ROOT" + bash scripts/bootstrap-macos.sh "$LLVM_ROOT" + ./target/bootstrap/bin/mcpp --version + + - name: Self-host (mcpp builds mcpp) + 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!" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8437df2..d7cce14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -255,3 +255,146 @@ jobs: dist/SHA256SUMS dist/mcpp-${{ steps.stage.outputs.version }}.tar.gz dist/mcpp.lua + + build-macos: + name: build (macOS / ARM64) + runs-on: macos-15 + needs: build-release + permissions: + contents: write + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve tag + id: resolve + run: | + if [ "${{ github.event_name }}" = "push" ]; then + TAG="${{ github.ref_name }}" + elif [ -n "${{ github.event.inputs.tag }}" ]; then + TAG="${{ github.event.inputs.tag }}" + else + VER=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) + TAG="v$VER" + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + if [ "${{ github.event_name }}" = "workflow_dispatch" ] \ + && git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then + git checkout --detach "refs/tags/$TAG" + fi + + - name: Cache xlings + uses: actions/cache@v4 + with: + path: ~/.xlings + key: xlings-macos15-release-${{ hashFiles('.xlings.json') }} + restore-keys: | + xlings-macos15-release- + xlings-macos15-arm64- + + - name: Bootstrap xlings + LLVM + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '0.4.30' + run: | + if [ ! -x "$HOME/.xlings/subos/default/bin/xlings" ]; then + WORK=$(mktemp -d) + tarball="xlings-${XLINGS_VERSION}-macosx-arm64.tar.gz" + curl -fsSL -o "${WORK}/${tarball}" \ + "https://github.com/d2learn/xlings/releases/download/v${XLINGS_VERSION}/${tarball}" + tar -xzf "${WORK}/${tarball}" -C "${WORK}" + "${WORK}/xlings-${XLINGS_VERSION}-macosx-arm64/subos/default/bin/xlings" self install + 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 + + - name: Self-host rebuild (mcpp builds mcpp) + 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" + + - 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" + + # Create release layout + STAGING=$(mktemp -d) + mkdir -p "$STAGING/$WRAPPER/bin" + cp "$SELFHOST" "$STAGING/$WRAPPER/bin/mcpp" + # Strip (Mach-O) + strip "$STAGING/$WRAPPER/bin/mcpp" 2>/dev/null || true + # Copy metadata + cp LICENSE "$STAGING/$WRAPPER/" 2>/dev/null || true + cp README.md "$STAGING/$WRAPPER/" 2>/dev/null || true + + # Shell launcher (same as Linux) + cat > "$STAGING/$WRAPPER/mcpp" << 'LAUNCHER' + #!/bin/sh + exec "$(dirname "$0")/bin/mcpp" "$@" + LAUNCHER + chmod +x "$STAGING/$WRAPPER/mcpp" + + # Bundle xlings for install.sh consumers + XLINGS_BIN="$HOME/.xlings/subos/default/bin/xlings" + if [ -x "$XLINGS_BIN" ]; then + mkdir -p "$STAGING/$WRAPPER/registry/bin" + cp "$XLINGS_BIN" "$STAGING/$WRAPPER/registry/bin/xlings" + chmod +x "$STAGING/$WRAPPER/registry/bin/xlings" + fi + + # Create tarball + 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" + # 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") + + echo "tarball=${TARBALL_NAME}" >> "$GITHUB_OUTPUT" + ls -la dist/ + + - name: Smoke-test the tarball + run: | + VERSION="${{ steps.resolve.outputs.version }}" + TARBALL_NAME="${{ steps.stage.outputs.tarball }}" + WRAPPER="${TARBALL_NAME%.tar.gz}" + SMOKE=$(mktemp -d) + tar -xzf "dist/${TARBALL_NAME}" -C "$SMOKE" + "$SMOKE/$WRAPPER/bin/mcpp" --version + "$SMOKE/$WRAPPER/mcpp" --version | grep -q "$VERSION" + + - name: Upload macOS artifacts to release + uses: softprops/action-gh-release@v2 + 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 diff --git a/mcpp.toml b/mcpp.toml index ff1b9fb..5985952 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -13,6 +13,7 @@ include_dirs = ["src/libs/json"] [toolchain] default = "gcc@16.1.0" +macos = "llvm@20.1.7" # Per-target overrides: `mcpp build --target x86_64-linux-musl` (or the # four-segment form `x86_64-unknown-linux-musl`) picks musl-gcc 15.1 + full diff --git a/scripts/bootstrap-macos.sh b/scripts/bootstrap-macos.sh new file mode 100755 index 0000000..a0d6a28 --- /dev/null +++ b/scripts/bootstrap-macos.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# scripts/bootstrap-macos.sh — bootstrap mcpp on macOS via xmake. +# +# Uses xmake (which has mature C++23 module support) to compile mcpp +# from source on macOS. This is the "Plan B" bootstrap: xmake handles +# module dependency scanning and compilation ordering automatically. +# +# Prerequisites: +# - xmake (brew install xmake or https://xmake.io) +# - Clang 20+ (xlings LLVM or Homebrew LLVM) +# - macOS SDK (xcode-select --install) +# +# Usage: +# ./scripts/bootstrap-macos.sh [LLVM_ROOT] +# +# Output: +# target/bootstrap/bin/mcpp +# +set -euo pipefail + +PROJROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJROOT" + +# ─── Locate LLVM ──────────────────────────────────────────────────────────── + +if [ -n "${1:-}" ] && [ -d "$1/bin" ]; then + LLVM_ROOT="$1" +elif [ -n "${LLVM_ROOT:-}" ]; then + : # already set via env +elif [ -d "$HOME/.xlings/data/xpkgs/xim-x-llvm" ]; then + LLVM_ROOT=$(find "$HOME/.xlings/data/xpkgs/xim-x-llvm" -maxdepth 1 -type d | sort -V | tail -1) +elif command -v brew >/dev/null 2>&1 && [ -d "$(brew --prefix llvm)/bin" ]; then + LLVM_ROOT=$(brew --prefix llvm) +else + echo "error: cannot find LLVM. Pass LLVM_ROOT or install via xlings/Homebrew." >&2 + exit 1 +fi + +CXX="$LLVM_ROOT/bin/clang++" +echo ":: LLVM_ROOT = $LLVM_ROOT" +"$CXX" --version | head -1 + +# ─── Ensure xmake is available ─────────────────────────────────────────────── + +if ! command -v xmake >/dev/null 2>&1; then + echo ":: Installing xmake via Homebrew..." + brew install xmake +fi +echo ":: xmake $(xmake --version | head -1)" + +# ─── Ensure xmake.lua exists ──────────────────────────────────────────────── + +if [ ! -f "$PROJROOT/xmake.lua" ]; then + echo ":: Generating xmake.lua for bootstrap..." + cat > "$PROJROOT/xmake.lua" << 'XMAKE' +-- Bootstrap xmake.lua for mcpp (macOS) +-- This file is auto-generated by scripts/bootstrap-macos.sh + +add_rules("mode.release") +set_languages("c++23") + +package("cmdline") + set_homepage("https://github.com/mcpplibs/cmdline") + set_description("Modern C++ command-line parsing library") + set_license("Apache-2.0") + add_urls("https://github.com/mcpplibs/cmdline/archive/refs/tags/$(version).tar.gz") + add_versions("0.0.1", "3fb2f5495c1a144485b3cbb2e43e27059151633460f702af0f3851cbff387ef0") + on_install(function (package) + import("package.tools.xmake").install(package) + end) +package_end() + +add_requires("cmdline 0.0.1") + +target("mcpp") + set_kind("binary") + add_files("src/main.cpp") + add_files("src/**.cppm") + add_packages("cmdline") + add_includedirs("src/libs/json") + set_policy("build.c++.modules", true) +XMAKE +fi + +# ─── Build with xmake ──────────────────────────────────────────────────────── + +echo +echo ":: Building mcpp with xmake (LLVM/Clang toolchain)..." +echo + +# Configure xmake to use the specified LLVM toolchain +xmake f -y -m release \ + --toolchain=llvm \ + --sdk="$LLVM_ROOT" \ + 2>&1 | tail -5 + +# Build +xmake build -y mcpp 2>&1 + +# ─── Stage output ──────────────────────────────────────────────────────────── + +echo +echo ":: Staging output..." +OUTDIR="$PROJROOT/target/bootstrap/bin" +mkdir -p "$OUTDIR" + +# xmake puts output in build///release/mcpp +BUILT=$(find "$PROJROOT/build" -name mcpp -type f -perm +111 2>/dev/null | head -1) +if [ -z "$BUILT" ]; then + echo "error: mcpp binary not found in xmake build output" >&2 + find "$PROJROOT/build" -type f 2>/dev/null | head -20 + exit 1 +fi + +cp "$BUILT" "$OUTDIR/mcpp" +echo ":: SUCCESS: $OUTDIR/mcpp" +"$OUTDIR/mcpp" --version