Repository navigation
Expand file tree
/
Copy pathbootstrap.sh
More file actions
executable file
·367 lines (340 loc) · 15.5 KB
/
Copy pathbootstrap.sh
File metadata and controls
executable file
·367 lines (340 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#!/bin/sh
# Capsem developer bootstrap -- installs deps, runs doctor with auto-fix.
# Only prerequisite: sh, git, curl.
# Usage: sh bootstrap.sh [-y|--yes]
# -y, --yes Non-interactive: assume "yes" to every install prompt.
# Use in CI / unattended setup.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ASSUME_YES=0
for arg in "$@"; do
case "$arg" in
-y|--yes) ASSUME_YES=1 ;;
-h|--help)
sed -n '2,6p' "$0" | sed 's/^# \?//'
exit 0 ;;
*) printf "unknown arg: %s\n" "$arg" >&2; exit 2 ;;
esac
done
check_bootstrap_shape() {
cd "$SCRIPT_DIR"
for link in .agents/skills .claude/skills .codex/skills .cursor/skills .gemini/skills; do
[ "$(readlink "$link" 2>/dev/null || true)" = "../skills" ] || {
printf " [FAIL] %s must be a symlink to ../skills\n" "$link" >&2
exit 1
}
done
for file in \
skills/dev-sprint/SKILL.md \
skills/dev-testing/SKILL.md \
skills/dev-capsem/SKILL.md \
skills/ironbank/SKILL.md \
skills/frontend-design/SKILL.md \
web/marketing/package.json \
web/marketing/astro.config.mjs \
web/marketing/src/components/FAQ.svelte \
web/marketing/src/lib/data.ts; do
[ -f "$file" ] || { printf " [FAIL] missing %s\n" "$file" >&2; exit 1; }
done
SKILL_COUNT=$(find skills -mindepth 2 -name SKILL.md | wc -l | tr -d ' ')
[ "$SKILL_COUNT" -ge 25 ] || { printf " [FAIL] expected at least 25 project skills, found %s\n" "$SKILL_COUNT" >&2; exit 1; }
printf " [ok] project skills symlinks, key skills, and site surface\n"
}
check_bootstrap_shape
# Ask the developer "Install <tool>? [Y/n]". Returns 0 on yes, 1 on no.
# Default is YES (just press enter). Auto-yes when -y is set; auto-yes when
# stdin isn't a tty either (CI/pipelines should bootstrap fully -- pass an
# explicit "n" answer or skip the call site if you don't want the install).
confirm() {
if [ "$ASSUME_YES" = 1 ] || [ ! -t 0 ]; then
return 0
fi
printf " Install %s? [Y/n] " "$1"
read -r answer
case "$answer" in
n|N|no|NO) return 1 ;;
*) return 0 ;;
esac
}
printf "Capsem Bootstrap (%s)\n" "$(uname -s)"
echo "========================"
echo ""
# --- Phase 1: Bare minimum tools ---
# bash/git/curl are hard prereqs (we can't even curl an installer without
# curl). rustup and just are auto-installed via official installers --
# rustup from sh.rustup.rs, just from just.systems (no cargo dependency
# so it works on a fresh machine before any rust toolchain exists).
MISSING_HARD=0
for tool in bash git curl; do
if command -v "$tool" >/dev/null 2>&1; then
printf " [ok] %s\n" "$tool"
else
printf " [MISS] %s -- install via your OS package manager\n" "$tool"
MISSING_HARD=$((MISSING_HARD + 1))
fi
done
if [ "$MISSING_HARD" -gt 0 ]; then
echo ""
echo "Install the hard prereqs above, then re-run: sh bootstrap.sh"
exit 1
fi
# Make sure ~/.cargo/bin and ~/.local/bin are visible for the rest of this
# script -- both installers drop binaries there but don't reload PATH.
export PATH="$HOME/.cargo/bin:$HOME/.local/bin:$PATH"
# The workspace pin is the one Rust version setup is allowed to install. Keep
# its parser in a source-only helper shared with doctor so bootstrap, doctor,
# and the gate cannot silently qualify different compilers.
# shellcheck disable=SC1091
. "$SCRIPT_DIR/build_system/scripts/bootstrap/bootstrap-rust.sh"
CAPSEM_RUST_TOOLCHAIN=$(capsem_rust_toolchain "$SCRIPT_DIR/rust-toolchain.toml")
# Linux is a first-class build and runtime host. Provision the complete native
# fast-gate toolchain, the container runtime, same-session Docker/KVM access,
# and the Node major selected by config/docker/image/build.toml. The helper is
# sourced rather than executed so its verified ~/.local/bin Node install is
# immediately visible to the remaining bootstrap phases.
if [ "$(uname -s)" = "Linux" ]; then
# shellcheck disable=SC1091
. "$SCRIPT_DIR/build_system/scripts/bootstrap/bootstrap-linux.sh"
bootstrap_linux "$SCRIPT_DIR" "$ASSUME_YES"
fi
if command -v rustup >/dev/null 2>&1; then
printf " [ok] rustup\n"
elif confirm "rustup (Rust toolchain manager, via sh.rustup.rs)"; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain "$CAPSEM_RUST_TOOLCHAIN" --profile minimal
# shellcheck disable=SC1091
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
fi
if command -v just >/dev/null 2>&1; then
printf " [ok] just\n"
elif confirm "just (command runner, via just.systems -> ~/.local/bin)"; then
mkdir -p "$HOME/.local/bin"
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \
| bash -s -- --to "$HOME/.local/bin"
fi
# Final check: rustup + just are non-negotiable for the rest of the script.
for tool in rustup just; do
if ! command -v "$tool" >/dev/null 2>&1; then
printf " [FAIL] %s missing -- bootstrap cannot continue without it\n" "$tool"
exit 1
fi
done
# An unrelated standalone `cargo` is not proof that this checkout can build.
# Install and execute the exact compiler named by rust-toolchain.toml before
# dependency materialization or the private-copy gate can begin.
capsem_ensure_rust_toolchain "$CAPSEM_RUST_TOOLCHAIN" "$SCRIPT_DIR/config/gate.toml"
if [ "$(uname -s)" = "Linux" ]; then
capsem_expose_rustup_tools
fi
# --- Phase 2: Install dependencies ---
echo ""
echo "== Installing dependencies =="
if ! command -v uv >/dev/null 2>&1; then
if confirm "uv (Python package manager, via astral.sh -> ~/.local/bin)"; then
curl --proto '=https' --tlsv1.2 -LsSf https://astral.sh/uv/install.sh \
| env INSTALLER_NO_MODIFY_PATH=1 sh
fi
fi
if command -v uv >/dev/null 2>&1; then
printf " Python lock metadata + deps (uv lock --project build_system --locked; uv sync --project build_system --frozen)...\n"
uv lock --project build_system --locked
uv sync --project build_system --frozen
else
printf " [SKIP] Python deps (uv not installed -- some just recipes will fail)\n"
fi
# Rust workspace crates are fetched by doctor's fix below (`cargo-fetch`), the
# one owner of that check: a Cargo.lock that gains a crate after bootstrap is
# caught by `just doctor` and repaired by `just doctor fix` the same way.
# The production host-SBOM generator reads the exact Debian packages emitted
# by the Linux release rail. Ubuntu dpkg currently writes data.tar.zst, while
# macOS bsdtar delegates that member to the external zstd executable. Install
# it during canonical bootstrap so `just test` cannot discover the missing
# decoder only after both release packages and the macOS package are built.
if [ "$(uname -s)" = "Darwin" ] && ! command -v zstd >/dev/null 2>&1; then
if command -v brew >/dev/null 2>&1; then
if confirm "zstd (Debian package/SBOM archive support, via brew)"; then
brew install zstd
fi
else
printf " [SKIP] zstd (Homebrew not installed -- install brew, then: brew install zstd)\n"
fi
fi
# The canonical macOS gate installs the exact package in a disposable Tart
# guest. Bootstrap installs only the small host-side tools; the 25 GB macOS
# image remains an explicit `just test` cost and is cached by Tart thereafter.
if [ "$(uname -s)" = "Darwin" ] \
&& { ! command -v tart >/dev/null 2>&1 || ! command -v sshpass >/dev/null 2>&1; }; then
if command -v brew >/dev/null 2>&1; then
if confirm "Tart + sshpass (clean macOS package install gate, via brew)"; then
# Tart 2.32 depends on softnet from the same official Cirrus Labs
# tap. Current Homebrew requires that dependency to be trusted
# explicitly before it will evaluate the Tart formula.
brew trust --formula cirruslabs/cli/softnet
brew install cirruslabs/cli/tart cirruslabs/cli/sshpass
fi
else
printf " [SKIP] Tart install gate (Homebrew not installed -- install brew, then Tart + sshpass)\n"
fi
fi
# Bootstrap owns the one-time OCI pull and proves the VM really boots. Doctor
# repeats the clone/boot/SSH proof from the cache and fails if the base is
# missing, so the complete local release gate never discovers a dead Tart setup late.
if [ "$(uname -s)" = "Darwin" ] \
&& command -v tart >/dev/null 2>&1 \
&& command -v sshpass >/dev/null 2>&1; then
if [ "${CAPSEM_GATE_COMMAND_SANDBOX_MODE:-}" = "enforce" ]; then
printf " [DEFER] Tart boot readiness (owned by the gate's authenticated outside-sandbox action)\n"
else
printf " Tart base image + boot readiness...\n"
uv run --project build_system --frozen python "$SCRIPT_DIR/build_system/scripts/build/tart_readiness.py"
export CAPSEM_BOOTSTRAP_TART_PROVEN=1
fi
fi
PNPM_READY=0
PNPM_VERSION=""
PNPM_MAJOR=""
if command -v pnpm >/dev/null 2>&1; then
PNPM_VERSION=$(pnpm --version 2>/dev/null || true)
PNPM_MAJOR=${PNPM_VERSION%%.*}
if [ "$(uname -s)" != "Linux" ] || [ "$PNPM_MAJOR" = 10 ]; then
PNPM_READY=1
fi
fi
if [ "$PNPM_READY" -ne 1 ]; then
case "$(uname -s)" in
Darwin)
if command -v brew >/dev/null 2>&1 && confirm "pnpm (Node package manager, via brew)"; then
brew install pnpm
fi ;;
Linux)
# bootstrap_linux installed the configured Node major first. Keep
# pnpm aligned with CI and the immutable host-builder image.
if confirm "pnpm 10 (Node package manager, via npm)"; then
npm install --global pnpm@10 --prefix "$HOME/.local"
fi ;;
esac
fi
if command -v pnpm >/dev/null 2>&1; then
if [ "$(uname -s)" = "Linux" ]; then
PNPM_VERSION=$(pnpm --version 2>/dev/null || true)
PNPM_MAJOR=${PNPM_VERSION%%.*}
if [ "$PNPM_MAJOR" != 10 ]; then
printf " [FAIL] pnpm 10 is required after bootstrap; found %s\n" \
"${PNPM_VERSION:-unknown}" >&2
exit 1
fi
fi
if [ -n "${CAPSEM_GATE_RUN:-}" ]; then
printf " [SKIP] Locked Node workspaces (inside %s; fast.toolchain.node already owns every locked workspace)\n" \
"$CAPSEM_GATE_RUN"
else
printf " Locked Node workspaces...\n"
uv run --project build_system --frozen capsem-gate install-node
fi
else
printf " [SKIP] Node workspace deps (pnpm not installed -- doctor will catch this)\n"
fi
# Container runtime. Required by `just build-assets` (kernel/rootfs are built
# in Docker) which doctor's auto-fix step runs next.
# macOS: install colima + docker + docker-buildx via brew, start the VM.
# Linux: bootstrap_linux installed and started the native daemon, repaired
# current-session access, and verified Buildx before dependency work.
case "$(uname -s)" in
Darwin)
# Cache policy is strict Pydantic-owned TOML. Consume its validated JSON
# instead of maintaining a second partial TOML parser in this script.
DOCKER_DISK_BYTES=$(
uv run --project "$SCRIPT_DIR/build_system" --frozen \
capsem-cache --repository "$SCRIPT_DIR" contract docker \
| uv run --project "$SCRIPT_DIR/build_system" --frozen python -c \
'import json, sys; print(json.load(sys.stdin)["max_size_bytes"])'
)
case "$DOCKER_DISK_BYTES" in
''|*[!0-9]*)
printf "invalid docker max_size_bytes in validated cache policy\n" >&2
exit 2
;;
esac
DOCKER_DISK_GIB=$((DOCKER_DISK_BYTES / 1073741824))
if ! command -v brew >/dev/null 2>&1; then
printf " [SKIP] container runtime (Homebrew not installed)\n"
else
if ! command -v colima >/dev/null 2>&1; then
if confirm "colima + docker CLI (container runtime, via brew)"; then
brew install colima docker docker-buildx
# Wire docker-buildx into ~/.docker/cli-plugins so `docker buildx` works.
mkdir -p "$HOME/.docker/cli-plugins"
ln -sf "$(brew --prefix docker-buildx)/bin/docker-buildx" \
"$HOME/.docker/cli-plugins/docker-buildx"
fi
fi
# Start Colima if installed but not running. Doctor's fix can't
# do this -- it would just print the suggestion and fail.
if command -v colima >/dev/null 2>&1 && ! colima status >/dev/null 2>&1; then
if confirm "start Colima now (vz, 16 GB RAM, 8 CPU, ${DOCKER_DISK_GIB} GB disk -- release-gate profile)"; then
colima start --vm-type vz --vz-rosetta --memory 16 --cpu 8 --disk "$DOCKER_DISK_GIB"
fi
fi
# The persistent config can say Rosetta is enabled while the
# running VM predates that config and has no live binfmt handler.
# Repair that stale runtime before the Docker probe or expensive
# cross-architecture builds. Intel Macs do not need Rosetta.
if command -v colima >/dev/null 2>&1 \
&& colima status >/dev/null 2>&1 \
&& [ "$(uname -m)" = "arm64" ]; then
colima_yaml="$HOME/.colima/default/colima.yaml"
if [ -f "$colima_yaml" ] \
&& grep -q 'rosetta: true' "$colima_yaml" \
&& grep -q 'vmType: vz' "$colima_yaml"; then
if ! colima ssh -- test -f /proc/sys/fs/binfmt_misc/rosetta >/dev/null 2>&1; then
if confirm "restart Colima to register Rosetta for amd64 release builds"; then
colima restart
fi
fi
elif confirm "restart Colima with VZ Rosetta for amd64 release builds"; then
colima stop
colima start --vm-type vz --vz-rosetta --memory 16 --cpu 8 --disk "$DOCKER_DISK_GIB"
fi
fi
if command -v docker >/dev/null 2>&1; then
docker info >/dev/null
docker_dns_ready=0
for attempt in $(seq 1 30); do
if docker run --rm --pull=missing alpine:3.20 getent hosts ghcr.io >/dev/null 2>&1; then
docker_dns_ready=1
break
fi
sleep 1
done
if [ "$docker_dns_ready" -ne 1 ]; then
printf " [FAIL] Docker DNS did not become ready after Colima startup\n" >&2
exit 1
fi
printf " [ok] docker VM probe (info + registry DNS)\n"
fi
fi ;;
Linux)
docker info >/dev/null
docker buildx version >/dev/null
printf " [ok] native Docker daemon + Buildx\n" ;;
esac
# --- Phase 3: Run doctor with auto-fix ---
echo ""
echo "== Running doctor (with auto-fix) =="
echo ""
"$SCRIPT_DIR/build_system/scripts/doctor/doctor-common.sh" --fix
# The gate runs in a fresh process whose PATH cannot inherit this script's
# earlier export. Linux exposes exactly the config-owned Cargo tools through
# the same managed ~/.local/bin boundary as rustup; macOS keeps its existing
# Rustup/profile layout unchanged.
if [ "$(uname -s)" = "Linux" ]; then
capsem_expose_gate_cargo_tools "$SCRIPT_DIR/config/gate.toml"
fi
echo ""
echo "========================"
echo "Bootstrap complete. Verify with:"
echo ""
echo " just exec \"echo hi\" # Boot VM + run echo + exit"
echo " just shell # Interactive VM shell"
echo " just test # Unit tests + cross-compile + frontend check"
echo ""