makepad/tools/run/entry/makepad.sh

634 lines
32 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# Makepad for macOS and Linux: gets Rust, compiles Makepad's loader (makepad run) from its source on
# this computer and starts it. Read it before you run it; it is this one file.
#
# curl -fsSL https://makepad.nl/runner/unix | sh
#
# 1. Rust 1.98.0 (rustc, cargo, rust-std) from static.rust-lang.org, fetched as 25 MB pieces
# over parallel connections, checked against the sizes and SHA-256 sums below.
# 2. At the same time, the loader's source: the release service's licence-free loader release,
# a ZIP whose size and SHA-256 the service names (checked before unpacking); and, with no
# arguments, the packs of the runner's git repositories (the service's latest `runner`
# release), which makepad run takes instead of downloading them itself.
# 3. The loader is compiled for this computer's processor (`cargo build --profile host --offline`:
# the ZIP holds every crate) the moment Rust and the source are in place, and started with no
# arguments: it runs the runner, which downloads the rest. With arguments, the loader runs with
# them instead. A new loader release replaces only the files that changed, so only their crates
# compile again.
#
# In its folder it creates rust/ (the toolchain), cache/ (downloads, the loader, its caches and
# logs), apps/ (what `makepad` and `makepad app <id>` run: the runner's and each app's launch
# record), runner/ (the runner's own state: licence, catalog, launch logs) and target/ (what makepad
# run builds for the apps: build scripts, unit graphs, code cache). The folder is this
# file's own when it holds nothing else (or is an installation already), else ~/makepad-runner
# (piped into sh, or saved among other downloads); $MAKEPAD_LOADER_ROOT overrides. Run it again at any time: nothing is
# downloaded or compiled twice. The system needs a C linker (Apple's command line tools; on
# Linux cc) and, on Linux, OpenSSL 3's development files; it names the packages when missing.
set -u
umask 022
rust_version=1.98.0
rust_date=2026-08-20
dist=https://static.rust-lang.org/dist/$rust_date
piece=25000000
service=${MAKEPAD_LOADER_SERVICE:-https://makepad.nl/api/loader}
service=${service%/}
# --- The screen ---------------------------------------------------------------------------
# The Builder's look: header, one row per step, a rule, a status line and a footer, in plain
# terminal colours (bold title, dim secondary text, orange for work in progress, green checks).
# Rows are rewritten in place on a terminal; anywhere else each change is one plain line.
if [ -t 1 ] && [ "${TERM:-dumb}" != dumb ]; then
tty=1
E=$(printf '\033')
B="$E[0;1m" D="$E[0;2m" C="$E[0;38;2;255;92;57m" G="$E[0;32m" Y="$E[0;33m" R="$E[0;31m" N="$E[0m"
else
tty= E= B= D= C= G= Y= R= N=
fi
rule='────────────────────────────────────────────────────────────────────────────'
framed=
logs=
jobs=
shown=
now() { date +%s; }
# stamp NAME appends "NAME HH:MM:SS.cc" to the timing log
stamp() {
[ -n "$logs" ] && [ -d "$logs" ] || return 0
t=$(date +%H:%M:%S.%N)
case "$t" in *N) t=${t%.N} ;; *) t=$(printf '%s' "$t" | cut -c1-11) ;; esac
printf '%s %s\n' "$1" "$t" >> "$logs/bootstrap.log"
}
# row N NAME TEXT [KEY]: TEXT after the name column on screen row N; off a terminal the line is
# printed when KEY (default TEXT) changed, so progress shows in steps, not every second
row() {
[ -n "$framed" ] || return 0
if [ -n "$tty" ]; then
printf '%s[%s;1H%s[2K %-20s%s' "$E" "$1" "$E" "$2" "$3"
else
eval "last=\${last_$1:-}"
[ "$last" = "${4:-$3}" ] && return 0
eval "last_$1=\${4:-\$3}"
printf ' %-20s%s\n' "$2" "$3"
fi
}
status() {
[ -n "$framed" ] || return 0
if [ -n "$tty" ]; then printf '%s[11;1H%s[2K %s' "$E" "$E" "$1"
elif [ "${last_status:-}" != "$1" ]; then last_status=$1; printf ' %s\n' "$1"; fi
}
footer() {
[ -n "$framed" ] && [ -n "$tty" ] || return 0
printf '%s[13;1H%s[2K %s%s%s' "$E" "$E" "$D" "$1" "$N"
}
frame() {
framed=1
[ -z "$tty" ] || printf '%s[H%s[2J' "$E" "$E"
printf '\n %sMakepad%s › Getting started\n %sShipped as source code, so your coding agent can customize everything.%s\n\n' "$B" "$N" "$D" "$N"
if [ -n "$need_rust" ]; then row 5 "Rust $rust_version" "${D}○ downloads first$N"; else row 5 "Rust $rust_version" "$G✓$N ready"; fi
if [ -n "$need_zip" ]; then row 6 "Loader source" "${D}○ downloads with Rust$N"; else row 6 "Loader source" "$G✓$N here"; fi
row 7 "Makepad's loader" "${D}○ compiles once Rust is here$N"
row 8 "Makepad" "$D○$N"
[ -z "$tty" ] || printf '%s[10;1H%s[2K %s%s%s' "$E" "$E" "$D" "$rule" "$N"
status "Setting up this folder once; nothing outside it changes."
footer "working · ctrl+c stops"
}
# below the frame, for whatever is printed after it
leave() { [ -z "$framed" ] || [ -z "$tty" ] || printf '%s[14;1H\n' "$E"; }
# bar DONE TOTAL sets BAR (24 cells)
bar() {
filled=0
[ "$2" -gt 0 ] && filled=$(($1 * 24 / $2))
[ "$filled" -le 24 ] || filled=24
done_bar= left_bar= i=1
while [ $i -le 24 ]; do
if [ $i -le "$filled" ]; then done_bar="$done_bar━"; else left_bar="$left_bar─"; fi
i=$((i + 1))
done
BAR="$C$done_bar$D$left_bar$N"
}
# the background jobs this script started (by PID), stopped when it fails or is stopped
stop_jobs() {
[ -n "$jobs" ] && [ -d "$jobs" ] || return 0
for job in fetch build run unpack-rustc unpack-rust-std unpack-cargo; do
[ "$job" = run ] && [ -n "$shown" ] && continue
[ -f "$jobs/$job.pid" ] && [ ! -f "$jobs/$job.done" ] && kill "$(cat "$jobs/$job.pid")" 2>/dev/null
done
return 0
}
# fail MESSAGE: one red line, exit 1
fail() {
stamp failed
stop_jobs
if [ -n "$framed" ] && [ -n "$tty" ]; then
printf '%s[11;1H%s[2K %s✗%s %s%s[13;1H%s[2K%s[14;1H\n' "$E" "$E" "$R" "$N" "$1" "$E" "$E" "$E"
else
printf '\n %s✗%s %s\n\n' "$R" "$N" "$1"
fi
exit 1
}
trap 'stamp stopped; stop_jobs; leave; exit 130' INT TERM
# job NAME COMMAND...: runs COMMAND in the background; its PID in jobs/NAME.pid, its exit code
# in jobs/NAME.done when it ends
job() {
name=$1
shift
rm -f "$jobs/$name.pid" "$jobs/$name.done"
(
"$@" &
echo $! > "$jobs/$name.pid"
wait $!
echo $? > "$jobs/$name.code"
mv -f "$jobs/$name.code" "$jobs/$name.done"
) &
while [ ! -f "$jobs/$name.pid" ]; do sleep 0.1 2>/dev/null || sleep 1; done
}
nap() { sleep 0.5 2>/dev/null || sleep 1; }
sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi; }
size_of() { wc -c < "$1" | tr -d ' '; }
# a path inside a double-quoted curl config value
cfgq() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
# --- Where, and what this computer has ------------------------------------------------------
case "$0" in
sh | -sh | */sh | bash | -bash | */bash | dash | */dash | zsh | */zsh) script_dir= ;;
*) script_dir=; [ -f "$0" ] && script_dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P) ;;
esac
# The folder: this file's own when it is an installation already, or holds nothing else (a folder
# made for it); else ~/makepad-runner (piped into sh, or a download among other files), where the
# file copies itself.
if [ -n "$script_dir" ] && [ -z "${MAKEPAD_LOADER_ROOT:-}" ] && [ ! -d "$script_dir/cache" ] \
&& [ -n "$(ls -A "$script_dir" | grep -v -x -e "$(basename -- "$0")" -e .DS_Store)" ]; then
script_dir_other=1
fi
if [ -n "$script_dir" ] && [ -z "${script_dir_other:-}" ]; then default_root=$script_dir; else default_root=$HOME/makepad-runner; fi
root=${MAKEPAD_LOADER_ROOT:-$default_root}
mkdir -p "$root" 2>/dev/null && root=$(cd -P -- "$root" && pwd -P) || fail "Cannot create the folder $root"
rust=$root/rust
cache=$root/cache
downloads=$cache/downloads
logs_dir=$cache/logs
jobs=$cache/jobs
case "$(uname -s)/$(uname -m)" in
Darwin/arm64 | Darwin/aarch64) triple=aarch64-apple-darwin ;;
Darwin/x86_64) triple=x86_64-apple-darwin ;;
Linux/x86_64) triple=x86_64-unknown-linux-gnu ;;
Linux/aarch64 | Linux/arm64) triple=aarch64-unknown-linux-gnu ;;
*) fail "Makepad supports macOS, and glibc Linux on x86_64 and ARM64." ;;
esac
# "<part> <bytes> <sha256>" for rustc, rust-std and cargo of this platform
case "$triple" in
aarch64-apple-darwin) parts='rustc 83206125 7b39cbbb11995584741b5821511b8706ab8ecab3bc7e12b11fb1ab50cab9d079
rust-std 46977544 49dc82ff5b6fb033baa6e85635ed9cf07a27e5297a22314808ab4cf65a81335a
cargo 12993393 ae556d0783f4ab2a6fccd4674158df08cf36526b73de03b7fde0e10b2bfc3d2e' ;;
x86_64-apple-darwin) parts='rustc 102807421 a0cf817df78615a10f4d0d29dbaafd657027e1a3665be728d0f19e341c8671bb
rust-std 47585228 57a2c40ea919c06c4cd9ff4fc8b26e309c00c112538ea522227251e8ccc1f931
cargo 13345290 7fdd094e131f98864fea06c6e0059fcecaba7b0010d07b90023f42215b6f33e8' ;;
x86_64-unknown-linux-gnu) parts='rustc 134061173 18ed6559de1b8ea6b77474ea86992b9a507d3a3d134d9ee017d30cf3f406e3ee
rust-std 48777886 8aa6405356392ce50160d1b286e86091c5e14adae3061115699c84ed4394d546
cargo 15858529 18bf1598891b30dd5eb52a337d08a92b4456255ddbe4c1ab996ffb578077031c' ;;
aarch64-unknown-linux-gnu) parts='rustc 102526949 b6fdb37bc08e320bd92381e13d7311837f04a98423b8a5b678a5eb5e87c978ca
rust-std 48592329 a2eece726a579de1f554a2c3bcc62410226b0be55736f57c2b5430c1bc71b98f
cargo 15833225 e271c3c5d50336259a166e68b60ba792a64278ab117686183be3e51a0958d34b' ;;
esac
part_names='rustc rust-std cargo'
part_field() { printf '%s\n' "$parts" | awk -v p="$1" -v f="$2" '$1 == p { print $f }'; }
# https only, except a release service on this machine (tests, a local service)
case "$service" in
https://*) proto='=https' ;;
http://127.0.0.1:* | http://localhost:*) proto='=http,https' ;;
# MAKEPAD_ALLOW_HTTP=1 (tests only): a plain-http release service on another machine
http://*) [ "${MAKEPAD_ALLOW_HTTP:-}" = 1 ] && proto='=http,https' || fail "The release service must be an https URL ${D}· $service$N" ;;
*) fail "The release service must be an https URL ${D}· $service$N" ;;
esac
command -v curl >/dev/null 2>&1 || fail "Makepad needs curl ${D}· install it, then run this again$N"
command -v tar >/dev/null 2>&1 || fail "Makepad needs tar ${D}· install it, then run this again$N"
# The system's linker (and on Linux OpenSSL's development files): named, never installed here.
if [ "$(uname -s)" = Darwin ]; then
if ! xcode-select -p >/dev/null 2>&1 || ! /usr/bin/xcrun --find ld >/dev/null 2>&1; then
xcode-select --install >/dev/null 2>&1 || :
fail "Makepad needs Apple's command line tools ${D}· their installer opens now; run this again when it has finished$N"
fi
else
missing=
command -v cc >/dev/null 2>&1 || missing="a C linker (cc)"
if ! { [ -e /usr/lib/libssl.so ] || [ -e /usr/lib64/libssl.so ] || ls /usr/lib/*-linux-gnu/libssl.so >/dev/null 2>&1; }; then
missing="${missing:+$missing and }OpenSSL's development files (libssl.so)"
fi
if [ -n "$missing" ]; then
if command -v apt-get >/dev/null 2>&1; then cmd='sudo apt-get install build-essential libssl-dev'
elif command -v dnf >/dev/null 2>&1; then cmd='sudo dnf install gcc openssl-devel'
elif command -v pacman >/dev/null 2>&1; then cmd='sudo pacman -S --needed base-devel openssl'
elif command -v zypper >/dev/null 2>&1; then cmd='sudo zypper install gcc libopenssl-devel'
else cmd='a C compiler/linker and the OpenSSL 3 development package'; fi
fail "Makepad needs $missing ${D}· install it with: $cmd$N"
fi
fi
mkdir -p "$downloads" "$logs_dir" "$jobs" "$cache/loader" || fail "Cannot create $cache"
logs=$logs_dir
stamp start
export MAKEPAD_RUN_HOME="$root"
# --- The runner's packs, from the start (no arguments only) --------------------------------
# The release service's latest licence-free `runner` release names the runner's git
# repositories on the service's read-only git face; each commit's pack goes into
# $MAKEPAD_RUN_HOME/cache/incoming/<commit>.pack (renamed into place when complete), where makepad run
# takes it instead of downloading. A release fetched before is not downloaded again.
prefetch_pids=
runner_release=
repos=
# With a loader and a runner here already, the release service gets a few seconds: offline or
# unreachable, the folder's stores and loader start at once (makepad run runs the stored commits).
# The two questions to the service go out together.
meta="--retry 2 -m 60"
if [ -f "$cache/loader/src/.built" ] && [ -f "$cache/runner/release" ]; then meta="--connect-timeout 3 -m 6"; fi
mkdir -p "$cache"
: > "$cache/loader-latest.json"
( curl -fsS --proto "$proto" $meta "$service/public/app/relax/latest" > "$cache/loader-latest.json.part" 2>>"$logs/download.log" \
&& mv -f "$cache/loader-latest.json.part" "$cache/loader-latest.json" ) &
latest_pid=$!
if [ $# -eq 0 ]; then
mkdir -p "$cache/runner" "$cache/incoming"
release_json=$(curl -fsS --proto "$proto" $meta "$service/public/app/runner/latest" 2>>"$logs/download.log") || release_json=
runner_release=$(printf '%s' "$release_json" | sed -n 's/.*"release":"\([A-Za-z0-9_-]*\)".*/\1/p')
if [ -n "$runner_release" ]; then
# one "<name> <path> <commit>" line per repository of the release
repos=$(printf '%s\n' "$release_json" | tr '{}' '\n\n' | while IFS= read -r line; do
name=$(printf '%s' "$line" | sed -n 's/.*"name":"\([A-Za-z0-9_-]*\)".*/\1/p')
path=$(printf '%s' "$line" | sed -n 's/.*"path":"\([A-Za-z0-9_/-]*\)".*/\1/p')
commit=$(printf '%s' "$line" | sed -n 's/.*"commit":"\([0-9a-f]\{40\}\)".*/\1/p')
if [ -n "$name" ] && [ -n "$path" ]; then printf '%s %s %s\n' "$name" "$path" "$commit"; fi
done)
if [ "$(sed -n 1p "$cache/runner/release" 2>/dev/null)" != "$runner_release" ]; then
for entry in $(printf '%s\n' "$repos" | tr ' ' ':'); do
name=${entry%%:*}
commit=${entry##*:}
[ ${#commit} -eq 40 ] || continue
pack=$cache/incoming/$commit.pack
[ -f "$pack" ] && continue
( printf '0032want %s\n00000009done\n' "$commit" \
| curl -fsS --proto "$proto" --retry 2 -m 1800 -H 'Content-Type: application/x-git-upload-pack-request' \
--data-binary @- -o "$pack.part" "$service/public/git/runner/$name.git/git-upload-pack" \
&& mv -f "$pack.part" "$pack" ) >/dev/null 2>>"$logs/download.log" &
prefetch_pids="$prefetch_pids $!"
done
fi
fi
fi
# --- What there is to do --------------------------------------------------------------------
need_rust=
[ "$(cat "$rust/.toolchain-version" 2>/dev/null)" = "$rust_version $triple" ] || need_rust=1
[ -x "$rust/bin/cargo" ] && [ -x "$rust/bin/rustc" ] || need_rust=1
# The latest loader release; offline, the one compiled before.
wait "$latest_pid" 2>/dev/null
latest=$(cat "$cache/loader-latest.json" 2>/dev/null) || latest=
loader_version=$(printf '%s' "$latest" | sed -n 's/.*"release":"\([A-Za-z0-9_-]*\)".*/\1/p')
archive=$(printf '%s' "$latest" | sed -n 's/.*"archive":{"name":"\([A-Za-z0-9_.-]*\)","bytes":\([0-9]*\),"sha256":"\([0-9a-f]*\)"}.*/\1 \2 \3/p')
[ -n "$archive" ] || loader_version=
# One source folder for every version: an update replaces only the files that changed, so cargo
# recompiles only the crates they belong to (target/ stays inside it: makepad finds its std two
# levels above itself).
src=$cache/loader/src
[ -n "$loader_version" ] || loader_version=$(cat "$src/.version" 2>/dev/null)
[ -n "$loader_version" ] || fail "Could not reach the release service ${D}($service)$N and no loader is here yet. ${D}Check the connection, then run this again.$N"
makepad=$src/target/host/makepad
built="$loader_version $rust_version $triple"
need_build=
[ -x "$makepad" ] && [ "$(cat "$src/.built" 2>/dev/null)" = "$built" ] || need_build=1
need_zip=
[ -n "$need_build" ] && [ "$(cat "$src/.version" 2>/dev/null)" != "$loader_version" ] && need_zip=1
if [ -n "$need_zip" ]; then
[ -n "$archive" ] || fail "The release service did not answer; the loader's source is not here yet. ${D}Check the connection, then run this again.$N"
loader_name=$(printf '%s' "$archive" | cut -d' ' -f1)
loader_bytes=$(printf '%s' "$archive" | cut -d' ' -f2)
loader_sha256=$(printf '%s' "$archive" | cut -d' ' -f3)
zip=$downloads/$loader_name
fi
[ -z "$need_rust" ] && [ -z "$need_build" ] || frame
# --- 1 + 2. Rust's pieces and the loader's source, all at once ------------------------------
cfg=$downloads/fetch.curl
rm -f "$cfg"
rust_total=0
rust_base=0
# pieces PART SIZE adds the pieces of PART not yet here to the curl config
pieces() {
idx=0 from=0
while [ $from -lt "$2" ]; do
to=$((from + piece - 1))
[ $to -lt "$2" ] || to=$(($2 - 1))
file=$downloads/$1.tar.gz.$idx
have=0
[ -f "$file" ] && have=$(size_of "$file")
if [ "$have" != $((to - from + 1)) ]; then
[ ! -f "$cfg" ] || echo next >> "$cfg"
printf 'url = "%s"\noutput = "%s"\nrange = %s-%s\nproto = =https\nfail\nlocation\nretry = 3\n' \
"$dist/$1-$rust_version-$triple.tar.gz" "$(cfgq "$file")" $from $to >> "$cfg"
fi
idx=$((idx + 1))
from=$((to + 1))
done
}
if [ -n "$need_rust" ]; then
for part in $part_names; do
size=$(part_field $part 2)
rust_total=$((rust_total + size))
if [ -f "$downloads/$part.tar.gz" ] && [ "$(size_of "$downloads/$part.tar.gz")" = "$size" ]; then
rust_base=$((rust_base + size))
else
rm -f "$downloads/$part.tar.gz"
pieces $part "$size"
fi
done
fi
if [ -n "$need_zip" ]; then
rm -f "$zip.part"
[ ! -f "$cfg" ] || echo next >> "$cfg"
printf 'url = "%s"\noutput = "%s"\nproto = %s\nfail\nretry = 3\n' \
"$service/public/app/relax/$loader_version/$loader_name" "$(cfgq "$zip.part")" "$proto" >> "$cfg"
fi
# the summed size of the Rust pieces here
pieces_sum() { ls -ln "$downloads" | awk '$9 ~ /\.tar\.gz\.[0-9]+$/ { s += $5 } END { print s + 0 }'; }
if [ -f "$cfg" ]; then
status "Downloading Rust and Makepad's loader; once, into this folder."
job fetch curl -sS -Z --parallel-immediate --parallel-max 16 -K "$cfg" 2>"$logs/download.log"
prev_sum=0 prev_t=$(now) speed='0.0 MB/s'
while :; do
rsum=$((rust_base + $(pieces_sum)))
zsum=0
[ -z "$need_zip" ] || [ ! -f "$zip.part" ] || zsum=$(size_of "$zip.part")
sum=$((rsum + zsum))
t=$(now)
if [ $((t - prev_t)) -ge 1 ]; then
speed=$(awk -v b=$((sum - prev_sum)) -v s=$((t - prev_t)) 'BEGIN { printf "%.1f MB/s", b / s / 1048576 }')
prev_sum=$sum prev_t=$t
fi
if [ -n "$need_rust" ]; then
pct=$((rsum * 100 / rust_total))
[ $pct -le 100 ] || pct=100
bar $rsum $rust_total
row 5 "Rust $rust_version" "$C●$N $BAR $(printf '%3d' $pct)% $D· $((rsum / 1048576)) / $((rust_total / 1048576)) MB · $speed$N" \
"downloading · $((pct / 10 * 10))%"
fi
if [ -n "$need_zip" ]; then
if [ "$zsum" -ge "$loader_bytes" ]; then
row 6 "Loader source" "$G✓$N downloaded"
else
bar $zsum $loader_bytes
row 6 "Loader source" "$C●$N $BAR $D· $((zsum / 1024)) / $((loader_bytes / 1024)) KB$N" "downloading"
fi
fi
[ -f "$jobs/fetch.done" ] && break
nap
done
[ "$(cat "$jobs/fetch.done")" = 0 ] || fail "The download stopped. ${D}Run makepad.sh again to continue it · log: $logs/download.log$N"
rm -f "$cfg"
stamp downloaded
fi
# --- Rust: join the pieces, check, unpack the three parts side by side ----------------------
if [ -n "$need_rust" ]; then
for part in $part_names; do
archive_file=$downloads/$part.tar.gz
[ -f "$archive_file" ] && continue
idx=0
: > "$archive_file.joined" || fail "Could not join the pieces of $part."
while [ -f "$archive_file.$idx" ]; do
cat "$archive_file.$idx" >> "$archive_file.joined" || fail "Could not join the pieces of $part."
idx=$((idx + 1))
done
idx=0
while [ -f "$archive_file.$idx" ]; do rm -f "$archive_file.$idx"; idx=$((idx + 1)); done
mv -f "$archive_file.joined" "$archive_file"
done
row 5 "Rust $rust_version" "$C●$N checking $D· SHA-256 of each part$N"
for part in $part_names; do
if [ "$(sha256 "$downloads/$part.tar.gz")" != "$(part_field $part 3)" ]; then
rm -f "$downloads/$part.tar.gz"
fail "Checksum mismatch for $part.tar.gz ${D}· downloaded again on the next run$N"
fi
done
stage=$cache/rust.unpack
rm -rf "$stage"
mkdir -p "$stage" || fail "Cannot create $stage"
for part in $part_names; do
top=$part-$rust_version-$triple
inner=$part
[ $part != rust-std ] || inner=rust-std-$triple
mkdir "$stage/$part" || fail "Cannot create $stage/$part"
job unpack-$part tar -xzf "$downloads/$part.tar.gz" -C "$stage/$part" --strip-components=2 "$top/$inner" 2>"$logs/unpack-$part.log"
done
while :; do
unpacked=0
for part in $part_names; do [ ! -f "$jobs/unpack-$part.done" ] || unpacked=$((unpacked + 1)); done
[ $unpacked -lt 3 ] || break
row 5 "Rust $rust_version" "$C●$N unpacking $D· $unpacked of 3$N" "unpacking"
nap
done
ready=$stage/ready
mkdir "$ready" || fail "Cannot create $ready"
for part in $part_names; do
[ "$(cat "$jobs/unpack-$part.done")" = 0 ] || fail "Unpacking $part failed. ${D}· log: $logs/unpack-$part.log$N"
rm -f "$stage/$part/manifest.in"
# the parts share bin/, lib/ and share/: move each file into place
( cd "$stage/$part" && find . -type d -exec sh -c 'for d; do mkdir -p "$0/$d"; done' "$ready" {} + \
&& find . ! -type d -exec sh -c 'for f; do mv -f "$f" "$0/$f"; done' "$ready" {} + ) \
2>>"$logs/unpack-$part.log" || fail "Unpacking $part failed. ${D}· log: $logs/unpack-$part.log$N"
done
printf '%s %s' "$rust_version" "$triple" > "$ready/.toolchain-version"
rm -rf "$rust"
mv "$ready" "$rust" || fail "Cannot move Rust into $rust"
rm -rf "$stage"
for part in $part_names; do rm -f "$downloads/$part.tar.gz"; done
row 5 "Rust $rust_version" "$G✓$N ready $D· in rust/$N"
stamp rust
fi
# --- The loader's source: check and unpack --------------------------------------------------
if [ -n "$need_zip" ]; then
if [ "$(size_of "$zip.part" 2>/dev/null)" != "$loader_bytes" ]; then
rm -f "$zip.part"
fail "The loader's source has the wrong size ${D}· downloaded again on the next run$N"
fi
if [ "$(sha256 "$zip.part")" != "$loader_sha256" ]; then
rm -f "$zip.part"
fail "Checksum mismatch for $loader_name ${D}· downloaded again on the next run$N"
fi
unpack=$cache/loader/.unpack
rm -rf "$unpack"
mkdir -p "$unpack" || fail "Cannot create $unpack"
if command -v unzip >/dev/null 2>&1; then unzip -q "$zip.part" -d "$unpack"
elif command -v bsdtar >/dev/null 2>&1; then bsdtar -xf "$zip.part" -C "$unpack"
elif command -v python3 >/dev/null 2>&1; then python3 -m zipfile -e "$zip.part" "$unpack"
else fail "Makepad needs unzip ${D}· install it, then run this again$N"
fi >>"$logs/download.log" 2>&1 || fail "Unpacking the loader's source failed. ${D}Run this again · log: $logs/download.log$N"
new=$unpack/$loader_version
[ -f "$new/Cargo.toml" ] || fail "The loader's source has no $loader_version/Cargo.toml. ${D}Run this again later; the release service is being updated.$N"
if [ ! -f "$src/Cargo.toml" ]; then
rm -rf "$src"
mv "$new" "$src" || fail "Cannot move the loader's source into $src"
else
# an update: files that changed are replaced (with the time of now, so cargo sees them
# as changed), files the new version no longer has are removed, the rest stay as they are
row 7 "Makepad's loader" "$C●$N updating its source $D· $loader_version$N" "updating"
# one `diff -rq` names what differs (one process, not one per file)
LC_ALL=C diff -rq -x target -x .version -x .built "$new" "$src" > "$unpack/changes" 2>&1
[ $? -le 1 ] || fail "Cannot compare the loader's source with $src ${D}· $(head -1 "$unpack/changes")$N"
while IFS= read -r line; do
case "$line" in
"Files $new/"*" differ")
rel=${line#"Files $new/"}
rel=${rel%%" and $src/"*}
mv -f "$new/$rel" "$src/$rel" && touch "$src/$rel" || exit 1 ;;
"Only in $new"*)
dir=${line#"Only in $new"}
dir=${dir%%": "*}
rel=${dir#/}${dir:+/}${line#*": "}
rel=${rel#/}
mkdir -p "$(dirname "$src/$rel")" && mv -f "$new/$rel" "$src/$rel" || exit 1
find "$src/$rel" -exec touch {} + ;;
"Only in $src"*)
dir=${line#"Only in $src"}
dir=${dir%%": "*}
rel=${dir#/}${dir:+/}${line#*": "}
rm -rf "$src/${rel#/}" ;;
esac
done < "$unpack/changes" || fail "Cannot update the loader's source in $src"
fi
printf '%s\n' "$loader_version" > "$src/.version"
rm -rf "$unpack"
rm -f "$zip.part"
stamp source
fi
# --- 3. Compile the loader --------------------------------------------------------------------
# The compile sees no Rust settings from elsewhere on this computer and writes only into
# cache/. Cargo's own output goes to the log; the screen shows one progress line.
unset RUSTUP_TOOLCHAIN RUSTFLAGS CARGO_ENCODED_RUSTFLAGS CARGO_BUILD_TARGET CARGO_BUILD_RUSTFLAGS CARGO_TARGET_DIR RUSTC_WRAPPER RUSTDOC
export PATH="$rust/bin:$PATH" CARGO_HOME="$cache/cargo-home" RUSTC="$rust/bin/rustc" CARGO_INCREMENTAL=0
if [ -n "$need_build" ]; then
build_log=$logs/build.log
rm -f "$build_log"
# the crates to compile: the lock file's packages (the archive holds only the loader's crates)
crates=$(grep -c '^name = ' "$src/Cargo.lock" 2>/dev/null)
[ "${crates:-0}" -gt 0 ] || crates=1
# the archive's lock file (path crates only): nothing is resolved or fetched on this computer
locked=
[ ! -f "$src/Cargo.lock" ] || locked=--locked
# room for the build (about 2 GB of target/), said before it starts rather than as a cargo error
free_kb=$(df -Pk "$cache" | awk 'NR == 2 { print $4 }')
[ "${free_kb:-0}" -ge 3000000 ] || fail "Compiling Makepad's loader needs about 3 GB free on this disk. ${D}Free some space, then run this again.$N"
status "Compiling Makepad's loader; once per version."
build_t0=$(now)
# for this computer's processor ([profile.host]: thin LTO, 16 codegen units, target-cpu=native)
job build env RUSTFLAGS=-Ctarget-cpu=native sh -c 'cd "$1" && exec "$2" build --profile host --offline $3 -p makepad-run --bin makepad' \
build "$src" "$rust/bin/cargo" "$locked" >"$build_log" 2>&1
while [ ! -f "$jobs/build.done" ]; do
compiled=$(grep -c '^ *Compiling ' "$build_log" 2>/dev/null)
compiled=${compiled:-0}
[ "$compiled" -le $crates ] || compiled=$crates
bar "$compiled" $crates
row 7 "Makepad's loader" "$C●$N $BAR $D· $compiled of $crates crates · $(($(now) - build_t0)) s$N" "compiling"
nap
done
if [ "$(cat "$jobs/build.done")" != 0 ]; then
why=$(grep -m1 '^error' "$build_log" | cut -c1-90)
fail "Makepad's loader did not compile${why:+: $why}. ${D}Run this again; if it stops the same way, the log says why: $build_log$N"
fi
printf '%s\n' "$built" > "$src/.built"
row 7 "Makepad's loader" "$G✓$N compiled $D· $(($(now) - build_t0)) s$N"
stamp built
fi
# This script stays in the folder for the next start.
if [ -n "$script_dir" ] && [ "$script_dir" != "$root" ]; then
cp "$0" "$root/makepad.sh.next" 2>/dev/null && chmod 755 "$root/makepad.sh.next" && mv -f "$root/makepad.sh.next" "$root/makepad.sh" || :
fi
# --- 4. Start it --------------------------------------------------------------------------------
# makepad run runs cargo/rustc for build scripts and unit graphs (until it does that itself), so the
# toolchain stays on PATH for it and every app it starts. With no arguments `makepad` runs the
# runner: its launch record names the runner's repositories, the release service's latest
# licence-free `runner` release, which the service serves read-only over git
# ($service/public/git/runner/<repository>.git, one branch per release). The repository at
# `makepad` is the workspace, the others mount at their folders inside it. makepad run keeps its
# stores in $MAKEPAD_RUN_HOME/cache and, with --cached, starts the last one at once while it
# fetches the new release for the next start. Offline, the release recorded last.
export MAKEPAD_LOADER_ROOT="$root" MAKEPAD_LOADER_SERVICE="$service"
if [ $# -gt 0 ]; then
stamp start-loader
leave
exec "$makepad" "$@"
fi
if [ -n "$framed" ]; then
row 8 "Makepad" "$C●$N starting"
status "Makepad opens in its own window; closing it ends this one."
else
printf ' %sMakepad%s %s· starting · log: %s%s\n' "$B" "$N" "$D" "$logs/makepad.log" "$N"
fi
# the runner's packs downloaded alongside the build are complete before makepad run looks for them
[ -z "$prefetch_pids" ] || wait $prefetch_pids || :
if [ -n "$runner_release" ]; then
[ -n "$repos" ] || fail "The runner release names no repositories."
{ printf '%s\n' "$runner_release"; printf '%s\n' "$repos"; } > "$cache/runner/release.next" && mv -f "$cache/runner/release.next" "$cache/runner/release"
else
[ -f "$cache/runner/release" ] || fail "Could not reach the release service ${D}($service)$N and the runner was never fetched."
status "The release service did not answer; starting the runner fetched before."
runner_release=$(sed -n 1p "$cache/runner/release")
repos=$(sed -n '2,$p' "$cache/runner/release")
fi
git_base=$service/public/git/runner
# The runner's launch record ($MAKEPAD_RUN_HOME/apps/runner.launch, one `key<TAB>value` line each):
# what `makepad` with no arguments runs, from here and from anywhere else (makepad launch.rs).
launch_dir=$root/apps
mkdir -p "$launch_dir" || fail "Cannot create $launch_dir"
record=$launch_dir/runner.launch
printf 'title\tMakepad\narg\trun\narg\t--quiet\narg\t--cached\n' > "$record.part" || fail "Cannot write $record"
rm -f "$record.mounts"
root_repo=
for entry in $(printf '%s\n' "$repos" | tr ' ' ':'); do
name=${entry%%:*}
path=${entry#*:}
path=${path%%:*}
# the release's commit pinned: makepad run takes the pack downloaded above (or its store) as it is,
# without asking the service again
commit=${entry##*:}
pin=
[ ${#commit} -ne 40 ] || pin=@$commit
case "$path" in
makepad) root_repo="$git_base/$name.git#$runner_release$pin" ;;
makepad/*) printf 'arg\t--mount\narg\t%s\n' "${path#makepad/}=$git_base/$name.git#$runner_release$pin" >> "$record.mounts" ;;
*) rm -f "$record.mounts"; fail "The runner release has a repository outside its workspace: $path" ;;
esac
done
[ -n "$root_repo" ] || fail "The runner release has no repository at makepad."
printf 'arg\t%s\n' "$root_repo" >> "$record.part"
[ ! -f "$record.mounts" ] || { cat "$record.mounts" >> "$record.part"; rm -f "$record.mounts"; }
printf 'arg\tmakepad-runner\n' >> "$record.part"
mv -f "$record.part" "$record" || fail "Cannot write $record"
stamp start-loader
# The loader runs as a job; the runner reports its first frame (MAKEPAD_RUN_REPORT_TO), and the row
# turns to running then. Its own output goes to the log.
report=$logs/runner.report
: > "$report"
export MAKEPAD_RUN_REPORT_TO="$report"
job run "$makepad" >"$logs/makepad.log" 2>&1
while [ ! -f "$jobs/run.done" ]; do
if [ -z "$shown" ] && grep -q '^present' "$report" 2>/dev/null; then
shown=1
stamp runner-present
row 8 "Makepad" "$G✓$N running"
status "Makepad is open in its own window; closing it ends this one."
footer "ctrl+c leaves Makepad running"
fi
nap
done
code=$(cat "$jobs/run.done")
stamp "loader-exit-$code"
[ "$code" = 0 ] || fail "Makepad stopped with an error (exit $code). ${D}Run this again to start it; the log says why: $logs/makepad.log$N"
leave
exit 0