Skip to content

Commit 4955291

Browse files
committed
refactor: use xmake for macOS bootstrap (replaces manual module compilation)
xmake has mature C++23 module support and handles dependency scanning, topological ordering, and partition compilation automatically. This is far more reliable than the manual regex-based approach. Based on github.com/Sunrisepeak/mcpp-dev xmake.lua pattern.
1 parent c5e63cf commit 4955291

3 files changed

Lines changed: 82 additions & 251 deletions

File tree

.github/workflows/ci-macos.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,12 @@ jobs:
268268
*) echo "FAIL: unexpected platform"; exit 1 ;;
269269
esac
270270
271-
- name: Bootstrap mcpp from source
271+
- name: Install xmake (for bootstrap)
272+
run: |
273+
brew install xmake
274+
xmake --version
275+
276+
- name: Bootstrap mcpp from source (xmake)
272277
run: |
273278
export LLVM_ROOT="$LLVM_ROOT"
274279
bash scripts/bootstrap-macos.sh "$LLVM_ROOT"

.github/workflows/release.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -315,14 +315,13 @@ jobs:
315315
LLVM_ROOT=$(find "$HOME/.xlings" -path "*/xpkgs/xim-x-llvm/*/bin/clang++" | head -1 | xargs dirname | xargs dirname)
316316
echo "LLVM_ROOT=$LLVM_ROOT" >> "$GITHUB_ENV"
317317
318-
- name: Bootstrap-compile mcpp
319-
env:
320-
PROJROOT: ${{ github.workspace }}
318+
- name: Install xmake (for bootstrap)
319+
run: brew install xmake
320+
321+
- name: Bootstrap-compile mcpp (xmake + LLVM)
321322
run: |
322323
export LLVM_ROOT="$LLVM_ROOT"
323-
export CXX="$LLVM_ROOT/bin/clang++"
324324
bash scripts/bootstrap-macos.sh "$LLVM_ROOT"
325-
# Verify
326325
./target/bootstrap/bin/mcpp --version
327326
328327
- name: Self-host rebuild (mcpp builds mcpp)

scripts/bootstrap-macos.sh

Lines changed: 72 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#!/usr/bin/env bash
2-
# scripts/bootstrap-macos.sh — one-shot bootstrap of mcpp on macOS.
2+
# scripts/bootstrap-macos.sh — bootstrap mcpp on macOS via xmake.
33
#
4-
# Compiles mcpp from source using upstream LLVM/Clang with C++23 modules.
5-
# Only needed ONCE to produce the first macOS binary; afterwards mcpp
6-
# can self-host via `mcpp build`.
4+
# Uses xmake (which has mature C++23 module support) to compile mcpp
5+
# from source on macOS. This is the "Plan B" bootstrap: xmake handles
6+
# module dependency scanning and compilation ordering automatically.
77
#
88
# Prerequisites:
9-
# - Clang 20+ with libc++ module support (xlings LLVM or Homebrew LLVM)
9+
# - xmake (brew install xmake or https://xmake.io)
10+
# - Clang 20+ (xlings LLVM or Homebrew LLVM)
1011
# - macOS SDK (xcode-select --install)
11-
# - Python 3 (ships with macOS)
1212
#
1313
# Usage:
1414
# ./scripts/bootstrap-macos.sh [LLVM_ROOT]
@@ -18,6 +18,9 @@
1818
#
1919
set -euo pipefail
2020

21+
PROJROOT="$(cd "$(dirname "$0")/.." && pwd)"
22+
cd "$PROJROOT"
23+
2124
# ─── Locate LLVM ────────────────────────────────────────────────────────────
2225

2326
if [ -n "${1:-}" ] && [ -d "$1/bin" ]; then
@@ -35,256 +38,80 @@ fi
3538

3639
CXX="$LLVM_ROOT/bin/clang++"
3740
echo ":: LLVM_ROOT = $LLVM_ROOT"
38-
echo ":: CXX = $CXX"
3941
"$CXX" --version | head -1
4042

41-
# ─── Locate macOS SDK ────────────────────────────────────────────────────────
43+
# ─── Ensure xmake is available ───────────────────────────────────────────────
4244

43-
SDKROOT=$(xcrun --show-sdk-path 2>/dev/null || true)
44-
if [ -z "$SDKROOT" ]; then
45-
echo "error: macOS SDK not found. Run: xcode-select --install" >&2
46-
exit 1
45+
if ! command -v xmake >/dev/null 2>&1; then
46+
echo ":: Installing xmake via Homebrew..."
47+
brew install xmake
4748
fi
48-
echo ":: SDKROOT = $SDKROOT"
49-
50-
# ─── Locate std.cppm ────────────────────────────────────────────────────────
51-
52-
STD_CPPM=$(find "$LLVM_ROOT" -name "std.cppm" -path "*/libc++/*" | head -1)
53-
if [ -z "$STD_CPPM" ] || [ ! -f "$STD_CPPM" ]; then
54-
echo "error: std.cppm not found in LLVM installation" >&2
55-
exit 1
49+
echo ":: xmake $(xmake --version | head -1)"
50+
51+
# ─── Ensure xmake.lua exists ────────────────────────────────────────────────
52+
53+
if [ ! -f "$PROJROOT/xmake.lua" ]; then
54+
echo ":: Generating xmake.lua for bootstrap..."
55+
cat > "$PROJROOT/xmake.lua" << 'XMAKE'
56+
-- Bootstrap xmake.lua for mcpp (macOS)
57+
-- This file is auto-generated by scripts/bootstrap-macos.sh
58+
59+
add_rules("mode.release")
60+
set_languages("c++23")
61+
62+
package("cmdline")
63+
set_homepage("https://github.com/mcpplibs/cmdline")
64+
set_description("Modern C++ command-line parsing library")
65+
set_license("Apache-2.0")
66+
add_urls("https://github.com/mcpplibs/cmdline/archive/refs/tags/$(version).tar.gz")
67+
add_versions("0.0.1", "3fb2f5495c1a144485b3cbb2e43e27059151633460f702af0f3851cbff387ef0")
68+
on_install(function (package)
69+
import("package.tools.xmake").install(package)
70+
end)
71+
package_end()
72+
73+
add_requires("cmdline 0.0.1")
74+
75+
target("mcpp")
76+
set_kind("binary")
77+
add_files("src/main.cpp")
78+
add_files("src/**.cppm")
79+
add_packages("cmdline")
80+
add_includedirs("src/libs/json")
81+
set_policy("build.c++.modules", true)
82+
XMAKE
5683
fi
57-
echo ":: std.cppm = $STD_CPPM"
5884

59-
STD_COMPAT_CPPM=$(find "$LLVM_ROOT" -name "std.compat.cppm" -path "*/libc++/*" | head -1)
60-
echo ":: std.compat= ${STD_COMPAT_CPPM:-(not found)}"
61-
62-
# ─── Setup ───────────────────────────────────────────────────────────────────
63-
64-
PROJROOT="$(cd "$(dirname "$0")/.." && pwd)"
65-
OUTDIR="$PROJROOT/target/bootstrap"
66-
PCMDIR="$OUTDIR/pcm.cache"
67-
OBJDIR="$OUTDIR/obj"
68-
BINDIR="$OUTDIR/bin"
69-
DEPSDIR="$OUTDIR/deps"
70-
mkdir -p "$PCMDIR" "$OBJDIR" "$BINDIR" "$DEPSDIR"
71-
72-
# ─── Fetch dependencies ─────────────────────────────────────────────────────
73-
echo ":: Fetching dependencies"
74-
# mcpplibs.cmdline — small cmdline parsing library
75-
CMDLINE_URL="https://github.com/mcpplibs/cmdline/archive/refs/tags/0.0.1.tar.gz"
76-
if [ ! -d "$DEPSDIR/cmdline" ]; then
77-
curl -fsSL "$CMDLINE_URL" | tar -xz -C "$DEPSDIR"
78-
mv "$DEPSDIR/cmdline-0.0.1" "$DEPSDIR/cmdline"
79-
fi
80-
echo " mcpplibs.cmdline at: $DEPSDIR/cmdline/src/"
81-
82-
# Export for Python
83-
export PROJROOT OUTDIR PCMDIR OBJDIR BINDIR DEPSDIR CXX LLVM_ROOT STD_CPPM STD_COMPAT_CPPM SDKROOT
85+
# ─── Build with xmake ────────────────────────────────────────────────────────
8486

8587
echo
86-
echo ":: Compiling mcpp (42 modules + main.cpp)..."
88+
echo ":: Building mcpp with xmake (LLVM/Clang toolchain)..."
8789
echo
8890

89-
# ─── All-in-one Python build script ─────────────────────────────────────────
90-
python3 << 'PYTHON'
91-
import os, re, sys, subprocess
92-
from pathlib import Path
93-
from collections import defaultdict
94-
95-
projroot = Path(os.environ["PROJROOT"])
96-
outdir = Path(os.environ["OUTDIR"])
97-
pcmdir = Path(os.environ["PCMDIR"])
98-
objdir = Path(os.environ["OBJDIR"])
99-
bindir = Path(os.environ["BINDIR"])
100-
depsdir = Path(os.environ["DEPSDIR"])
101-
cxx = os.environ["CXX"]
102-
sdkroot = os.environ["SDKROOT"]
103-
std_cppm = os.environ["STD_CPPM"]
104-
std_compat = os.environ.get("STD_COMPAT_CPPM", "")
105-
106-
# Base flags — check if clang++.cfg already sets sysroot
107-
result = subprocess.run([cxx, "-v", "-x", "c++", "/dev/null", "-c", "-o", "/dev/null"],
108-
capture_output=True, text=True, timeout=10)
109-
has_sysroot = "sysroot" in (result.stdout + result.stderr)
110-
cxxflags = f"-std=c++23 -O2 -I{projroot}/src/libs/json"
111-
if not has_sysroot:
112-
cxxflags += f" --sysroot={sdkroot}"
113-
114-
def run(cmd, desc=""):
115-
rc = os.system(cmd)
116-
if rc != 0:
117-
print(f"\nFAILED ({desc}):\n {cmd}", file=sys.stderr)
118-
sys.exit(1)
119-
120-
# ─── Phase 1: Pre-compile std module ────────────────────────────────────────
121-
print(":: Phase 1: Pre-compile std + std.compat")
122-
run(f'{cxx} {cxxflags} -Wno-reserved-module-identifier --precompile "{std_cppm}" -o "{pcmdir}/std.pcm"',
123-
"precompile std")
124-
run(f'{cxx} {cxxflags} -Wno-reserved-module-identifier "{pcmdir}/std.pcm" -c -o "{objdir}/std.o"',
125-
"compile std.o")
126-
127-
all_objs = [str(objdir / "std.o")]
128-
std_mod_flags = f'-fmodule-file=std="{pcmdir}/std.pcm"'
129-
130-
if std_compat and os.path.isfile(std_compat):
131-
run(f'{cxx} {cxxflags} -Wno-reserved-module-identifier {std_mod_flags} --precompile "{std_compat}" -o "{pcmdir}/std.compat.pcm"',
132-
"precompile std.compat")
133-
run(f'{cxx} {cxxflags} -Wno-reserved-module-identifier {std_mod_flags} "{pcmdir}/std.compat.pcm" -c -o "{objdir}/std.compat.o"',
134-
"compile std.compat.o")
135-
all_objs.append(str(objdir / "std.compat.o"))
136-
std_mod_flags += f' -fmodule-file=std.compat="{pcmdir}/std.compat.pcm"'
137-
138-
# ─── Phase 2: Scan module declarations from source ──────────────────────────
139-
print("\n:: Phase 2: Scanning module declarations")
140-
141-
# Regex patterns for module declarations in the module declaration region
142-
re_export = re.compile(r'^\s*export\s+module\s+([\w.:]+)\s*;')
143-
re_import = re.compile(r'^\s*(?:export\s+)?import\s+([\w.:]+)\s*;')
144-
re_module = re.compile(r'^\s*module\s+([\w.:]+)\s*;') # module implementation unit
145-
146-
# Include both project sources and dependency sources
147-
sources = sorted(projroot.glob("src/**/*.cppm")) + sorted(projroot.glob("src/**/*.cpp"))
148-
# Add dependency modules (mcpplibs.cmdline)
149-
dep_sources = sorted(depsdir.glob("cmdline/src/*.cppm"))
150-
sources = dep_sources + sources # deps first so they're available
91+
# Configure xmake to use the specified LLVM toolchain
92+
xmake f -y -m release \
93+
--toolchain=llvm \
94+
--sdk="$LLVM_ROOT" \
95+
2>&1 | tail -5
15196

152-
# Map: module_name -> source_path
153-
mod_source = {}
154-
# Map: source_path -> [module_names_it_provides]
155-
src_provides = {}
156-
# Map: source_path -> [module_names_it_requires] (excluding std/std.compat)
157-
src_requires = {}
97+
# Build
98+
xmake build -y mcpp 2>&1
15899

159-
for src in sources:
160-
provides = []
161-
requires = []
162-
current_module = None # track which module this file belongs to (for partition resolution)
163-
try:
164-
with open(src, 'r') as f:
165-
for line in f:
166-
line = line.strip()
167-
if line.startswith('//') or line.startswith('#') or not line:
168-
continue
169-
m = re_export.match(line)
170-
if m:
171-
mod_name = m.group(1)
172-
provides.append(mod_name)
173-
# Track the base module name for partition resolution
174-
if ':' in mod_name:
175-
current_module = mod_name.split(':')[0]
176-
else:
177-
current_module = mod_name
178-
continue
179-
m = re_module.match(line)
180-
if m and not provides: # module implementation unit
181-
mod_name = m.group(1)
182-
if ':' in mod_name:
183-
current_module = mod_name.split(':')[0]
184-
else:
185-
current_module = mod_name
186-
requires.append(mod_name)
187-
continue
188-
m = re_import.match(line)
189-
if m:
190-
mod_name = m.group(1)
191-
# Handle partition imports: `import :options;` → `<current_module>:options`
192-
if mod_name.startswith(':') and current_module:
193-
mod_name = current_module + mod_name
194-
if mod_name not in ("std", "std.compat"):
195-
requires.append(mod_name)
196-
continue
197-
# If we hit something that's not a module-related keyword, stop
198-
if not line.startswith('export') and not line.startswith('import') and not line.startswith('module'):
199-
break
200-
except Exception:
201-
pass
202-
203-
src_provides[str(src)] = provides
204-
src_requires[str(src)] = requires
205-
for mod in provides:
206-
mod_source[mod] = str(src)
207-
208-
print(f" Found {len(mod_source)} modules")
209-
210-
# ─── Phase 3: Topological sort ──────────────────────────────────────────────
211-
print("\n:: Phase 3: Topological sort")
212-
213-
# Build dependency graph
214-
visited = set()
215-
order = []
216-
in_progress = set()
217-
218-
def visit(mod_name):
219-
if mod_name in visited:
220-
return
221-
if mod_name in in_progress:
222-
# Circular dependency — shouldn't happen with well-formed modules
223-
print(f" WARNING: circular dependency involving {mod_name}")
224-
return
225-
in_progress.add(mod_name)
226-
src = mod_source.get(mod_name)
227-
if src:
228-
for dep in src_requires.get(src, []):
229-
if dep in mod_source:
230-
visit(dep)
231-
in_progress.discard(mod_name)
232-
visited.add(mod_name)
233-
order.append(mod_name)
234-
235-
for mod_name in mod_source:
236-
visit(mod_name)
237-
238-
print(f" Build order: {len(order)} modules")
239-
240-
# ─── Phase 4: Compile modules in dependency order ───────────────────────────
241-
print("\n:: Phase 4: Compiling modules")
242-
243-
# Track accumulated -fmodule-file flags
244-
accumulated_mod_flags = std_mod_flags
245-
246-
for i, mod_name in enumerate(order):
247-
src = mod_source[mod_name]
248-
safe_name = mod_name.replace(".", "_")
249-
pcm_path = pcmdir / f"{safe_name}.pcm"
250-
obj_path = objdir / f"{safe_name}.o"
251-
252-
# Build dep flags for this module's requirements
253-
dep_flags = accumulated_mod_flags
254-
255-
# Precompile .cppm → .pcm
256-
print(f" [{i+1}/{len(order)}] {mod_name}")
257-
run(f'{cxx} {cxxflags} {dep_flags} --precompile "{src}" -o "{pcm_path}"',
258-
f"precompile {mod_name}")
259-
260-
# Compile .pcm → .o
261-
run(f'{cxx} {cxxflags} {dep_flags} "{pcm_path}" -c -o "{obj_path}"',
262-
f"compile {mod_name}")
263-
264-
# Add this module to accumulated flags for subsequent modules
265-
accumulated_mod_flags += f' -fmodule-file={mod_name}="{pcm_path}"'
266-
all_objs.append(str(obj_path))
267-
268-
# ─── Phase 5: Compile main.cpp ──────────────────────────────────────────────
269-
print("\n:: Phase 5: Compile main.cpp")
270-
main_src = projroot / "src" / "main.cpp"
271-
main_obj = objdir / "main.o"
272-
run(f'{cxx} {cxxflags} {accumulated_mod_flags} -c "{main_src}" -o "{main_obj}"',
273-
"compile main.cpp")
274-
all_objs.append(str(main_obj))
275-
276-
# ─── Phase 6: Link ──────────────────────────────────────────────────────────
277-
print("\n:: Phase 6: Link")
278-
binary = bindir / "mcpp"
279-
objs_str = " ".join(f'"{o}"' for o in all_objs)
280-
run(f'{cxx} {objs_str} -o "{binary}"', "link")
281-
282-
# ─── Done ────────────────────────────────────────────────────────────────────
283-
print(f"\n:: Bootstrap complete!")
284-
result = subprocess.run([str(binary), "--version"], capture_output=True, text=True)
285-
print(f" {result.stdout.strip()}")
286-
PYTHON
100+
# ─── Stage output ────────────────────────────────────────────────────────────
287101

288102
echo
289-
"$BINDIR/mcpp" --version
290-
echo ":: SUCCESS: $BINDIR/mcpp"
103+
echo ":: Staging output..."
104+
OUTDIR="$PROJROOT/target/bootstrap/bin"
105+
mkdir -p "$OUTDIR"
106+
107+
# xmake puts output in build/<platform>/<arch>/release/mcpp
108+
BUILT=$(find "$PROJROOT/build" -name mcpp -type f -perm +111 2>/dev/null | head -1)
109+
if [ -z "$BUILT" ]; then
110+
echo "error: mcpp binary not found in xmake build output" >&2
111+
find "$PROJROOT/build" -type f 2>/dev/null | head -20
112+
exit 1
113+
fi
114+
115+
cp "$BUILT" "$OUTDIR/mcpp"
116+
echo ":: SUCCESS: $OUTDIR/mcpp"
117+
"$OUTDIR/mcpp" --version

0 commit comments

Comments
 (0)