|
1 | 1 | #!/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. |
3 | 3 | # |
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. |
7 | 7 | # |
8 | 8 | # 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) |
10 | 11 | # - macOS SDK (xcode-select --install) |
11 | | -# - Python 3 (ships with macOS) |
12 | 12 | # |
13 | 13 | # Usage: |
14 | 14 | # ./scripts/bootstrap-macos.sh [LLVM_ROOT] |
|
18 | 18 | # |
19 | 19 | set -euo pipefail |
20 | 20 |
|
| 21 | +PROJROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 22 | +cd "$PROJROOT" |
| 23 | + |
21 | 24 | # ─── Locate LLVM ──────────────────────────────────────────────────────────── |
22 | 25 |
|
23 | 26 | if [ -n "${1:-}" ] && [ -d "$1/bin" ]; then |
|
35 | 38 |
|
36 | 39 | CXX="$LLVM_ROOT/bin/clang++" |
37 | 40 | echo ":: LLVM_ROOT = $LLVM_ROOT" |
38 | | -echo ":: CXX = $CXX" |
39 | 41 | "$CXX" --version | head -1 |
40 | 42 |
|
41 | | -# ─── Locate macOS SDK ──────────────────────────────────────────────────────── |
| 43 | +# ─── Ensure xmake is available ─────────────────────────────────────────────── |
42 | 44 |
|
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 |
47 | 48 | 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 |
56 | 83 | fi |
57 | | -echo ":: std.cppm = $STD_CPPM" |
58 | 84 |
|
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 ──────────────────────────────────────────────────────── |
84 | 86 |
|
85 | 87 | echo |
86 | | -echo ":: Compiling mcpp (42 modules + main.cpp)..." |
| 88 | +echo ":: Building mcpp with xmake (LLVM/Clang toolchain)..." |
87 | 89 | echo |
88 | 90 |
|
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 |
151 | 96 |
|
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 |
158 | 99 |
|
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 ──────────────────────────────────────────────────────────── |
287 | 101 |
|
288 | 102 | 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