3822 lines
181 KiB
Bash
Executable file
3822 lines
181 KiB
Bash
Executable file
#!/bin/sh
|
||
# Makepad Builder for macOS and Linux.
|
||
#
|
||
# curl -fsSL https://makepad.nl/builder/unix | sh
|
||
#
|
||
# You can read this whole file before running it: it is one shell script and
|
||
# nothing compiled. It opens the Builder in your terminal, which asks for your
|
||
# email (the one identity; empty continues with the free apps), a folder
|
||
# (default ~/makepad-commercial) and which Rust to use, then installs Rust
|
||
# into that folder only to compile the apps. It copies itself into the folder
|
||
# as the `makepad` command:
|
||
#
|
||
# ~/makepad-commercial/makepad opens the Builder again
|
||
# ~/makepad-commercial/makepad build APP compiles an app (for coding agents)
|
||
#
|
||
# Everything the Builder downloads, builds and records stays in that folder
|
||
# (in its builder/ folder: beside `makepad` there are only the apps you
|
||
# build); Apple's developer tools or your distribution's development packages are
|
||
# the only things installed elsewhere, and it shows and asks about them first.
|
||
#
|
||
# The Builder itself is the text between the MAKEPAD_BUILDER lines below.
|
||
# Everything runs from the last line, so a download cut off halfway does
|
||
# nothing at all.
|
||
|
||
makepad_builder() {
|
||
set -eu
|
||
[ "$(id -u)" != 0 ] || { echo 'Run the Makepad Builder as your normal user, not as root.' >&2; return 1; }
|
||
makepad_dir=$(mktemp -d "${TMPDIR:-/tmp}/makepad-builder.XXXXXX") || return 1
|
||
cat > "$makepad_dir/makepad" <<'MAKEPAD_BUILDER'
|
||
#!/bin/sh
|
||
# Makepad Builder for macOS and Linux, installed as `makepad` in its folder.
|
||
#
|
||
# ./makepad the Builder: licenses, apps, updates, agents
|
||
# ./makepad build APP compile an app after you or an agent changed it
|
||
# ./makepad run APP compile it when needed, then open it
|
||
# ./makepad list every app, its state and its source folder
|
||
# ./makepad env this folder's build environment, for eval
|
||
# ./makepad agent-context what coding agents are told about this folder
|
||
#
|
||
# One shell script and nothing compiled for itself; Rust is installed only to
|
||
# build the apps. Every run is a restart: all state is read from the folder,
|
||
# and a stopped download continues where it was. Everything stays in this
|
||
# folder, in builder/ (beside this command there are only the apps), except Apple's developer tools or your distribution's development
|
||
# packages, which it shows and asks about first.
|
||
set -eu
|
||
umask 077
|
||
|
||
service=${MAKEPAD_LOADER_SERVICE:-https://makepad.nl/api/loader}
|
||
service=${service%/}
|
||
# A personalized download names the email here; the Builder still asks.
|
||
builder_email='@EMAIL@'
|
||
case "$builder_email" in @*@) builder_email= ;; esac
|
||
|
||
case "$(uname -s)/$(uname -m)" in
|
||
Darwin/arm64 | Darwin/aarch64) triple=aarch64-apple-darwin plat=mac ;;
|
||
Darwin/x86_64) triple=x86_64-apple-darwin plat=mac ;;
|
||
Linux/x86_64) triple=x86_64-unknown-linux-gnu plat=linux ;;
|
||
Linux/aarch64 | Linux/arm64) triple=aarch64-unknown-linux-gnu plat=linux ;;
|
||
*) printf 'The Makepad Builder supports macOS, and glibc Linux on x86_64 and ARM64.\n' >&2; exit 1 ;;
|
||
esac
|
||
|
||
self=$0
|
||
case "$self" in /*) ;; *) self="$(pwd -P)/$self" ;; esac
|
||
# The terminal UI reads keys with bash's read builtin; where dash or another
|
||
# sh started it, the same script continues under /bin/bash when there is one.
|
||
if [ "$#" = 0 ] && [ -z "${BASH_VERSION:-}" ] && [ -z "${MAKEPAD_BUILDER_SH:-}" ] && [ -x /bin/bash ] && [ -f "$self" ]; then
|
||
MAKEPAD_BUILDER_SH=1 exec /bin/bash "$self"
|
||
fi
|
||
# The screen's faint logo strips colour codes with an extended pattern.
|
||
[ -z "${BASH_VERSION:-}" ] || shopt -s extglob
|
||
self_dir=$(cd -P -- "$(dirname -- "$self")" && pwd -P)
|
||
# home: the folder people see, with this command and the apps built there;
|
||
# root: the Builder's own folder in it (builder/), which holds everything
|
||
# else. Folders from before that layout are moved over on the next start.
|
||
home= root=
|
||
if [ -f "$self_dir/builder/makepad-builder.json" ] || [ -f "$self_dir/makepad-builder.json" ] || [ -f "$self_dir/makepad-loader.json" ]; then
|
||
home=$self_dir root=$self_dir/builder
|
||
fi
|
||
|
||
# ------------------------------------------------------------ utilities ---
|
||
|
||
nl='
|
||
'
|
||
ifs_default=$(printf ' \t\n_'); ifs_default=${ifs_default%_}
|
||
# Shell pattern ranges ([a-z], [!!-~]) follow the locale's collation: in a
|
||
# UTF-8 locale they stop meaning byte ranges, and the email check refused
|
||
# every address. Collation alone is pinned to byte order; the character
|
||
# type (UTF-8 text and widths) stays the person's.
|
||
if [ -n "${LC_ALL:-}" ]; then LC_CTYPE=$LC_ALL; export LC_CTYPE; unset LC_ALL; fi
|
||
LC_COLLATE=C; export LC_COLLATE
|
||
now() { date +%s; }
|
||
short() { case "$1" in "$HOME"/*) printf '~/%s' "${1#"$HOME"/}" ;; *) printf '%s' "$1" ;; esac; }
|
||
# pad TEXT WIDTH -> $padded, without forking.
|
||
pad() { padded=$1; while [ "${#padded}" -lt "$2" ]; do padded="$padded "; done; }
|
||
sha256() {
|
||
if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{ print $1 }'
|
||
else shasum -a 256 "$1" | awk '{ print $1 }'; fi
|
||
}
|
||
size_of() { if [ -f "$1" ]; then wc -c < "$1" | tr -d ' '; else printf 0; fi; }
|
||
# put FILE TEXT: written through a temporary name, so readers never see half.
|
||
put() { printf '%s\n' "$2" > "$1.$$.next" && mv -f "$1.$$.next" "$1"; }
|
||
log() { [ -z "${log_file:-}" ] || printf '%s %s\n' "$(date '+%H:%M:%S')" "$*" >> "$log_file"; }
|
||
# remove_inside PATH: deletes PATH only when it lies strictly inside the
|
||
# Builder's installation folder; anything else is refused and left alone.
|
||
remove_inside() {
|
||
[ -n "$home" ] || return 1
|
||
case "$1" in
|
||
"$home"/?*) case "$1" in *'/../'* | *'/..') return 1 ;; esac; rm -rf -- "$1" ;;
|
||
*) log "refused to delete $1"; return 1 ;;
|
||
esac
|
||
}
|
||
identifier() { case "$1" in '' | *[!A-Za-z0-9_-]*) return 1 ;; esac; [ "${#1}" -le 128 ]; }
|
||
relative() {
|
||
case "$1" in '' | /* | */ | *//* | *[!A-Za-z0-9_/-]*) return 1 ;; esac
|
||
case "/$1/" in */../* | */./*) return 1 ;; esac
|
||
}
|
||
hex() { case "$1" in *[!0-9a-f]*) return 1 ;; esac; [ "${#1}" = "$2" ]; }
|
||
|
||
# email TEXT -> $email_ok (lowercase) or fails: the rules of the Windows
|
||
# Builder and the service.
|
||
check_email() {
|
||
email_ok=$(printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | tr 'A-Z' 'a-z')
|
||
case "$email_ok" in *@*@* | @* | *@ | *[!!-~]* | *'<'* | *'>'* | *'"'* | *'\'*) return 1 ;; *@*) ;; *) return 1 ;; esac
|
||
em_local=${email_ok%%@*}
|
||
[ "${#email_ok}" -le 254 ] && [ "${#em_local}" -le 64 ]
|
||
}
|
||
|
||
# --------------------------------------------------------------- JSON ---
|
||
# One awk program reads the service's JSON: "flat" prints path<TAB>value
|
||
# per value, "items" each element of a top-level array on one line,
|
||
# "release" shell assignments for one release, "apps" one line per app.
|
||
json_awk='
|
||
function jstr( out, r, p, c) {
|
||
out = ""; i++
|
||
while (i <= n) {
|
||
r = substr(s, i, 4096)
|
||
p = match(r, /["\\]/)
|
||
if (p == 0) { out = out r; i += length(r); continue }
|
||
out = out substr(r, 1, p - 1); i += p - 1
|
||
c = substr(s, i, 1)
|
||
if (c == "\"") { i++; return out }
|
||
c = substr(s, i + 1, 1); i += 2
|
||
if (c == "u") { out = out "?"; i += 4 }
|
||
else if (c == "n" || c == "t" || c == "r" || c == "b" || c == "f") out = out " "
|
||
else out = out c
|
||
}
|
||
return out
|
||
}
|
||
function jpath( p, d) {
|
||
p = ""
|
||
for (d = 1; d <= depth; d++) p = p (d > 1 ? "/" : "") (kind[d] == "a" ? idx[d] : key[d])
|
||
return p
|
||
}
|
||
function jval(v) { np++; P[np] = jpath(); V[np] = v; if (mode == "flat") print P[np] "\t" v }
|
||
function q(v) { gsub(sq, sq "\"" sq "\"" sq, v); return sq v sq }
|
||
function release_vars( k, a, name, nrep, r, x, y, t, lines, total, pl, feat) {
|
||
nrep = 0; pl = ""; feat = ""
|
||
for (k = 1; k <= np; k++) {
|
||
name = P[k]
|
||
if (name ~ /^(id|title|release|rust|package|binary|workspace|license|public|cuda)$/) F[name] = V[k]
|
||
else if (name ~ /^platforms\//) pl = pl " " V[k]
|
||
else if (name ~ /^features\//) feat = feat (feat == "" ? "" : ",") V[k]
|
||
else if (name ~ /^repositories\/[0-9]+\/[a-z0-9]+$/) {
|
||
split(name, a, "/"); R[a[2] "," a[3]] = V[k]
|
||
if (a[2] + 1 > nrep) nrep = a[2] + 1
|
||
}
|
||
}
|
||
split("id title release rust package binary workspace license public cuda", a, " ")
|
||
for (k = 1; k <= 10; k++) print "rel_" a[k] "=" q(F[a[k]])
|
||
for (r = 0; r < nrep; r++) o[r] = r
|
||
for (x = 1; x < nrep; x++)
|
||
for (y = x; y > 0 && length(R[o[y - 1] ",path"]) > length(R[o[y] ",path"]); y--) { t = o[y]; o[y] = o[y - 1]; o[y - 1] = t }
|
||
lines = ""; total = 0
|
||
for (x = 0; x < nrep; x++) {
|
||
r = o[x]
|
||
lines = lines (x ? "\n" : "") R[r ",name"] "|" R[r ",path"] "|" R[r ",commit"] "|" R[r ",sha256"] "|" R[r ",bytes"]
|
||
total += R[r ",bytes"]
|
||
}
|
||
print "rel_repos=" q(lines); print "rel_bytes=" (total + 0); print "rel_platforms=" q(pl); print "rel_features=" q(feat)
|
||
}
|
||
function app_lines( k, a, count, x) {
|
||
count = 0
|
||
for (k = 1; k <= np; k++) {
|
||
split(P[k], a, "/")
|
||
if (a[2] == "features") A[a[1] ",features"] = A[a[1] ",features"] (A[a[1] ",features"] == "" ? "" : ",") V[k]
|
||
else A[a[1] "," a[2]] = V[k]
|
||
if (a[1] + 1 > count) count = a[1] + 1
|
||
}
|
||
for (x = 0; x < count; x++) print A[x ",id"] "|" A[x ",title"] "|" A[x ",package"] "|" A[x ",binary"] "|" A[x ",workspace"] "|" A[x ",features"] "|" A[x ",menu"]
|
||
}
|
||
BEGIN { sq = sprintf("%c", 39) }
|
||
{ s = s $0 "\n" }
|
||
END {
|
||
n = length(s); i = 1; depth = 0; np = 0
|
||
while (i <= n) {
|
||
c = substr(s, i, 1)
|
||
if (c == "{" || c == "[") {
|
||
if (depth == 1 && kind[1] == "a") start = i
|
||
depth++; kind[depth] = (c == "{") ? "o" : "a"; idx[depth] = 0; key[depth] = ""; want[depth] = (c == "{"); i++
|
||
} else if (c == "}" || c == "]") {
|
||
depth--; i++
|
||
if (mode == "items" && depth == 1 && kind[1] == "a") print substr(s, start, i - start)
|
||
} else if (c == ",") {
|
||
if (kind[depth] == "a") idx[depth]++; else want[depth] = 1
|
||
i++
|
||
} else if (c == ":") { want[depth] = 0; i++ }
|
||
else if (c == "\"") {
|
||
v = jstr()
|
||
if (kind[depth] == "o" && want[depth]) key[depth] = v; else jval(v)
|
||
} else if (c == " " || c == "\t" || c == "\n" || c == "\r") i++
|
||
else {
|
||
j = i
|
||
while (j <= n) { d = substr(s, j, 1); if (d == "," || d == "}" || d == "]" || d == " " || d == "\n" || d == "\t" || d == "\r") break; j++ }
|
||
jval(substr(s, i, j - i)); i = j
|
||
}
|
||
}
|
||
if (mode == "release") release_vars()
|
||
if (mode == "apps") app_lines()
|
||
}'
|
||
|
||
json() { awk -v mode="$1" "$json_awk"; }
|
||
|
||
# rel_load FILE: the release's fields as rel_* variables; fails when the file
|
||
# is missing or does not describe a release this Builder can use safely.
|
||
rel_load() {
|
||
[ -f "$1" ] || return 1
|
||
rel_id=
|
||
eval "$(json release < "$1")" || return 1
|
||
rel_valid
|
||
}
|
||
rel_valid() {
|
||
identifier "$rel_id" && identifier "$rel_release" && identifier "$rel_package" && identifier "$rel_binary" || return 1
|
||
relative "$rel_workspace" || return 1
|
||
case "$rel_rust" in *[!0-9.]* | *..* | .* | *.) return 1 ;; esac
|
||
[ -n "$rel_repos" ] || return 1
|
||
rel_makepad_path=
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
identifier "$v_name" && relative "$v_path" && hex "$v_commit" 40 && hex "$v_sha" 64 || return 1
|
||
case "$v_bytes" in '' | *[!0-9]*) return 1 ;; esac
|
||
[ "$v_name" != makepad ] || rel_makepad_path=$v_path
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
# Every repository and the workspace lie inside the Makepad checkout,
|
||
# which the one Cargo workspace needs.
|
||
[ -n "$rel_makepad_path" ] || return 1
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
case "$v_path/" in "$rel_makepad_path"/*) ;; *) return 1 ;; esac
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
case "$rel_workspace/" in "$rel_makepad_path"/*) ;; *) return 1 ;; esac
|
||
[ -n "$rel_title" ] || rel_title=$rel_id
|
||
[ -n "$rel_license" ] || rel_license=commercial
|
||
}
|
||
rel_supported() { case "$rel_platforms " in *" $triple "*) return 0 ;; esac; return 1; }
|
||
|
||
# The app registry: the snapshot's own copy once the Makepad source is here
|
||
# (it names packages and features for that commit); before that, the list
|
||
# below, only to show the experiments' names.
|
||
registry_fallback='wm|Makepad WM|||||
|
||
calculator|Calculator|||||other
|
||
calendar|Calendar|||||other
|
||
clock|Clock|||||other
|
||
director|Director|||||other
|
||
fab|Fab|||||other
|
||
fabric|Fabric|||||other
|
||
files|Files|||||other
|
||
finance|Finance|||||other
|
||
image|Image|||||other
|
||
mail|Mail|||||other
|
||
mixer|Mixer|||||other
|
||
notes|Notes|||||other
|
||
photos|Photos|||||other
|
||
reminders|Reminders|||||other
|
||
route|Route|||||other
|
||
score|Score|||||other
|
||
sheets|Sheets|||||other
|
||
task|Task|||||other
|
||
terminal|Terminal|||||other
|
||
weather|Weather|||||other'
|
||
registry=
|
||
load_registry() {
|
||
registry=
|
||
rel_load "$root/available/makepad.json" 2>/dev/null || return 0
|
||
rel_dir
|
||
reg_file="$rel_directory/$rel_makepad_path/tools/makepad_builder/apps.json"
|
||
[ -f "$reg_file" ] || return 0
|
||
registry=$(json apps < "$reg_file")
|
||
}
|
||
# The experiments list: Makepad WM first, then the registry's "other" apps.
|
||
free_apps() {
|
||
reg=${registry:-$registry_fallback}
|
||
printf '%s\n' "$reg" | awk -F'|' '$1 == "wm" { print $1 "|" $2 }'
|
||
printf '%s\n' "$reg" | awk -F'|' '$7 == "other" { print $1 "|" $2 }'
|
||
}
|
||
is_free() { printf '%s\n' "${registry:-$registry_fallback}" | grep -q "^$1|"; }
|
||
|
||
# --------------------------------------------------------------- network ---
|
||
# curl without redirects; HTTPS only (plain HTTP only to this computer, for
|
||
# a local test service). The email travels in a header file, never in a
|
||
# process argument.
|
||
curl_safe() {
|
||
case "$1" in
|
||
http://127.0.0.1:* | http://localhost:*) shift; curl --proto '=http' --max-redirs 0 "$@" ;;
|
||
*) shift; curl --proto '=https' --tlsv1.2 --max-redirs 0 "$@" ;;
|
||
esac
|
||
}
|
||
# The header file for the email; created once per run, readable only by you.
|
||
email_headers() {
|
||
email_header_file="$scratch/email-headers"
|
||
printf 'X-Makepad-Email: %s\r\nCache-Control: no-store\r\n' "$email" > "$email_header_file"
|
||
}
|
||
# http_get URL FILE [HEADERFILE]: body to FILE; prints the HTTP status.
|
||
http_get() {
|
||
if [ -n "${3:-}" ]; then
|
||
curl_safe "$1" -sS -o "$2" -w '%{http_code}' -H @"$3" "$1" 2>>"${log_file:-/dev/null}" || printf 000
|
||
else
|
||
curl_safe "$1" -sS -o "$2" -w '%{http_code}' "$1" 2>>"${log_file:-/dev/null}" || printf 000
|
||
fi
|
||
}
|
||
# The service's {"error": "..."} as one line.
|
||
http_error() { json flat < "$1" 2>/dev/null | awk -F'\t' '$1 == "error" { print $2; exit }'; }
|
||
|
||
# The newest public Makepad release (free apps and the pinned Rust) from the
|
||
# public catalog; kept in available/makepad.json.
|
||
fetch_public() {
|
||
code=$(http_get "$service/public/catalog" "$scratch/public.json")
|
||
[ "$code" = 200 ] || { public_error="public catalog: HTTP $code $(http_error "$scratch/public.json")"; return 1; }
|
||
json items < "$scratch/public.json" > "$scratch/public.items"
|
||
while IFS= read -r item; do
|
||
printf '%s\n' "$item" > "$scratch/public.one"
|
||
if rel_load "$scratch/public.one" && [ "$rel_public" = true ]; then
|
||
mkdir -p "$root/available"
|
||
mv -f "$scratch/public.one" "$root/available/makepad.json"
|
||
return 0
|
||
fi
|
||
done < "$scratch/public.items"
|
||
public_error='The public Makepad source is unavailable.'
|
||
return 1
|
||
}
|
||
|
||
# The licensed apps of $email: available/<id>.json for each, $licensed_ids
|
||
# in catalog order. lic_state: none (no email), ok, rejected, error.
|
||
check_licenses() {
|
||
licensed_ids= lic_error=
|
||
if [ -z "$email" ]; then lic_state=none; return 0; fi
|
||
email_headers
|
||
code=$(http_get "$service/catalog" "$scratch/catalog.json" "$email_header_file")
|
||
case "$code" in
|
||
200) ;;
|
||
401 | 403) lic_state=rejected lic_error=$(http_error "$scratch/catalog.json"); return 1 ;;
|
||
*) lic_state=error lic_error="HTTP $code $(http_error "$scratch/catalog.json")"; [ "$code" != 000 ] || lic_error='the license service could not be reached'; return 1 ;;
|
||
esac
|
||
json items < "$scratch/catalog.json" > "$scratch/catalog.items"
|
||
[ -z "$root" ] || mkdir -p "$root/available"
|
||
while IFS= read -r item; do
|
||
printf '%s\n' "$item" > "$scratch/catalog.one"
|
||
rel_load "$scratch/catalog.one" || continue
|
||
[ "$rel_public" != true ] || continue
|
||
licensed_ids="$licensed_ids $rel_id"
|
||
[ -z "$root" ] || cp "$scratch/catalog.one" "$root/available/$rel_id.json"
|
||
done < "$scratch/catalog.items"
|
||
lic_state=ok
|
||
put "$scratch/licensed" "$licensed_ids"
|
||
return 0
|
||
}
|
||
license_count() { set -- $licensed_ids; license_count=$#; }
|
||
|
||
# ------------------------------------------------------ releases on disk ---
|
||
# The release file behind an app's row: cached (available/), installed, or
|
||
# the public release projected onto the app's registry entry.
|
||
app_release_file() {
|
||
for arf in "$root/available/$1.json" "$root/installed/$1.json"; do
|
||
[ -f "$arf" ] && { release_file=$arf; return 0; }
|
||
done
|
||
is_free "$1" || return 1
|
||
project_public "$1" "$root/tmp/projected-$1.json" || return 1
|
||
release_file="$root/tmp/projected-$1.json"
|
||
}
|
||
# project_public APP FILE: the public Makepad release as app APP (its
|
||
# package, binary, workspace and features from the registry).
|
||
project_public() {
|
||
rel_load "$root/available/makepad.json" || return 1
|
||
line=$(printf '%s\n' "${registry:-}" | awk -F'|' -v id="$1" '$1 == id { print; exit }')
|
||
[ -n "$line" ] || return 1
|
||
IFS='|' read -r d_id d_title d_package d_binary d_workspace d_features d_menu <<EOF
|
||
$line
|
||
EOF
|
||
[ -n "$d_package" ] || return 1
|
||
mp=$(printf '%s\n' "$rel_repos" | awk -F'|' '$1 == "makepad"')
|
||
IFS='|' read -r m_name m_path m_commit m_sha m_bytes <<EOF
|
||
$mp
|
||
EOF
|
||
features=
|
||
if [ -n "$d_features" ]; then features="\"$(printf '%s' "$d_features" | sed 's/,/","/g')\""; fi
|
||
platforms=
|
||
for p in $rel_platforms; do platforms="$platforms${platforms:+,}\"$p\""; done
|
||
title=$(printf '%s' "$d_title" | tr -d '"\\')
|
||
mkdir -p "$(dirname "$2")"
|
||
put "$2" "{\"id\":\"$d_id\",\"title\":\"$title\",\"release\":\"$rel_release\",\"rust\":\"$rel_rust\",\"package\":\"$d_package\",\"binary\":\"$d_binary\",\"workspace\":\"$d_workspace\",\"platforms\":[$platforms],\"cuda\":false,\"public\":true,\"features\":[$features],\"repositories\":[{\"name\":\"makepad\",\"path\":\"$m_path\",\"commit\":\"$m_commit\",\"bytes\":$m_bytes,\"sha256\":\"$m_sha\"}]}"
|
||
}
|
||
|
||
# repo_installed DIR NAME PATH COMMIT: the receipt names the commit and the
|
||
# checkout is there. The commit identifies the tree, whichever pack it came in.
|
||
repo_installed() {
|
||
[ -d "$1/$3/.git" ] || return 1
|
||
ri_commit=
|
||
read -r ri_commit ri_rest < "$1/.builder-repositories/$2" 2>/dev/null || :
|
||
[ "$ri_commit" = "$4" ]
|
||
}
|
||
# rel_dir: the snapshot folder this release builds from, as the Windows
|
||
# Builder picks it: sources/<release> when it exists, else an existing
|
||
# snapshot holding the same commits (so their build output is shared),
|
||
# preferring one an app was built from; else a new sources/<release>.
|
||
rel_dir() {
|
||
rel_directory="$root/sources/$rel_release"
|
||
[ ! -e "$rel_directory" ] || { rel_label=$rel_release; return 0; }
|
||
rd_best=
|
||
for rd_cand in "$root"/sources/*; do
|
||
[ -d "$rd_cand" ] || continue
|
||
rd_held=0 rd_usable=1
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
if repo_installed "$rd_cand" "$v_name" "$v_path" "$v_commit"; then rd_held=$((rd_held + 1))
|
||
elif [ -e "$rd_cand/$v_path" ]; then rd_usable=0; break; fi
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
[ "$rd_usable" = 1 ] && [ "$rd_held" -gt 0 ] || continue
|
||
rd_built=0; [ ! -f "$rd_cand/.builder-built" ] || rd_built=1
|
||
rd_best="$rd_best$rd_built $((1000 + rd_held)) ${rd_cand##*/}|$rd_cand
|
||
"
|
||
done
|
||
if [ -n "$rd_best" ]; then
|
||
rel_directory=$(printf '%s' "$rd_best" | sort | tail -n 1 | cut -d'|' -f2-)
|
||
fi
|
||
rel_label=${rel_directory##*/}
|
||
}
|
||
rel_installed() {
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
repo_installed "$rel_directory" "$v_name" "$v_path" "$v_commit" || return 1
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
[ -f "$rel_directory/$rel_workspace/Cargo.toml" ]
|
||
}
|
||
rel_target() { rel_target_dir="$root/target/sources/$rel_label"; }
|
||
|
||
# app_state ID: new, partial, compile, update, ready or merge, from the
|
||
# folder alone (the state machine of the Windows Builder).
|
||
app_state() {
|
||
as_id=$1
|
||
if [ -f "$root/changes/$as_id.merge" ]; then state=merge; return; fi
|
||
app_release_file "$as_id" && rel_load "$release_file" || { state=new; return; }
|
||
rel_dir
|
||
as_bin="$root/$rel_binary.bin"
|
||
as_installed=
|
||
if [ -f "$root/installed/$as_id.json" ]; then
|
||
as_installed=$(json flat < "$root/installed/$as_id.json" | awk -F'\t' '$1 == "release" { print $2; exit }')
|
||
fi
|
||
if [ "$as_installed" = "$rel_release" ] && [ -f "$as_bin" ]; then
|
||
if sources_changed; then state=compile; else state=ready; fi
|
||
elif rel_installed; then state=compile
|
||
elif [ -n "$as_installed" ]; then state=update
|
||
elif [ -n "$(ls -A "$rel_directory/.builder-repositories" 2>/dev/null)" ] || ls "$root"/cache/*.pack.part* >/dev/null 2>&1; then state=partial
|
||
else state=new; fi
|
||
}
|
||
# Whether a source file Cargo used for the built app (its .d file) is newer
|
||
# than the app: an agent or the person edited it, so it compiles again.
|
||
sources_changed() {
|
||
rel_target
|
||
sc_deps="$rel_target_dir/release/$rel_binary.d"
|
||
[ -f "$sc_deps" ] || return 1
|
||
# Its first line: "target: source source ...", spaces in paths as "\ ".
|
||
sc_list=$(awk 'NR == 1 { sub(/^[^:]*: /, ""); gsub(/\\ /, "\001"); n = split($0, a, " "); for (i = 1; i <= n; i++) { gsub(/\001/, " ", a[i]); print a[i] } exit }' "$sc_deps")
|
||
while IFS= read -r sc_file; do
|
||
[ -n "$sc_file" ] || continue
|
||
[ -e "$sc_file" ] || return 0
|
||
[ ! "$sc_file" -nt "$as_bin" ] || return 0
|
||
done <<EOF
|
||
$sc_list
|
||
EOF
|
||
return 1
|
||
}
|
||
|
||
# ------------------------------------------------------------ compiler ---
|
||
# The recorded choice (selected-rust): "private", or the sysroot of your own
|
||
# Rust. rust-check below validates it on every start and before each build.
|
||
rust_version() {
|
||
rv=
|
||
if rel_load "$root/available/makepad.json" 2>/dev/null; then rv=$rel_rust; fi
|
||
rust_version=$rv
|
||
}
|
||
private_rust_dir() { private_rust="$root/toolchain/rust/$1-$triple"; }
|
||
private_rust_ready() {
|
||
private_rust_dir "$1"
|
||
[ "$(cat "$private_rust/.toolchain-version" 2>/dev/null || :)" = "$1 $triple" ] &&
|
||
[ -x "$private_rust/bin/rustc" ] && [ -x "$private_rust/bin/cargo" ]
|
||
}
|
||
recorded_rust() { selected_rust=$(cat "$root/selected-rust" 2>/dev/null || :); }
|
||
record_rust() { put "$root/selected-rust" "$1"; }
|
||
# rust_ready VERSION: sets rust_sysroot when the chosen Rust can build.
|
||
rust_ready() {
|
||
recorded_rust
|
||
rust_sysroot=
|
||
case "$selected_rust" in
|
||
/*) rust_check --validate "$1" "$selected_rust" && return 0; return 1 ;;
|
||
*) private_rust_ready "$1" && rust_sysroot=$private_rust ;;
|
||
esac
|
||
}
|
||
# rust_check --probe MIN | --validate MIN SYSROOT: read-only. Prints nothing;
|
||
# sets rust_sysroot, or rust_reason. Only the toolchain's own binaries run,
|
||
# so no rustup proxy can download or switch anything.
|
||
rust_check() {
|
||
rust_reason= rust_sysroot=
|
||
if [ "$1" = --probe ]; then
|
||
rc_toolchain=$(cd / && unset RUSTUP_TOOLCHAIN && RUSTUP_AUTO_INSTALL=0 rustc --print sysroot 2>/dev/null) || { rust_reason='No installed Rust was found on PATH.'; return 1; }
|
||
[ -n "$rc_toolchain" ] || { rust_reason='No installed Rust was found on PATH.'; return 1; }
|
||
else
|
||
rc_toolchain=$3
|
||
fi
|
||
rc_min=$2
|
||
case "$rc_toolchain" in /*) ;; *) rust_reason='The recorded Rust location is not an absolute path.'; return 1 ;; esac
|
||
[ -d "$rc_toolchain" ] || { rust_reason="Rust is no longer installed at $rc_toolchain."; return 1; }
|
||
rc_toolchain=$(cd -P -- "$rc_toolchain" && pwd)
|
||
[ -x "$rc_toolchain/bin/rustc" ] || { rust_reason="No rustc in $rc_toolchain/bin."; return 1; }
|
||
rc_meta=$("$rc_toolchain/bin/rustc" --version --verbose 2>/dev/null) || { rust_reason="rustc in $rc_toolchain/bin does not run."; return 1; }
|
||
rc_version=$(printf '%s\n' "$rc_meta" | awk '$1 == "release:" { print $2 }')
|
||
rc_host=$(printf '%s\n' "$rc_meta" | awk '$1 == "host:" { print $2 }')
|
||
[ "$rc_host" = "$triple" ] || { rust_reason="Installed Rust is built for ${rc_host:-an unknown host}; this Builder needs $triple."; return 1; }
|
||
rc_status=0
|
||
version_at_least "$rc_version" "$rc_min" || rc_status=$?
|
||
case "$rc_status" in
|
||
0) ;;
|
||
1) rust_reason="Installed Rust $rc_version is not a stable release; the Builder needs $rc_min or newer."; return 1 ;;
|
||
*) rust_reason="Installed Rust $rc_version is older than $rc_min."; return 1 ;;
|
||
esac
|
||
[ -x "$rc_toolchain/bin/cargo" ] || { rust_reason='Installed Rust has no cargo beside rustc.'; return 1; }
|
||
rc_cargo=$("$rc_toolchain/bin/cargo" --version 2>/dev/null | awk '$1 == "cargo" { print $2 }') || :
|
||
version_at_least "${rc_cargo:-none}" "$rc_min" || { rust_reason="Installed cargo ${rc_cargo:-is unusable and} is older than $rc_min."; return 1; }
|
||
[ -d "$rc_toolchain/lib/rustlib/$triple/lib" ] || { rust_reason="Installed Rust has no standard library for $triple."; return 1; }
|
||
if [ "$plat" = mac ]; then
|
||
# Release builds strip through rustc's bundled rust-objcopy; report a
|
||
# copy that cannot load its LLVM library, never repair it.
|
||
"$rc_toolchain/lib/rustlib/$triple/bin/rust-objcopy" --version >/dev/null 2>&1 || { rust_reason="Installed Rust's rust-objcopy does not run; release builds could not be stripped."; return 1; }
|
||
fi
|
||
rust_sysroot=$rc_toolchain
|
||
}
|
||
# version_at_least ACTUAL MINIMUM: 0 when newer or equal, 1 when not a
|
||
# stable x.y.z, 2 when older. Components compare as numbers.
|
||
version_at_least() {
|
||
awk -v actual="$1" -v minimum="$2" 'BEGIN {
|
||
if (actual !~ /^[0-9]+[.][0-9]+[.][0-9]+$/) exit 1
|
||
split(actual, a, "."); split(minimum, b, ".")
|
||
for (i = 1; i <= 3; i++) { if (a[i] + 0 > b[i] + 0) exit 0; if (a[i] + 0 < b[i] + 0) exit 2 }
|
||
exit 0
|
||
}'
|
||
}
|
||
# macOS: rust-objcopy finds LLVM beside rustlib's host tools. A relative
|
||
# link keeps the folder movable.
|
||
prepare_host_tools() {
|
||
[ "$plat" = mac ] || return 0
|
||
pht_lib="$1/lib/rustlib/$triple/lib"
|
||
if [ -f "$1/lib/libLLVM.dylib" ] && [ ! -e "$pht_lib/libLLVM.dylib" ]; then
|
||
mkdir -p "$pht_lib"
|
||
ln -s ../../../libLLVM.dylib "$pht_lib/libLLVM.dylib"
|
||
fi
|
||
}
|
||
|
||
# System tools: Apple's command line developer tools on macOS, the
|
||
# distribution's development packages on Linux. Read-only; tools_problem is
|
||
# empty when ready, else missing, license or unsupported.
|
||
tools_check() {
|
||
tools_problem=
|
||
if [ "$plat" = mac ]; then
|
||
tc_dev=${DEVELOPER_DIR:-$(/usr/bin/xcode-select -p 2>/dev/null || :)}
|
||
if [ -z "$tc_dev" ] || [ ! -d "$tc_dev" ]; then tools_problem=missing; return 1; fi
|
||
for tc_probe in 'clang --version' '--show-sdk-path' '--find ld'; do
|
||
# shellcheck disable=SC2086 # the probe words are fixed above
|
||
if ! tc_detail=$(/usr/bin/xcrun --sdk macosx $tc_probe 2>&1); then
|
||
case "$tc_detail" in *license*) tools_problem=license ;; *) tools_problem=missing ;; esac
|
||
return 1
|
||
fi
|
||
done
|
||
git --version >/dev/null 2>&1 || { tools_problem=missing; return 1; }
|
||
else
|
||
getconf GNU_LIBC_VERSION >/dev/null 2>&1 || { tools_problem=unsupported; return 1; }
|
||
for tc_tool in cc c++ make cmake git curl tar gzip pkg-config; do
|
||
command -v "$tc_tool" >/dev/null 2>&1 || { tools_problem=missing; return 1; }
|
||
done
|
||
pkg-config --exists openssl x11 xcursor xkbcommon xrandr xi xinerama alsa libpulse \
|
||
wayland-client egl gl glx libdrm gbm || { tools_problem=missing; return 1; }
|
||
fi
|
||
command -v curl >/dev/null 2>&1 || { tools_problem=missing; return 1; }
|
||
return 0
|
||
}
|
||
# Linux: the one command that installs the development packages, shown
|
||
# before it runs. Sets packages_cmd (without sudo) and distro_name.
|
||
linux_packages() {
|
||
ID= ID_LIKE= PRETTY_NAME=
|
||
# shellcheck disable=SC1091
|
||
[ ! -r /etc/os-release ] || . /etc/os-release
|
||
distro_name=${PRETTY_NAME:-your distribution}
|
||
case " $ID $ID_LIKE " in
|
||
*debian* | *ubuntu*) packages_cmd='apt-get install -y --no-install-recommends build-essential clang cmake pkg-config git curl ca-certificates tar gzip libssl-dev libx11-dev libxcursor-dev libxkbcommon-dev libxrandr-dev libxi-dev libxinerama-dev libasound2-dev libpulse-dev libwayland-dev wayland-protocols libegl-dev libgl-dev libglx-dev libdrm-dev libgbm-dev' ;;
|
||
*fedora* | *rhel* | *centos*) packages_cmd='dnf install -y gcc gcc-c++ make clang cmake pkgconf-pkg-config git curl ca-certificates tar gzip openssl-devel libX11-devel libXcursor-devel libxkbcommon-devel libXrandr-devel libXi-devel libXinerama-devel alsa-lib-devel pulseaudio-libs-devel wayland-devel wayland-protocols-devel libglvnd-devel libdrm-devel mesa-libgbm-devel' ;;
|
||
*arch*) packages_cmd='pacman -S --needed --noconfirm base-devel clang cmake pkgconf git curl ca-certificates tar gzip openssl libx11 libxcursor libxkbcommon libxrandr libxi libxinerama alsa-lib libpulse wayland wayland-protocols libglvnd mesa libdrm' ;;
|
||
*suse*) packages_cmd='zypper --non-interactive install gcc gcc-c++ make clang cmake pkg-config git curl ca-certificates tar gzip libopenssl-devel libX11-devel libXcursor-devel libxkbcommon-devel libXrandr-devel libXi-devel libXinerama-devel alsa-devel libpulse-devel wayland-devel wayland-protocols-devel libglvnd-devel libdrm-devel Mesa-libgbm-devel' ;;
|
||
*) packages_cmd= ; return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# ------------------------------------------------------- build environment ---
|
||
# rustc's parallel frontend (-Zthreads, allowed on the stable compiler by
|
||
# RUSTC_BOOTSTRAP=1) compiles about a third faster; the Windows Builder and
|
||
# this script use it the same way. The thread count lives in rustc-threads,
|
||
# written once per folder: the processor count, at most 8. 1 means one
|
||
# thread: the parallel frontend failed here once, and the file says why.
|
||
# Every cargo run (the Builder's builds, `makepad build`, agent shells,
|
||
# `makepad env`) gets the same flags, so the shared target stays fresh.
|
||
rustc_threads() {
|
||
rustc_threads=
|
||
[ ! -f "$root/rustc-threads" ] || read -r rustc_threads rt_rest < "$root/rustc-threads" || :
|
||
case "$rustc_threads" in '' | *[!0-9]* | 0) rustc_threads= ;; esac
|
||
if [ -z "$rustc_threads" ]; then
|
||
rustc_threads=$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 1)
|
||
case "$rustc_threads" in '' | *[!0-9]* | 0) rustc_threads=1 ;; esac
|
||
[ "$rustc_threads" -le 8 ] || rustc_threads=8
|
||
put "$root/rustc-threads" "$rustc_threads"
|
||
fi
|
||
}
|
||
cargo_env() { # cargo_env LABEL APP [THREADS]: exports into the current (sub)shell
|
||
# Inherited overrides would move Cargo's output or change its flags from
|
||
# one session to the next, rebuilding everything.
|
||
unset MAKEPAD_LOADER_EMAIL RUSTUP_TOOLCHAIN CARGO_ENCODED_RUSTFLAGS RUSTC_WRAPPER \
|
||
RUSTC_WORKSPACE_WRAPPER CARGO_INCREMENTAL CARGO_BUILD_TARGET CARGO_BUILD_TARGET_DIR \
|
||
CARGO_BUILD_RUSTFLAGS CARGO_BUILD_RUSTDOCFLAGS CARGO_BUILD_DEP_INFO_BASEDIR \
|
||
CARGO_TARGET_APPLIES_TO_HOST CARGO_UNSTABLE_BUILD_STD RUSTDOC 2>/dev/null || :
|
||
for ce_var in $(env | sed -n 's/^\(CARGO_PROFILE_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$ce_var"; done
|
||
mkdir -p "$root/cargo-home" "$root/tmp" "$root/target/sources/$1"
|
||
export CARGO_HOME="$root/cargo-home"
|
||
export CARGO_TARGET_DIR="$root/target/sources/$1"
|
||
export RUSTC="$rust_sysroot/bin/rustc" CARGO="$rust_sysroot/bin/cargo"
|
||
export TMPDIR="$root/tmp" TMP="$root/tmp" TEMP="$root/tmp"
|
||
export RUSTUP_HOME="$root/rustup-home" RUSTUP_AUTO_INSTALL=0 RUSTUP_TOOLCHAIN=
|
||
export MAKEPAD_LOADER_EMAIL= MAKEPAD_PACKAGE_DIR=. MAKEPAD_BUILDER_APP="$2"
|
||
export CUDA_PATH= CUDA_HOME= CUDACXX= MAKEPAD_GGML_NO_CUDA=1 MAKEPAD_GGML_REQUIRE_CUDA=0
|
||
# Pinned: the shell's RUSTFLAGS would change every fingerprint in the
|
||
# shared target.
|
||
if [ -n "${3:-}" ]; then ce_threads=$3; else rustc_threads; ce_threads=$rustc_threads; fi
|
||
if [ "$ce_threads" -gt 1 ]; then export RUSTC_BOOTSTRAP=1 RUSTFLAGS="-Zthreads=$ce_threads"
|
||
else export RUSTC_BOOTSTRAP= RUSTFLAGS=; fi
|
||
if [ -x "$rust_sysroot/bin/rustdoc" ]; then export RUSTDOC="$rust_sysroot/bin/rustdoc"; fi
|
||
export PATH="$rust_sysroot/bin:$root/cargo-home/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
||
}
|
||
|
||
# prepare_target: the snapshot's target directory. A folder from before the
|
||
# per-snapshot layout has Cargo's output directly in target/; the snapshot
|
||
# built last adopts it, so its apps do not compile again.
|
||
prepare_target() {
|
||
rel_target
|
||
if [ ! -e "$rel_target_dir" ] && { [ -d "$root/target/release" ] || [ -f "$root/target/.rustc_info.json" ]; }; then
|
||
pt_newest=$(ls -t "$root"/sources/*/.builder-built 2>/dev/null | head -n 1)
|
||
if [ -z "$pt_newest" ] || [ "${pt_newest%/.builder-built}" = "$rel_directory" ]; then
|
||
mkdir -p "$rel_target_dir"
|
||
for pt_entry in "$root"/target/* "$root"/target/.[!.]*; do
|
||
[ -e "$pt_entry" ] || continue
|
||
[ "${pt_entry##*/}" != sources ] || continue
|
||
mv "$pt_entry" "$rel_target_dir/"
|
||
done
|
||
fi
|
||
fi
|
||
mkdir -p "$rel_target_dir"
|
||
}
|
||
|
||
# ------------------------------------------------------------ downloads ---
|
||
# Byte ranges over parallel connections for large files, when the host
|
||
# offers them; every curl of this run shares one cap of $max_connections.
|
||
max_connections=8
|
||
slot_take() {
|
||
while :; do
|
||
st_i=0
|
||
while [ "$st_i" -lt "$max_connections" ]; do
|
||
if mkdir "$slots/$st_i" 2>/dev/null; then slot=$st_i; return 0; fi
|
||
st_i=$((st_i + 1))
|
||
done
|
||
sleep 0.1
|
||
done
|
||
}
|
||
slot_give() { rmdir "$slots/$1" 2>/dev/null || :; }
|
||
|
||
# fetch URL FILE SIZE RANGES [CURL-ARGS...]: downloads FILE in the background.
|
||
# RANGES=yes splits a known SIZE into parts FILE.part0..N (up to 8, at least
|
||
# 8 MB each), each resumed from what an earlier run left; otherwise one
|
||
# connection into FILE.part0, resumed when the host allows. FILE.status
|
||
# reads "running" until done, then 0 or 1. Sets fetch_parts.
|
||
fetch() {
|
||
f_url=$1 f_file=$2 f_size=$3 f_ranges=$4; shift 4
|
||
fetch_parts=1
|
||
if [ "$f_ranges" = yes ] && [ "$f_size" -gt 16777216 ]; then
|
||
fetch_parts=$((f_size / 8388608))
|
||
[ "$fetch_parts" -le 8 ] || fetch_parts=8
|
||
fi
|
||
printf 'running\n' > "$f_file.status"
|
||
(
|
||
f_pids=
|
||
f_part_size=$(( (f_size + fetch_parts - 1) / fetch_parts ))
|
||
f_i=0
|
||
while [ "$f_i" -lt "$fetch_parts" ]; do
|
||
f_part="$f_file.part$f_i"
|
||
if [ "$fetch_parts" = 1 ]; then
|
||
(
|
||
slot_take
|
||
trap 'slot_give $slot' EXIT
|
||
if [ -s "$f_part" ] && [ "$f_size" -gt 0 ] && [ "$(size_of "$f_part")" -ge "$f_size" ]; then exit 0; fi
|
||
if [ -s "$f_part" ] && curl_safe "$f_url" -sS -f -C - -o "$f_part" "$@" "$f_url" 2>>"$log_file"; then exit 0; fi
|
||
# No resume from this host: from the start. Without -f
|
||
# the service's error text lands in the part, for the
|
||
# step's reason; the HTTP status says which it is.
|
||
: > "$f_part"
|
||
f_code=$(curl_safe "$f_url" -sS -o "$f_part" -w '%{http_code}' "$@" "$f_url" 2>>"$log_file") || f_code=000
|
||
printf '%s\n' "$f_code" > "$f_file.code"
|
||
case "$f_code" in 200 | 206) exit 0 ;; *) exit 1 ;; esac
|
||
) &
|
||
f_pids="$f_pids $!"
|
||
else
|
||
f_from=$((f_i * f_part_size))
|
||
f_to=$((f_from + f_part_size - 1))
|
||
[ "$f_to" -lt "$f_size" ] || f_to=$((f_size - 1))
|
||
f_have=$(size_of "$f_part")
|
||
if [ "$f_have" -lt $((f_to - f_from + 1)) ]; then
|
||
(
|
||
slot_take
|
||
trap 'slot_give $slot' EXIT
|
||
# A part cut off earlier continues after its last byte.
|
||
curl_safe "$f_url" -sS -f --range "$((f_from + f_have))-$f_to" "$@" "$f_url" >> "$f_part" 2>>"$log_file"
|
||
) &
|
||
f_pids="$f_pids $!"
|
||
fi
|
||
fi
|
||
f_i=$((f_i + 1))
|
||
done
|
||
f_status=0
|
||
for f_pid in $f_pids; do wait "$f_pid" || f_status=1; done
|
||
printf '%s\n' "$f_status" > "$f_file.status"
|
||
) &
|
||
}
|
||
# fetch_bytes FILE PARTS: bytes of FILE's parts so far.
|
||
fetch_bytes() {
|
||
fb_sum=0 fb_i=0
|
||
while [ "$fb_i" -lt "$2" ]; do
|
||
[ ! -f "$1.part$fb_i" ] || fb_sum=$((fb_sum + $(size_of "$1.part$fb_i")))
|
||
fb_i=$((fb_i + 1))
|
||
done
|
||
fetch_bytes=$fb_sum
|
||
}
|
||
# fetch_join FILE PARTS SIZE: the parts become FILE.
|
||
fetch_join() {
|
||
if [ "$2" = 1 ]; then mv -f "$1.part0" "$1"; else
|
||
fj_i=0
|
||
: > "$1.joined"
|
||
while [ "$fj_i" -lt "$2" ]; do cat "$1.part$fj_i" >> "$1.joined"; fj_i=$((fj_i + 1)); done
|
||
[ "$3" = 0 ] || [ "$(size_of "$1.joined")" = "$3" ] || return 1
|
||
mv -f "$1.joined" "$1"
|
||
fj_i=0
|
||
while [ "$fj_i" -lt "$2" ]; do rm -f "$1.part$fj_i"; fj_i=$((fj_i + 1)); done
|
||
fi
|
||
rm -f "$1.status" "$1.code"
|
||
}
|
||
# head_size URL: Content-Length when the host offers byte ranges, else 0.
|
||
head_size() {
|
||
hs=$(curl_safe "$1" -sS -I "$1" 2>/dev/null | tr -d '\r') || hs=
|
||
if printf '%s\n' "$hs" | grep -qi '^accept-ranges: *bytes'; then
|
||
printf '%s\n' "$hs" | awk 'tolower($1) == "content-length:" { n = $2 } END { print n + 0 }'
|
||
else
|
||
printf 0
|
||
fi
|
||
}
|
||
|
||
# ----------------------------------------------------------------- steps ---
|
||
# A work page lists steps, one per line: ○ next, ● now with its bar, ✓ done
|
||
# with a summary, ✗ failed with the reason, ◌ held. Each step's state is one
|
||
# line in a file, written by the step itself (background jobs included) and
|
||
# drawn about ten times a second:
|
||
# now|DONE|TOTAL|UNIT|BYTES|DOING done|SUMMARY failed|REASON held|REASON
|
||
step_key() { step_key=$(printf '%s' "$1" | tr -c 'A-Za-z0-9' '_'); }
|
||
step() { # step LABEL LINE
|
||
step_key "$1"
|
||
put "$steps_dir/$step_key" "$2"
|
||
}
|
||
step_fail() { step "$1" "failed|$2"; log "$1 failed: $2"; }
|
||
|
||
# ------------------------------------------------------------ Rust job ---
|
||
# rustc, rust-std and cargo download side by side, the large ones in byte
|
||
# ranges, each checked against its SHA-256 and unpacked in the background as
|
||
# soon as it is in. One bar counts every archive once downloaded and once
|
||
# unpacked. A stopped run keeps its parts and staging folder.
|
||
job_rust() { # job_rust LABEL VERSION
|
||
jr_label=$1 jr_version=$2
|
||
jr_started=$(now)
|
||
private_rust_dir "$jr_version"
|
||
mkdir -p "$root/cache" "$root/toolchain/rust" "$root/tmp"
|
||
step "$jr_label" "now|0|0|MB|0|reading the manifest"
|
||
jr_manifest="$root/cache/channel-rust-$jr_version.toml"
|
||
if [ ! -s "$jr_manifest" ]; then
|
||
curl_safe "https://static.rust-lang.org" -sS -f -o "$jr_manifest.part" "https://static.rust-lang.org/dist/channel-rust-$jr_version.toml" 2>>"$log_file" ||
|
||
{ step_fail "$jr_label" 'Could not download the Rust manifest'; return 1; }
|
||
mv -f "$jr_manifest.part" "$jr_manifest"
|
||
fi
|
||
jr_staging="$root/toolchain/rust/.staging-$jr_version-$triple"
|
||
mkdir -p "$jr_staging"
|
||
jr_n=0
|
||
for jr_component in rustc rust-std cargo; do
|
||
jr_n=$((jr_n + 1))
|
||
jr_section="[pkg.$jr_component.target.$triple]"
|
||
jr_url=$(awk -v section="$jr_section" -v key=url '$0 == section { inside = 1; next } /^\[/ { inside = 0 } inside && $1 == key && $2 == "=" { gsub(/"/, "", $3); print $3; exit }' "$jr_manifest")
|
||
jr_hash=$(awk -v section="$jr_section" -v key=hash '$0 == section { inside = 1; next } /^\[/ { inside = 0 } inside && $1 == key && $2 == "=" { gsub(/"/, "", $3); print $3; exit }' "$jr_manifest")
|
||
case "$jr_url" in https://static.rust-lang.org/dist/*.tar.gz) ;; *) step_fail "$jr_label" "Unexpected Rust download address for $jr_component"; return 1 ;; esac
|
||
hex "$jr_hash" 64 || { step_fail "$jr_label" "Invalid Rust checksum for $jr_component"; return 1; }
|
||
jr_archive="$root/cache/$jr_hash.tar.gz"
|
||
eval "jr_url_$jr_n=\$jr_url jr_hash_$jr_n=\$jr_hash jr_archive_$jr_n=\$jr_archive jr_name_$jr_n=\$jr_component"
|
||
if [ -f "$jr_archive" ] && [ "$(sha256 "$jr_archive")" = "$jr_hash" ]; then
|
||
eval "jr_size_$jr_n=$(size_of "$jr_archive") jr_state_$jr_n=ready"
|
||
else
|
||
jr_size=$(head_size "$jr_url")
|
||
fetch "$jr_url" "$jr_archive" "$jr_size" yes
|
||
eval "jr_size_$jr_n=\$jr_size jr_parts_$jr_n=\$fetch_parts jr_state_$jr_n=down"
|
||
fi
|
||
done
|
||
while :; do
|
||
jr_busy=0 jr_total=0 jr_done=0 jr_down=0 jr_unpack=0
|
||
jr_n=0
|
||
while [ "$jr_n" -lt 3 ]; do
|
||
jr_n=$((jr_n + 1))
|
||
eval "jr_state=\$jr_state_$jr_n jr_size=\$jr_size_$jr_n jr_archive=\$jr_archive_$jr_n jr_hash=\$jr_hash_$jr_n jr_url=\$jr_url_$jr_n jr_component=\$jr_name_$jr_n"
|
||
case "$jr_state" in
|
||
down)
|
||
eval "jr_parts=\$jr_parts_$jr_n"
|
||
case "$(cat "$jr_archive.status" 2>/dev/null || echo running)" in
|
||
running) jr_down=$((jr_down + 1)); fetch_bytes "$jr_archive" "$jr_parts"; jr_done=$((jr_done + fetch_bytes)) ;;
|
||
0)
|
||
fetch_join "$jr_archive" "$jr_parts" "$jr_size" || { step_fail "$jr_label" "Download failed: $jr_component"; return 1; }
|
||
[ "$(sha256 "$jr_archive")" = "$jr_hash" ] || { rm -f "$jr_archive"; step_fail "$jr_label" "The $jr_component download does not match its checksum"; return 1; }
|
||
jr_size=$(size_of "$jr_archive")
|
||
eval "jr_size_$jr_n=$jr_size jr_state_$jr_n=ready"
|
||
jr_done=$((jr_done + jr_size)) ;;
|
||
*) step_fail "$jr_label" "Download failed: $jr_component (see builder.log)"; return 1 ;;
|
||
esac ;;
|
||
ready)
|
||
# An official archive holds <name>/<component>/{bin,lib,...};
|
||
# only that inner folder is unpacked. cargo's manifest.in is kept.
|
||
jr_top=$(basename "$jr_url" .tar.gz)
|
||
jr_inner=$jr_component
|
||
[ "$jr_component" != rust-std ] || jr_inner=rust-std-$triple
|
||
jr_keep=
|
||
[ "$jr_component" = cargo ] || jr_keep="--exclude=$jr_top/$jr_inner/manifest.in"
|
||
printf 'running\n' > "$root/tmp/rust-$jr_n.unpack"
|
||
(
|
||
jr_status=0
|
||
# shellcheck disable=SC2086 # jr_keep is one word or none
|
||
tar -xzf "$jr_archive" -C "$jr_staging" --strip-components=2 $jr_keep "$jr_top/$jr_inner" 2>>"$log_file" || jr_status=1
|
||
printf '%s\n' "$jr_status" > "$root/tmp/rust-$jr_n.unpack"
|
||
) &
|
||
eval "jr_state_$jr_n=unpack"
|
||
jr_unpack=$((jr_unpack + 1))
|
||
jr_done=$((jr_done + jr_size)) ;;
|
||
unpack)
|
||
jr_done=$((jr_done + jr_size))
|
||
case "$(cat "$root/tmp/rust-$jr_n.unpack")" in
|
||
running) jr_unpack=$((jr_unpack + 1)) ;;
|
||
0) eval "jr_state_$jr_n=done"; jr_done=$((jr_done + jr_size)) ;;
|
||
*) step_fail "$jr_label" "Unpacking $jr_component failed"; return 1 ;;
|
||
esac ;;
|
||
done) jr_done=$((jr_done + 2 * jr_size)) ;;
|
||
esac
|
||
jr_total=$((jr_total + 2 * jr_size))
|
||
eval "jr_state=\$jr_state_$jr_n"
|
||
[ "$jr_state" = done ] || jr_busy=1
|
||
done
|
||
if [ "$jr_down" -gt 0 ]; then jr_doing=downloading
|
||
elif [ "$jr_unpack" -gt 0 ]; then jr_doing=unpacking
|
||
else jr_doing=checking; fi
|
||
step "$jr_label" "now|$jr_done|$jr_total|MB|$((jr_total / 2))|$jr_doing"
|
||
[ "$jr_busy" = 1 ] || break
|
||
sleep 0.2
|
||
done
|
||
wait
|
||
prepare_host_tools "$jr_staging"
|
||
case "$("$jr_staging/bin/rustc" --version 2>/dev/null)" in
|
||
"rustc $jr_version "*) ;;
|
||
*) step_fail "$jr_label" 'The private Rust did not pass its version check'; return 1 ;;
|
||
esac
|
||
"$jr_staging/bin/cargo" --version >/dev/null 2>&1 || { step_fail "$jr_label" 'The private cargo does not run'; return 1; }
|
||
printf '%s' "$jr_version $triple" > "$jr_staging/.toolchain-version"
|
||
if [ -e "$private_rust" ]; then remove_inside "$private_rust"; fi
|
||
mv "$jr_staging" "$private_rust"
|
||
log "Rust $jr_version installed: $((jr_total / 2 / 1048576)) MB in $(($(now) - jr_started)) s"
|
||
step "$jr_label" "done|$((jr_total / 2 / 1048576)) MB · $(($(now) - jr_started)) s"
|
||
}
|
||
|
||
# ----------------------------------------------------------- source job ---
|
||
# One repository of a release: its pack is downloaded (public ones in byte
|
||
# ranges when the host offers them), checked against the catalog's size and
|
||
# SHA-256, and checked out in a staging folder that is renamed into place;
|
||
# a repository nested inside another starts writing once its parent is
|
||
# there. Unpacking one pack overlaps the downloads of the others.
|
||
job_pack() { # job_pack LABEL DEST NAME PATH COMMIT SHA BYTES APP RELEASE PUBLIC HAVE PARENT
|
||
jp_label=$1 jp_dest=$2 jp_name=$3 jp_path=$4 jp_commit=$5 jp_sha=$6 jp_bytes=$7 jp_app=$8 jp_release=$9
|
||
shift 9
|
||
jp_public=$1 jp_have=$2 jp_parent=$3
|
||
jp_started=$(now)
|
||
jp_pack="$root/cache/$jp_sha.pack"
|
||
mkdir -p "$root/cache" "$jp_dest/.builder-repositories"
|
||
if [ -f "$jp_pack" ] && [ "$(size_of "$jp_pack")" = "$jp_bytes" ] && [ "$(sha256 "$jp_pack")" = "$jp_sha" ]; then
|
||
:
|
||
else
|
||
if [ "$jp_public" = true ]; then
|
||
jp_url="$service/public/source/$jp_release/makepad.pack"
|
||
jp_ranges=no
|
||
[ "$(head_size "$jp_url")" = "$jp_bytes" ] && jp_ranges=yes
|
||
fetch "$jp_url" "$jp_pack" "$jp_bytes" "$jp_ranges"
|
||
else
|
||
# Licensed sources: one connection (each request counts toward the
|
||
# download limit); an update names the pack it replaces.
|
||
jp_url="$service/source/$jp_app/$jp_release/$jp_name.pack"
|
||
if [ -n "$jp_have" ]; then
|
||
fetch "$jp_url" "$jp_pack" "$jp_bytes" no -X POST -H 'Content-Length: 0' -H @"$email_header_file" -H "X-Makepad-Have: $jp_have"
|
||
else
|
||
fetch "$jp_url" "$jp_pack" "$jp_bytes" no -X POST -H 'Content-Length: 0' -H @"$email_header_file"
|
||
fi
|
||
fi
|
||
jp_parts=$fetch_parts
|
||
while :; do
|
||
jp_status=$(cat "$jp_pack.status" 2>/dev/null || echo running)
|
||
fetch_bytes "$jp_pack" "$jp_parts"
|
||
step "$jp_label" "now|$fetch_bytes|$((2 * jp_bytes))|MB|$jp_bytes|downloading"
|
||
[ "$jp_status" = running ] || break
|
||
sleep 0.2
|
||
done
|
||
if [ "$jp_status" != 0 ]; then
|
||
if [ "$(cat "$jp_pack.code" 2>/dev/null || echo 200)" != 200 ] && [ -s "$jp_pack.part0" ] && [ "$(size_of "$jp_pack.part0")" -lt 2048 ]; then
|
||
jp_reason=$(http_error "$jp_pack.part0"); : > "$jp_pack.part0"
|
||
step_fail "$jp_label" "${jp_reason:-Download failed}"
|
||
else
|
||
step_fail "$jp_label" 'Download failed (see builder.log)'
|
||
fi
|
||
return 1
|
||
fi
|
||
fetch_join "$jp_pack" "$jp_parts" "$jp_bytes" || { step_fail "$jp_label" 'Download failed size verification'; return 1; }
|
||
step "$jp_label" "now|$jp_bytes|$((2 * jp_bytes))|MB|$jp_bytes|checking"
|
||
if [ "$(size_of "$jp_pack")" != "$jp_bytes" ] || [ "$(sha256 "$jp_pack")" != "$jp_sha" ]; then
|
||
rm -f "$jp_pack"
|
||
step_fail "$jp_label" "$jp_name download failed size/hash verification"
|
||
return 1
|
||
fi
|
||
fi
|
||
# A nested repository waits for its parent's checkout.
|
||
if [ -n "$jp_parent" ]; then
|
||
while [ ! -f "$jp_dest/.builder-repositories/$jp_parent" ]; do
|
||
step_key "$jp_parent source"
|
||
case "$(cut -d'|' -f1 "$steps_dir/$step_key" 2>/dev/null || :)" in
|
||
failed | held) step "$jp_label" "held|stopped because $jp_parent failed"; return 1 ;;
|
||
esac
|
||
step "$jp_label" "now|$jp_bytes|$((2 * jp_bytes))|MB|$jp_bytes|waiting for $jp_parent"
|
||
sleep 0.2
|
||
done
|
||
fi
|
||
jp_checkout="$jp_dest/$jp_path"
|
||
if [ -e "$jp_checkout" ]; then
|
||
step_fail "$jp_label" "Existing source at $jp_checkout has no matching receipt; it was left unchanged"
|
||
return 1
|
||
fi
|
||
jp_stage="$jp_dest/.$jp_name-staging"
|
||
[ ! -e "$jp_stage" ] || remove_inside "$jp_stage"
|
||
step "$jp_label" "now|$jp_bytes|$((2 * jp_bytes))|MB|$jp_bytes|unpacking"
|
||
git init -q "$jp_stage" 2>>"$log_file"
|
||
[ "$jp_name" != makepad ] || git -C "$jp_stage" config remote.origin.url https://github.com/makepad/makepad.git
|
||
git -C "$jp_stage" index-pack --stdin < "$jp_pack" >/dev/null 2>>"$log_file" || { step_fail "$jp_label" 'The pack could not be unpacked'; return 1; }
|
||
printf '%s\n' "$jp_commit" > "$jp_stage/.git/shallow"
|
||
step "$jp_label" "now|$((jp_bytes * 3 / 2))|$((2 * jp_bytes))|MB|$jp_bytes|writing files"
|
||
git -C "$jp_stage" -c advice.detachedHead=false checkout -q -f --detach "$jp_commit" 2>>"$log_file" || { step_fail "$jp_label" 'The source could not be checked out'; return 1; }
|
||
[ "$(git -C "$jp_stage" rev-parse HEAD)" = "$jp_commit" ] || { step_fail "$jp_label" 'The source is not the expected commit'; return 1; }
|
||
mkdir -p "$(dirname "$jp_checkout")"
|
||
mv "$jp_stage" "$jp_checkout"
|
||
put "$jp_dest/.builder-repositories/$jp_name" "$jp_commit $jp_sha"
|
||
log "$jp_name $jp_commit checked out in $jp_dest"
|
||
step "$jp_label" "done|$((jp_bytes / 1048576)) MB · $(($(now) - jp_started)) s"
|
||
}
|
||
|
||
# ---------------------------------------------------------------- jobs ---
|
||
# jobs_start KEY CMD...: CMD in the background; jobs_wait redraws until all
|
||
# are finished and fails when one failed.
|
||
running_jobs=
|
||
job_start() {
|
||
( "$@" ) </dev/null >/dev/null 2>>"$log_file" &
|
||
running_jobs="$running_jobs $!"
|
||
}
|
||
jobs_wait() {
|
||
jw_status=0
|
||
# Escape stops waiting for Apple's installer (the other jobs go on);
|
||
# only then are keys read here.
|
||
while :; do
|
||
jw_alive=
|
||
for jw_pid in $running_jobs; do
|
||
if kill -0 "$jw_pid" 2>/dev/null; then jw_alive="$jw_alive $jw_pid"; fi
|
||
done
|
||
[ -z "${tui:-}" ] || work_redraw
|
||
[ -n "$jw_alive" ] || break
|
||
if [ -n "${tui:-}" ] && [ -n "${xcode_job:-}" ] && kill -0 "$xcode_job" 2>/dev/null; then
|
||
if key_read 1 1 && [ "$kr" = "$e" ]; then
|
||
kill_tree "$xcode_job"
|
||
step 'Xcode tools' 'held|stopped waiting; select Xcode tools to check again'
|
||
fi
|
||
else
|
||
sleep 0.1
|
||
fi
|
||
done
|
||
for jw_pid in $running_jobs; do wait "$jw_pid" || jw_status=1; done
|
||
running_jobs= xcode_job=
|
||
# A job that ended without saying so failed unexpectedly.
|
||
for jw_key in $work_labels_keys; do
|
||
case "$(cut -d'|' -f1 "$steps_dir/$jw_key" 2>/dev/null || :)" in
|
||
now) put "$steps_dir/$jw_key" 'failed|stopped unexpectedly (see builder.log)'; jw_status=1 ;;
|
||
failed | held) jw_status=1 ;;
|
||
esac
|
||
done
|
||
[ -z "${tui:-}" ] || work_redraw
|
||
return "$jw_status"
|
||
}
|
||
|
||
# ---------------------------------------------------------- source sets ---
|
||
# checkout_jobs: one job per repository of the loaded release that is not
|
||
# here yet, labelled "<Name> source". Sets checkout_labels.
|
||
checkout_jobs() {
|
||
rel_dir
|
||
checkout_labels=
|
||
cj_dest=$rel_directory
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
repo_installed "$cj_dest" "$v_name" "$v_path" "$v_commit" && continue
|
||
cj_parent=$(printf '%s\n' "$rel_repos" | awk -F'|' -v p="$v_path" '$2 != p && index(p "/", $2 "/") == 1 { n = $1 } END { print n }')
|
||
cj_have=
|
||
if [ "$rel_public" != true ] && [ -f "$root/installed/$rel_id.json" ]; then
|
||
cj_have=$(json flat < "$root/installed/$rel_id.json" | awk -F'\t' -v name="$v_name" '$1 ~ /^repositories\/[0-9]+\/name$/ && $2 == name { split($1, a, "/"); want = a[2] } $1 ~ /\/sha256$/ { split($1, a, "/"); sha[a[2]] = $2 } END { if (want != "") print sha[want] }')
|
||
[ "$cj_have" != "$v_sha" ] || cj_have=
|
||
fi
|
||
cj_title=$(printf '%s' "$v_name" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }')
|
||
cj_label="$cj_title source"
|
||
checkout_labels="$checkout_labels$cj_label
|
||
"
|
||
job_start job_pack "$cj_label" "$cj_dest" "$v_name" "$v_path" "$v_commit" "$v_sha" "$v_bytes" "$rel_id" "$rel_release" "$rel_public" "$cj_have" "$cj_parent"
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
return 0
|
||
}
|
||
source_labels() { # the labels checkout_jobs will use, without starting anything
|
||
rel_dir
|
||
source_labels=
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
repo_installed "$rel_directory" "$v_name" "$v_path" "$v_commit" && continue
|
||
sl_title=$(printf '%s' "$v_name" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }')
|
||
source_labels="$source_labels$sl_title source
|
||
"
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
}
|
||
|
||
# save_changes APP PREVIOUS-RELEASE-FILE: before an update moves an app off
|
||
# the snapshot it was built from, the edits made there are saved as
|
||
# changes/<app>-<date>.diff (unified, new files included) and .files (one
|
||
# "M|A|D path" line each), and changes/<app>.merge names the diff. Updates
|
||
# never merge; the person's coding agent reapplies the diff.
|
||
save_changes() {
|
||
rel_load "$2" || return 0
|
||
rel_dir
|
||
sv_dir=$rel_directory
|
||
sv_diff="$root/tmp/changes.diff" sv_files="$root/tmp/changes.files"
|
||
: > "$sv_diff"; : > "$sv_files"
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
sv_co="$sv_dir/$v_path"
|
||
[ -d "$sv_co/.git" ] || continue
|
||
# Nested repositories report their own edits.
|
||
sv_exclude=$(printf '%s\n' "$rel_repos" | awk -F'|' -v p="$v_path" 'index($2, p "/") == 1 { print ":(exclude)" substr($2, length(p) + 2) }')
|
||
# shellcheck disable=SC2086 # one pathspec per line, none with spaces
|
||
git -C "$sv_co" diff --name-status HEAD -- . $sv_exclude 2>/dev/null | while IFS=' ' read -r sv_kind sv_path; do
|
||
case "$sv_kind" in D*) sv_kind=D ;; A*) sv_kind=A ;; *) sv_kind=M ;; esac
|
||
printf '%s|%s/%s\n' "$sv_kind" "$v_path" "$sv_path"
|
||
done >> "$sv_files"
|
||
# shellcheck disable=SC2086
|
||
git -C "$sv_co" diff --binary HEAD --src-prefix="a/$v_path/" --dst-prefix="b/$v_path/" -- . $sv_exclude >> "$sv_diff" 2>/dev/null || :
|
||
# shellcheck disable=SC2086
|
||
git -C "$sv_co" ls-files --others --exclude-standard -- . $sv_exclude 2>/dev/null | while IFS= read -r sv_path; do
|
||
case "$sv_path" in target/* | */target/*) continue ;; esac
|
||
printf 'A|%s/%s\n' "$v_path" "$sv_path" >> "$sv_files"
|
||
git -C "$sv_co" diff --no-index --binary --src-prefix="a/$v_path/" --dst-prefix="b/$v_path/" /dev/null "$sv_path" >> "$sv_diff" 2>/dev/null || :
|
||
done
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
[ -s "$sv_files" ] || return 0
|
||
mkdir -p "$root/changes"
|
||
sv_name="$1-$(date +%Y-%m-%d)" sv_n=2
|
||
while [ -e "$root/changes/$sv_name.diff" ]; do sv_name="$1-$(date +%Y-%m-%d)-$sv_n"; sv_n=$((sv_n + 1)); done
|
||
mv "$sv_diff" "$root/changes/$sv_name.diff"
|
||
mv "$sv_files" "$root/changes/$sv_name.files"
|
||
put "$root/changes/$1.merge" "$sv_name.diff"
|
||
log "Saved your changes to $1 in changes/$sv_name.diff before updating; the edited source stays in $sv_dir"
|
||
}
|
||
keep_edits() { # keep_edits APP NEW-RELEASE-FILE
|
||
[ -f "$root/installed/$1.json" ] || return 0
|
||
[ ! -f "$root/changes/$1.merge" ] || return 0
|
||
rel_load "$2" || return 0
|
||
rel_dir; ke_new_dir=$rel_directory ke_new=$rel_release
|
||
rel_load "$root/installed/$1.json" || return 0
|
||
rel_dir
|
||
[ "$rel_release" != "$ke_new" ] && [ "$rel_directory" != "$ke_new_dir" ] || return 0
|
||
# Saved once per update: after the merge the diff is not taken again.
|
||
[ "$(cat "$root/changes/$1.saved" 2>/dev/null || :)" != "$rel_release $ke_new" ] || return 0
|
||
ke_old=$rel_release
|
||
save_changes "$1" "$root/installed/$1.json"
|
||
mkdir -p "$root/changes"
|
||
put "$root/changes/$1.saved" "$ke_old $ke_new"
|
||
}
|
||
|
||
# ------------------------------------------------------------- compiling ---
|
||
# How many crates this build produces, for the bar: the count of its last
|
||
# successful build, else Cargo's unit graph (no compiling). 0 when unknown.
|
||
expected_crates() {
|
||
ec_record="$rel_target_dir/$rel_binary.crates"
|
||
if [ -s "$ec_record" ]; then cat "$ec_record"; return; fi
|
||
# The same features the build passes (build_features); an unset one
|
||
# stopped this under set -u, and the bar never got its end.
|
||
build_features
|
||
# shellcheck disable=SC2086 # feature_args is fixed words
|
||
(cd "$rel_directory/$rel_workspace" && RUSTC_BOOTSTRAP=1 "$CARGO" build --release -p "$rel_package" --bin "$rel_binary" $feature_args --unit-graph -Z unstable-options 2>/dev/null) |
|
||
grep -o '"mode":"build"' | wc -l | tr -d ' '
|
||
}
|
||
# build_features -> $feature_args: the release's features, as cargo takes them.
|
||
build_features() {
|
||
feature_args=--no-default-features
|
||
[ -z "$rel_features" ] || feature_args="$feature_args --features $rel_features"
|
||
}
|
||
# compile_app: builds the loaded release's app in the background, with its
|
||
# progress on the step "Compile"; build log in the snapshot's target.
|
||
job_compile() { # job_compile LABEL APP
|
||
jc_label=$1
|
||
jc_started=$(now)
|
||
step "$jc_label" "now|0|0|none|0|resolving"
|
||
( cargo_env "$rel_label" "$2" 1 >/dev/null; expected_crates > "$root/tmp/expected-crates" ) || :
|
||
jc_expected=$(cat "$root/tmp/expected-crates" 2>/dev/null || echo 0)
|
||
build_app "$2" || { step_fail "$jc_label" "$build_error"; return 1; }
|
||
publish_app || { step_fail "$jc_label" "$publish_error"; return 1; }
|
||
step "$jc_label" "done|$build_crates crates · $(($(now) - jc_started)) s"
|
||
}
|
||
# build_app APP: cargo build of the loaded release into the snapshot's
|
||
# target (log: builder-build.log; the TUI shows the crate count on the step
|
||
# "$jc_label"). With the parallel frontend, a crash (an internal compiler
|
||
# error, the deadlock detector, a compiler killed by a signal, no memory) or
|
||
# 10 minutes without output is retried once with one thread; the parallel
|
||
# log is kept as builder-build.parallel.log, and when one thread works,
|
||
# rustc-threads says 1 from then on. Ordinary compile errors are not retried.
|
||
build_app() {
|
||
build_error= build_crates=0
|
||
rustc_threads
|
||
ba_threads=$rustc_threads
|
||
ba_log="$rel_target_dir/builder-build.log"
|
||
while :; do
|
||
run_cargo_build "$1" "$ba_threads" && break
|
||
ba_retry=0
|
||
if [ "$ba_threads" -gt 1 ]; then
|
||
if [ "$ba_stalled" = 1 ] || grep -Eq "internal compiler error|the compiler unexpectedly panicked|deadlock detected|rustc interrupted by|memory allocation of .* failed|didn.t exit successfully: .*--crate-name.*\(signal: " "$ba_log"; then ba_retry=1; fi
|
||
fi
|
||
if [ "$ba_retry" = 0 ]; then
|
||
build_error=$(grep -m 1 '^error' "$ba_log" 2>/dev/null || :)
|
||
cat "$ba_log" >> "$log_file"
|
||
build_error="${build_error:-Build failed} (full log in builder.log)"
|
||
return 1
|
||
fi
|
||
if [ "$ba_stalled" = 1 ]; then ba_reason='made no progress for 10 minutes'; else ba_reason='crashed'; fi
|
||
cp "$ba_log" "$rel_target_dir/builder-build.parallel.log"
|
||
log "The parallel Rust compiler ($ba_threads threads) $ba_reason; compiling again with one thread."
|
||
ba_parallel=$ba_threads ba_threads=1
|
||
done
|
||
if [ "$ba_threads" = 1 ] && [ "${ba_parallel:-1}" -gt 1 ]; then
|
||
put "$root/rustc-threads" "1
|
||
Serial since the parallel Rust compiler ($ba_parallel threads) $ba_reason and the serial one then compiled the same build. Delete this file to try the parallel compiler again."
|
||
log 'Compiled with one thread. Builds here stay on one thread from now on (see rustc-threads).'
|
||
fi
|
||
build_crates=$(grep -c '"reason":"compiler-artifact"' "$ba_log" 2>/dev/null) || build_crates=0
|
||
printf '%s\n' "$build_crates" > "$rel_target_dir/$rel_binary.crates"
|
||
}
|
||
run_cargo_build() { # run_cargo_build APP THREADS -> status; ba_stalled
|
||
ba_stalled=0
|
||
(
|
||
cargo_env "$rel_label" "$1" "$2" >/dev/null
|
||
build_features
|
||
cd "$rel_directory/$rel_workspace"
|
||
# shellcheck disable=SC2086 # feature_args is fixed words
|
||
exec "$CARGO" build --release --message-format=json-render-diagnostics -p "$rel_package" --bin "$rel_binary" $feature_args
|
||
) </dev/null > "$ba_log" 2>&1 &
|
||
rb_pid=$!
|
||
rb_size=-1 rb_quiet=0 rb_ticks=0
|
||
while kill -0 "$rb_pid" 2>/dev/null; do
|
||
if [ -n "${jc_label:-}" ]; then
|
||
rb_crates=$(grep -c '"reason":"compiler-artifact"' "$ba_log" 2>/dev/null) || rb_crates=0
|
||
rb_fresh=$(grep '"reason":"compiler-artifact"' "$ba_log" 2>/dev/null | grep -c '"fresh":true') || rb_fresh=0
|
||
rb_built=$((rb_crates - rb_fresh)) rb_of=0
|
||
if [ "${jc_expected:-0}" -gt 0 ]; then
|
||
rb_of=$((jc_expected - rb_fresh))
|
||
[ "$rb_of" -gt "$rb_built" ] || rb_of=$((rb_built + 1))
|
||
fi
|
||
step "$jc_label" "now|$rb_built|$rb_of|crates|0|compiling"
|
||
fi
|
||
sleep 0.3
|
||
rb_ticks=$((rb_ticks + 1))
|
||
[ $((rb_ticks % 3)) = 0 ] || continue
|
||
rb_now=$(size_of "$ba_log")
|
||
if [ "$rb_now" = "$rb_size" ]; then rb_quiet=$((rb_quiet + 1)); else rb_size=$rb_now rb_quiet=0; fi
|
||
if [ "$2" -gt 1 ] && [ "$rb_quiet" -ge 600 ]; then
|
||
kill_tree "$rb_pid"
|
||
ba_stalled=1
|
||
fi
|
||
done
|
||
wait "$rb_pid"
|
||
}
|
||
# Background jobs ignore Ctrl+C; stopping one stops its children too. Each
|
||
# process is paused first, so it cannot start another while its children
|
||
# are stopped, then ended.
|
||
kill_tree() {
|
||
kill -STOP "$1" 2>/dev/null || :
|
||
for kt_child in $(pgrep -P "$1" 2>/dev/null); do kill_tree "$kt_child"; done
|
||
kill -TERM "$1" 2>/dev/null || :
|
||
kill -CONT "$1" 2>/dev/null || :
|
||
}
|
||
|
||
# publish_app: the built executable as builder/<binary>.bin with the map
|
||
# from crate names to their source folders beside it (fonts, icons and other
|
||
# resources stay in the sources), its <binary> command beside this script,
|
||
# and on macOS a real <Title>.app there for the Dock.
|
||
publish_app() {
|
||
publish_error=
|
||
pa_log="$rel_target_dir/builder-build.log"
|
||
pa_map="$root/tmp/package-paths"
|
||
: > "$pa_map"
|
||
awk '
|
||
index($0, "\"reason\":\"compiler-artifact\"") {
|
||
if (!match($0, /"manifest_path":"[^"]*"/)) next
|
||
m = substr($0, RSTART + 17, RLENGTH - 18)
|
||
t = substr($0, index($0, "\"target\":{"))
|
||
k = substr(t, index(t, "\"kind\":[") + 8); k = substr(k, 1, index(k, "]") - 1)
|
||
if (k == "\"custom-build\"") next
|
||
if (!match(t, /"name":"[^"]*"/)) next
|
||
name = substr(t, RSTART + 8, RLENGTH - 9); gsub(/-/, "_", name)
|
||
sub(/\/[^\/]*$/, "", m)
|
||
print name "\t" m
|
||
}' "$pa_log" | sort -u > "$root/tmp/artifacts"
|
||
while IFS=' ' read -r pa_name pa_dir; do
|
||
case "$pa_dir" in "$rel_directory"/*) ;; *) [ -d "$pa_dir/resources" ] || continue ;; esac
|
||
case "$pa_dir" in "$root"/*) ;; *) publish_error="Resources for $pa_name are outside the portable installation"; return 1 ;; esac
|
||
printf '%s\t%s\n' "$pa_name" "${pa_dir#"$root"/}" >> "$pa_map"
|
||
done < "$root/tmp/artifacts"
|
||
[ -s "$pa_map" ] || { publish_error='Cargo produced no application resource paths'; return 1; }
|
||
pa_bin="$root/$rel_binary.bin"
|
||
mv -f "$pa_map" "$pa_bin.makepad-package-paths"
|
||
# Older runtimes read one shared map: keep other apps' entries.
|
||
{ cat "$pa_bin.makepad-package-paths"; cat "$root/makepad-package-paths" 2>/dev/null || :; } |
|
||
awk -F'\t' '!seen[$1]++' > "$root/makepad-package-paths.next"
|
||
mv -f "$root/makepad-package-paths.next" "$root/makepad-package-paths"
|
||
cp "$rel_target_dir/release/$rel_binary" "$pa_bin.next"
|
||
chmod 755 "$pa_bin.next"
|
||
mv -f "$pa_bin.next" "$pa_bin"
|
||
# This snapshot's artifacts are in the shared target now: later releases
|
||
# at the same commits build from here.
|
||
put "$rel_directory/.builder-built" "$rel_id $rel_release"
|
||
write_launcher "$rel_binary"
|
||
if [ "$plat" = mac ]; then mac_bundle || return 1; fi
|
||
mkdir -p "$root/installed"
|
||
cp "$release_file" "$root/installed/$rel_id.json.next"
|
||
mv -f "$root/installed/$rel_id.json.next" "$root/installed/$rel_id.json"
|
||
cp "$root/installed/$rel_id.json" "$root/installed-release.json"
|
||
cp "$root/installed/$rel_id.json" "$rel_directory/$rel_id.json"
|
||
}
|
||
|
||
# write_launcher BINARY: the <binary> command beside this script. It keeps
|
||
# the invoking directory as the app's project; on macOS it opens the app's
|
||
# bundle, so the shell command and the Dock icon are the same app.
|
||
write_launcher() {
|
||
cat > "$home/$1.next" <<EOF
|
||
#!/bin/sh
|
||
# Opens $1, built by the Makepad Builder in this folder (./makepad build $1).
|
||
set -eu
|
||
builder_script=\$0
|
||
while [ -L "\$builder_script" ]; do
|
||
builder_directory=\$(cd -P -- "\$(dirname -- "\$builder_script")" && pwd)
|
||
builder_link=\$(readlink "\$builder_script")
|
||
case "\$builder_link" in /*) builder_script=\$builder_link ;; *) builder_script=\$builder_directory/\$builder_link ;; esac
|
||
done
|
||
builder_directory=\$(cd -P -- "\$(dirname -- "\$builder_script")" && pwd)
|
||
unset MAKEPAD_LOADER_EMAIL MAKEPAD_PACKAGE_DIR
|
||
if [ "\$#" -gt 0 ]; then
|
||
case "\$1" in -*) ;; *) set -- --cwd "\$@" ;; esac
|
||
fi
|
||
if [ "\$(uname -s)" = Darwin ]; then
|
||
for builder_app in "\$builder_directory/"*.app; do
|
||
if [ -x "\$builder_app/Contents/MacOS/$1" ]; then
|
||
builder_has_cwd=0
|
||
for builder_arg in "\$@"; do
|
||
[ "\$builder_arg" != --cwd ] || builder_has_cwd=1
|
||
done
|
||
if [ "\$builder_has_cwd" = 0 ]; then set -- --cwd "\$PWD" "\$@"; fi
|
||
exec "\$builder_app/Contents/MacOS/$1" "\$@"
|
||
fi
|
||
done
|
||
fi
|
||
exec "\$builder_directory/builder/$1.bin" "\$@"
|
||
EOF
|
||
chmod 755 "$home/$1.next"
|
||
mv -f "$home/$1.next" "$home/$1"
|
||
}
|
||
# macos_launcher BINARY: the executable a bundle's Dock launch runs. It
|
||
# resolves everything from the bundle, so the whole folder can move.
|
||
macos_launcher() {
|
||
cat <<EOF
|
||
#!/bin/sh
|
||
set -eu
|
||
app_dir=\$(cd -P -- "\$(dirname -- "\$0")" && pwd)
|
||
install_dir=\$(cd -P -- "\$app_dir/../../../builder" && pwd)
|
||
unset MAKEPAD_LOADER_EMAIL MAKEPAD_PACKAGE_DIR
|
||
has_project=0
|
||
for arg in "\$@"; do
|
||
[ "\$arg" != --cwd ] || has_project=1
|
||
done
|
||
if [ "\$has_project" = 0 ]; then
|
||
project=\$(cat "\$install_dir/installed/$1.project")
|
||
case "\$project" in /*) ;; *) project="\$install_dir/\$project" ;; esac
|
||
set -- --cwd "\$project" "\$@"
|
||
fi
|
||
mkdir -p "\$install_dir/target"
|
||
exec "\$app_dir/$1-bin" "\$@" </dev/null >>"\$install_dir/target/$1-app.log" 2>&1
|
||
EOF
|
||
}
|
||
# mac_bundle: "<Title>.app" beside this script with the executable, a
|
||
# launcher, a relative link to the Builder's folder (resources stay in the
|
||
# sources) and the app's icon, so it can be kept in the Dock.
|
||
mac_bundle() {
|
||
mb_name=$(printf '%s' "$rel_title" | tr -cd 'A-Za-z0-9 ._-' | sed 's/^[ .]*//; s/ *$//')
|
||
[ -n "$mb_name" ] || mb_name=$(printf '%s' "$rel_binary" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }')
|
||
mb_bundle="$home/$mb_name.app"
|
||
mb_tools="$rel_directory/$rel_makepad_path/tools/makepad_builder"
|
||
mb_project="$rel_directory/$rel_makepad_path"
|
||
mkdir -p "$root/installed"
|
||
put "$root/installed/$rel_binary.project" "${mb_project#"$root"/}"
|
||
mb_launcher=$(macos_launcher "$rel_binary")
|
||
mb_icon=
|
||
if [ "$rel_id" = scope ] && [ -f "$mb_tools/resources/scope.icns" ]; then mb_icon="$mb_tools/resources/scope.icns"
|
||
elif [ -f "$rel_directory/$rel_workspace/resources/icon.icns" ]; then mb_icon="$rel_directory/$rel_workspace/resources/icon.icns"
|
||
elif [ -f "$rel_directory/$rel_workspace/resources/icon_1024.png" ]; then mb_icon="$rel_directory/$rel_workspace/resources/icon_1024.png"
|
||
else mb_icon=$(declared_icon); fi
|
||
mb_stamp="bundle-v1 $(cksum < "$root/$rel_binary.bin") $(cksum < "$root/$rel_binary.bin.makepad-package-paths") $rel_title $(printf '%s' "$mb_launcher" | cksum) $mb_icon"
|
||
if [ "$(cat "$mb_bundle/Contents/Resources/build-receipt" 2>/dev/null || :)" = "$mb_stamp" ] && [ -x "$mb_bundle/Contents/MacOS/$rel_binary" ]; then
|
||
return 0
|
||
fi
|
||
mb_stage="$home/.$mb_name.app.next"
|
||
[ ! -e "$mb_stage" ] || remove_inside "$mb_stage"
|
||
mkdir -p "$mb_stage/Contents/MacOS" "$mb_stage/Contents/Resources"
|
||
cp "$root/$rel_binary.bin" "$mb_stage/Contents/MacOS/$rel_binary-bin"
|
||
printf '%s\n' "$mb_launcher" > "$mb_stage/Contents/MacOS/$rel_binary"
|
||
chmod 755 "$mb_stage/Contents/MacOS/$rel_binary"
|
||
ln -s ../../../builder "$mb_stage/Contents/MacOS/installation"
|
||
sed 's# # installation/#' "$root/$rel_binary.bin.makepad-package-paths" > "$mb_stage/Contents/MacOS/$rel_binary-bin.makepad-package-paths"
|
||
mb_icon_key=
|
||
case "$mb_icon" in
|
||
*.png) cp "$mb_icon" "$mb_stage/Contents/Resources/AppIcon.png"; mb_icon_key='<key>CFBundleIconFile</key><string>AppIcon.png</string>' ;;
|
||
?*) cp "$mb_icon" "$mb_stage/Contents/Resources/AppIcon.icns"; mb_icon_key='<key>CFBundleIconFile</key><string>AppIcon.icns</string>' ;;
|
||
esac
|
||
mb_title=$(printf '%s' "$rel_title" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||
cat > "$mb_stage/Contents/Info.plist" <<EOF
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0"><dict>
|
||
<key>CFBundleExecutable</key><string>$rel_binary</string>
|
||
<key>CFBundleIdentifier</key><string>nl.makepad.$(printf '%s' "$rel_id" | tr '_' '-')</string>
|
||
<key>CFBundleName</key><string>$mb_title</string>
|
||
<key>CFBundleDisplayName</key><string>$mb_title</string>
|
||
<key>CFBundlePackageType</key><string>APPL</string>
|
||
<key>CFBundleVersion</key><string>1.0</string>
|
||
<key>CFBundleShortVersionString</key><string>1.0</string>
|
||
<key>LSUIElement</key><false/>
|
||
<key>NSHighResolutionCapable</key><true/>
|
||
<key>NSLocationUsageDescription</key><string>Used to show your position on the map.</string>
|
||
<key>NSMicrophoneUsageDescription</key><string>Used for audio input in Makepad apps.</string>
|
||
<key>NSCameraUsageDescription</key><string>Used for camera input in Makepad apps.</string>
|
||
$mb_icon_key
|
||
</dict></plist>
|
||
EOF
|
||
printf 'APPL????' > "$mb_stage/Contents/PkgInfo"
|
||
put "$mb_stage/Contents/Resources/build-receipt" "$mb_stamp"
|
||
mb_previous="$home/.$mb_name.app.previous"
|
||
[ ! -e "$mb_previous" ] || remove_inside "$mb_previous"
|
||
[ ! -e "$mb_bundle" ] || mv "$mb_bundle" "$mb_previous"
|
||
mv "$mb_stage" "$mb_bundle"
|
||
[ ! -e "$mb_previous" ] || remove_inside "$mb_previous"
|
||
}
|
||
# The icon an app's package declares for desktop bundles (as cargo makepad
|
||
# reads it): icon = "..." under [package.metadata.makepad.desktop].
|
||
declared_icon() {
|
||
find "$rel_directory/$rel_workspace" -maxdepth 4 -name Cargo.toml -not -path '*/target/*' 2>/dev/null | while IFS= read -r di_manifest; do
|
||
di_icon=$(awk -v package="$rel_package" '
|
||
/^\[/ { table = $0; next }
|
||
table == "[package]" && $1 == "name" { v = $0; sub(/^[^"]*"/, "", v); sub(/".*/, "", v); name = v }
|
||
table == "[package.metadata.makepad.desktop]" && $1 == "icon" { v = $0; sub(/^[^"]*"/, "", v); sub(/".*/, "", v); icon = v }
|
||
END { if (name == package && icon != "") print icon }' "$di_manifest")
|
||
if [ -n "$di_icon" ] && [ -f "$(dirname "$di_manifest")/$di_icon" ]; then
|
||
printf '%s\n' "$(dirname "$di_manifest")/$di_icon"
|
||
break
|
||
fi
|
||
done
|
||
}
|
||
|
||
# launch_app: the built app, detached from this terminal (closing it does
|
||
# not stop the app), with its output in the snapshot's target folder.
|
||
launch_app() { # after rel_load/rel_dir of the installed release
|
||
rel_target
|
||
la_project="$rel_directory/$rel_makepad_path"
|
||
la_log="$rel_target_dir/$rel_binary-app.log"
|
||
mkdir -p "$rel_target_dir"
|
||
if [ "$plat" = mac ]; then
|
||
la_name=$(printf '%s' "$rel_title" | tr -cd 'A-Za-z0-9 ._-' | sed 's/^[ .]*//; s/ *$//')
|
||
[ -n "$la_name" ] || la_name=$(printf '%s' "$rel_binary" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }')
|
||
set -- "$home/$la_name.app/Contents/MacOS/$rel_binary" --cwd "$la_project"
|
||
[ "$rel_id" != scope ] || set -- "$@" --focus
|
||
else
|
||
set -- "$home/$rel_binary" --cwd "$la_project"
|
||
fi
|
||
(
|
||
cargo_env "$rel_label" "$rel_id" >/dev/null 2>&1
|
||
unset MAKEPAD_LOADER_EMAIL RUSTUP_TOOLCHAIN
|
||
export MAKEPAD_FEEDBACK_EMAIL="$email"
|
||
cd "$la_project"
|
||
if command -v setsid >/dev/null 2>&1; then exec setsid "$@"
|
||
elif command -v perl >/dev/null 2>&1; then exec perl -e 'setpgrp(0, 0); exec { $ARGV[0] } @ARGV or die "$!\n"' "$@"
|
||
else exec nohup "$@"; fi
|
||
) </dev/null >"$la_log" 2>&1 &
|
||
log "Running $rel_title"
|
||
}
|
||
launched_message() {
|
||
if [ "$plat" = mac ]; then message="${ok}✓${r0} $1 is running. ${dim}Right-click its Dock icon › Options › Keep in Dock.${r0}"
|
||
else message="${ok}✓${r0} $1 is running. ${dim}Run it again from here or from its command.${r0}"; fi
|
||
}
|
||
|
||
# Remove source snapshots nothing refers to any more and that hold no
|
||
# edits (git status is clean apart from nested checkouts), with their build
|
||
# output. A snapshot you or an agent worked in stays.
|
||
prune_snapshots() {
|
||
ps_keep=
|
||
for ps_file in "$root"/installed/*.json "$root"/available/*.json; do
|
||
[ -f "$ps_file" ] || continue
|
||
rel_load "$ps_file" 2>/dev/null || continue
|
||
rel_dir
|
||
ps_keep="$ps_keep|$rel_directory|"
|
||
done
|
||
for ps_snap in "$root"/sources/*; do
|
||
[ -d "$ps_snap" ] || continue
|
||
case "$ps_keep" in *"|$ps_snap|"*) continue ;; esac
|
||
ps_clean=1
|
||
for ps_receipt in "$ps_snap"/.builder-repositories/*; do
|
||
[ -f "$ps_receipt" ] || continue
|
||
ps_name=${ps_receipt##*/}
|
||
ps_path=$ps_name
|
||
[ "$ps_name" != makepad ] || ps_path=makepad
|
||
for ps_json in "$ps_snap"/*.json; do
|
||
[ -f "$ps_json" ] || continue
|
||
ps_found=$(json flat < "$ps_json" | awk -F'\t' -v n="$ps_name" '$1 ~ /\/name$/ && $2 == n { split($1, a, "/"); want = a[2] } $1 ~ /\/path$/ { split($1, a, "/"); path[a[2]] = $2 } END { if (want != "") print path[want] }')
|
||
[ -z "$ps_found" ] || ps_path=$ps_found
|
||
done
|
||
[ -d "$ps_snap/$ps_path/.git" ] || { ps_clean=0; break; }
|
||
ps_dirty=$(git -C "$ps_snap/$ps_path" status --porcelain 2>/dev/null | grep -v '^?? apps/[A-Za-z0-9_-]*/$' | head -n 1) || ps_dirty=error
|
||
[ -z "$ps_dirty" ] || { ps_clean=0; break; }
|
||
done
|
||
[ "$ps_clean" = 1 ] || continue
|
||
remove_inside "$ps_snap"
|
||
[ ! -e "$root/target/sources/${ps_snap##*/}" ] || remove_inside "$root/target/sources/${ps_snap##*/}"
|
||
log "Removed the unused sources ${ps_snap##*/}; nothing was built from them any more and they held no edits."
|
||
done
|
||
}
|
||
|
||
# -------------------------------------------------------- coding agents ---
|
||
agent_directive='Before editing read the installation instructions in AGENTS.md in this folder (also named by the MAKEPAD_AGENT_CONTEXT environment variable). Rebuild with ../makepad build <app>. Preserve the existing repository instructions and wait for the user to choose a change.'
|
||
print_agent_context() {
|
||
pac_rust=${rust_version:-pinned}
|
||
cat <<EOF
|
||
# Makepad apps in this folder
|
||
|
||
Everything here is Makepad applications as Rust source (the folder above
|
||
holds only the \`makepad\` command and the built apps). The person using this
|
||
folder wants you to change these apps and rebuild them.
|
||
|
||
## Build and run
|
||
|
||
Always use the \`makepad\` command in the folder above (../makepad); do not
|
||
call cargo directly.
|
||
It uses this folder's compiler (a private Rust $pac_rust in toolchain/, or the
|
||
installed Rust recorded in selected-rust), keeps caches and build output in
|
||
this folder, and rebuilds the macOS app bundle so Dock icons keep working.
|
||
|
||
../makepad build <app> compile after you change its source
|
||
../makepad run <app> compile if needed, then open it
|
||
../makepad list every app, its state and its source folder
|
||
|
||
A build prints compiler errors in full; fix them and build again. If you need
|
||
cargo itself (tests, clippy), first run: eval "\$(../makepad env)"
|
||
Do not install another compiler, change the global PATH or run rustup.
|
||
|
||
## Apps
|
||
|
||
| app | name | source |
|
||
|-----|------|--------|
|
||
EOF
|
||
list_apps | while IFS='|' read -r pa_id pa_title pa_state pa_source; do
|
||
printf '| %s | %s | %s |\n' "$pa_id" "$pa_title" "$pa_source"
|
||
done
|
||
cat <<EOF
|
||
|
||
Makepad itself (widgets, platform, shaders) is in the makepad folder of each
|
||
source snapshot; read its AGENTS.md before changing it. Each repository keeps
|
||
its own instructions. Fonts, icons and other resources stay in the sources;
|
||
each built app finds them through <app>.bin.makepad-package-paths.
|
||
|
||
## After an update
|
||
|
||
Updates never merge. When an app you changed gets a new release, the old
|
||
changes are saved first and the app is replaced by the clean new source:
|
||
|
||
changes/<app>-<date>.diff unified diff against the release you edited
|
||
changes/<app>-<date>.files one "M|A|D path" line per changed file
|
||
|
||
When asked to merge, reapply that diff onto the new source by editing files,
|
||
keep the intent of each change where the new release moved code, then
|
||
../makepad build <app> until it compiles. Report any change that no longer
|
||
applies.
|
||
|
||
## Rules
|
||
|
||
- Keep edits inside sources/. Built apps (<app>.bin, <Title>.app) are regenerated.
|
||
- Your licenses do not allow publishing the commercial sources.
|
||
- Do not read or expose makepad-builder.json or any email address.
|
||
EOF
|
||
}
|
||
# list_apps: id|title|state|source folder, licensed apps first.
|
||
list_apps() {
|
||
la_ids=$(cat "$root/tmp/builder/licensed" 2>/dev/null || :)
|
||
if [ -z "$la_ids" ]; then
|
||
for la_file in "$root"/available/*.json; do
|
||
[ -f "$la_file" ] || continue
|
||
rel_load "$la_file" 2>/dev/null || continue
|
||
[ "$rel_public" = true ] || la_ids="$la_ids $rel_id"
|
||
done
|
||
fi
|
||
for la_id in $la_ids; do
|
||
app_state "$la_id"
|
||
rel_load "$root/available/$la_id.json" 2>/dev/null || continue
|
||
rel_dir
|
||
printf '%s|%s|%s|%s\n' "$la_id" "$rel_title" "$state" "${rel_directory#"$root"/}/$rel_workspace"
|
||
done
|
||
free_apps | while IFS='|' read -r la_id la_title; do
|
||
app_state "$la_id"
|
||
la_src=sources/makepad/apps/$la_id
|
||
if app_release_file "$la_id" 2>/dev/null && rel_load "$release_file" 2>/dev/null; then rel_dir; la_src="${rel_directory#"$root"/}/$rel_workspace"; fi
|
||
printf '%s|%s|%s|%s\n' "$la_id" "$la_title" "$state" "$la_src"
|
||
done
|
||
}
|
||
write_agent_files() {
|
||
print_agent_context > "$root/AGENTS.md.next" && mv -f "$root/AGENTS.md.next" "$root/AGENTS.md"
|
||
cp "$root/AGENTS.md" "$root/CLAUDE.md"
|
||
cp "$root/AGENTS.md" "$root/agent-context.txt"
|
||
}
|
||
# The newest installed release, for the environment agents and shells get.
|
||
agent_release() {
|
||
for ar_file in "$root"/installed/*.json; do
|
||
[ -f "$ar_file" ] || continue
|
||
rel_load "$ar_file" 2>/dev/null || continue
|
||
rel_dir
|
||
rel_installed && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# ---------------------------------------------------------------- session ---
|
||
# The folder's own records: the email (makepad-builder.json, never sent
|
||
# anywhere but the license service), a scratch folder, the activity log.
|
||
read_email() {
|
||
email=
|
||
for re_file in "$root/makepad-builder.json" "$root/makepad-loader.json"; do
|
||
[ -f "$re_file" ] || continue
|
||
email=$(json flat < "$re_file" | awk -F'\t' '$1 == "email" { print $2; exit }')
|
||
break
|
||
done
|
||
}
|
||
save_email() {
|
||
put "$root/makepad-builder.json" "{\"version\":1,\"email\":\"$email\",\"app\":\"makepad\"}"
|
||
}
|
||
session() {
|
||
mkdir -p "$root/tmp/builder/steps" "$root/tmp/builder/slots"
|
||
scratch="$root/tmp/builder"
|
||
steps_dir="$scratch/steps"
|
||
slots="$scratch/slots"
|
||
log_file="$root/builder.log"
|
||
# Connection slots and step records of a stopped run are stale.
|
||
for s_old in "$slots"/* "$steps_dir"/*; do [ ! -e "$s_old" ] || remove_inside "$s_old"; done
|
||
read_email
|
||
load_registry
|
||
}
|
||
|
||
# ----------------------------------------------------------- commands ---
|
||
# Plain, line-based output for coding agents: every compiler message, then
|
||
# one result line they can check.
|
||
die() { printf '%s\n' "$*" >&2; exit 1; }
|
||
cmd_setup() { # the app's release, its Rust and system tools, for build/run
|
||
session
|
||
app_release_file "$1" || die "Unknown app $1; ./makepad list shows every app."
|
||
rel_load "$release_file" || die "$1 has no usable release; open ./makepad and update."
|
||
rel_supported || die "$rel_title is not available for this platform yet."
|
||
rust_ready "$rel_rust" || die "Rust $rel_rust is not set up here; open ./makepad to set it up."
|
||
tools_check || die "The system developer tools are not ready ($tools_problem); open ./makepad to set them up."
|
||
}
|
||
cmd_build() { # cmd_build APP [run]
|
||
cmd_setup "$1"
|
||
rel_dir
|
||
if ! rel_installed; then
|
||
[ "$rel_public" = true ] || [ -n "$email" ] || die "Log in with the email that has the $rel_title license: open ./makepad."
|
||
[ "$rel_public" = true ] || email_headers
|
||
printf 'Downloading the %s source\n' "$rel_title"
|
||
keep_edits "$1" "$release_file"
|
||
rel_load "$release_file"
|
||
work_labels_keys=
|
||
source_labels
|
||
while IFS= read -r cb_label; do
|
||
[ -n "$cb_label" ] || continue
|
||
step_key "$cb_label"; work_labels_keys="$work_labels_keys $step_key"
|
||
done <<EOF
|
||
$source_labels
|
||
EOF
|
||
checkout_jobs
|
||
if ! jobs_wait; then
|
||
for cb_key in $work_labels_keys; do cut -d'|' -f2 "$steps_dir/$cb_key" >&2; done
|
||
die 'Download failed.'
|
||
fi
|
||
rel_load "$release_file"
|
||
rel_dir
|
||
fi
|
||
prepare_target
|
||
printf 'Compiling %s with Rust %s\n' "$rel_title" "$rel_rust"
|
||
jc_label=
|
||
if ! build_app "$rel_id"; then
|
||
# Every compiler message for the agent, then the result.
|
||
grep -v '^{' "$rel_target_dir/builder-build.log" >&2 || :
|
||
die "Build failed: $rel_title"
|
||
fi
|
||
grep -v '^{' "$rel_target_dir/builder-build.log" | grep -v '^ *Compiling ' || :
|
||
publish_app || die "$publish_error"
|
||
printf 'Built %s: %s\n' "$rel_title" "$root/$rel_binary.bin"
|
||
if [ "$plat" = mac ]; then printf 'Bundled %s\n' "$mb_bundle"; fi
|
||
if [ "${2:-}" = run ]; then launch_app; printf 'Opened %s\n' "$rel_title"; fi
|
||
}
|
||
print_env() {
|
||
session
|
||
rust_version
|
||
rust_ready "${rust_version:-0.0.0}" || die 'Rust is not set up here; open ./makepad to set it up.'
|
||
rel_label=
|
||
if agent_release; then :; elif rel_load "$root/available/makepad.json" 2>/dev/null; then rel_dir; fi
|
||
[ -n "$rel_label" ] || die 'Download an app first; ./makepad list shows them.'
|
||
(
|
||
cargo_env "$rel_label" "${rel_id:-makepad}" >/dev/null
|
||
for pe_var in CARGO_HOME CARGO_TARGET_DIR RUSTC CARGO RUSTDOC RUSTC_BOOTSTRAP TMPDIR TMP TEMP RUSTUP_HOME RUSTUP_AUTO_INSTALL RUSTUP_TOOLCHAIN MAKEPAD_LOADER_EMAIL MAKEPAD_PACKAGE_DIR CUDA_PATH CUDA_HOME CUDACXX MAKEPAD_GGML_NO_CUDA MAKEPAD_GGML_REQUIRE_CUDA RUSTFLAGS PATH; do
|
||
eval "pe_value=\${$pe_var-}"
|
||
printf "export %s='%s'\n" "$pe_var" "$(printf '%s' "$pe_value" | sed "s/'/'\\\\''/g")"
|
||
done
|
||
)
|
||
}
|
||
|
||
# migrate_layout: an earlier Builder kept everything in the folder itself.
|
||
# Its records, sources, toolchains and built binaries move into builder/
|
||
# (renames within one folder: nothing is copied or downloaded again), the
|
||
# app commands and bundles are rewritten to look there, and the folder
|
||
# keeps only this command and the apps.
|
||
migrate_layout() {
|
||
[ -d "$home/installed" ] || [ -d "$home/toolchain" ] || [ -d "$home/sources" ] || [ -f "$home/makepad-builder.json" ] || [ -f "$home/makepad-loader.json" ] || return 0
|
||
mkdir -p "$root"
|
||
for ml_name in installed available sources target cache toolchain cargo-home rustup-home tmp changes selected-rust \
|
||
latest.json installed-release.json agreements-accepted .login-asked graphics-notice-read makepad-builder.json \
|
||
makepad-loader.json builder.log rustc-threads makepad-package-paths AGENTS.md CLAUDE.md agent-context.txt \
|
||
makepad-builder .builder-version bootstrap-builder.sh run-builder.sh check-tools.sh rust-tools.sh; do
|
||
if { [ -e "$home/$ml_name" ] || [ -L "$home/$ml_name" ]; } && [ ! -e "$root/$ml_name" ]; then
|
||
mv "$home/$ml_name" "$root/$ml_name"
|
||
fi
|
||
done
|
||
for ml_bin in "$home"/*.bin; do
|
||
[ -f "$ml_bin" ] || continue
|
||
ml_name=${ml_bin##*/}; ml_name=${ml_name%.bin}
|
||
identifier "$ml_name" || continue
|
||
mv -f "$ml_bin" "$root/$ml_name.bin"
|
||
[ ! -f "$ml_bin.makepad-package-paths" ] || mv -f "$ml_bin.makepad-package-paths" "$root/$ml_name.bin.makepad-package-paths"
|
||
[ ! -f "$home/$ml_name" ] || write_launcher "$ml_name"
|
||
done
|
||
for ml_app in "$home"/*.app; do
|
||
[ -L "$ml_app/Contents/MacOS/installation" ] || continue
|
||
ln -sfn ../../../builder "$ml_app/Contents/MacOS/installation"
|
||
for ml_file in "$ml_app"/Contents/MacOS/*; do
|
||
ml_name=${ml_file##*/}
|
||
case "$ml_name" in *-bin | *.makepad-package-paths | installation) continue ;; esac
|
||
identifier "$ml_name" || continue
|
||
macos_launcher "$ml_name" > "$ml_file.next" && chmod 755 "$ml_file.next" && mv -f "$ml_file.next" "$ml_file"
|
||
done
|
||
# Rebuilt from the moved binary on the next build.
|
||
rm -f "$ml_app/Contents/Resources/build-receipt"
|
||
done
|
||
}
|
||
[ -z "$home" ] || migrate_layout
|
||
|
||
case "${1:-}" in
|
||
build | run)
|
||
[ -n "$root" ] || die "Run this command from your Makepad folder: ./makepad $1 APP"
|
||
[ -n "${2:-}" ] || die "Usage: ./makepad $1 APP (./makepad list shows every app)"
|
||
cmd_build "$2" "$1"; exit 0 ;;
|
||
list)
|
||
[ -n "$root" ] || die 'Run this command from your Makepad folder.'
|
||
session
|
||
list_apps | awk -F'|' '{ printf "%-14s %-22s %-9s %s\n", $1, $2, $3, $4 }'
|
||
exit 0 ;;
|
||
env)
|
||
[ -n "$root" ] || die 'Run this command from your Makepad folder.'
|
||
print_env; exit 0 ;;
|
||
agent-context)
|
||
[ -n "$root" ] || die 'Run this command from your Makepad folder.'
|
||
session; rust_version; print_agent_context; exit 0 ;;
|
||
rust-check)
|
||
# The read-only Rust probe the Rust Builder shares:
|
||
# rust-check --probe MINIMUM TRIPLE | --validate MINIMUM TRIPLE SYSROOT
|
||
# prints the sysroot; exit 1 without output when no Rust is found,
|
||
# exit 2 with the reason when one is found but cannot be used.
|
||
triple=${4:-$triple}
|
||
[ "${2:-}" = --probe ] || [ "${2:-}" = --validate ] || die 'Usage: makepad rust-check --probe MIN TRIPLE | --validate MIN TRIPLE SYSROOT'
|
||
if rust_check "$2" "$3" "${5:-}"; then printf '%s\n' "$rust_sysroot"; exit 0; fi
|
||
[ "$rust_reason" != 'No installed Rust was found on PATH.' ] || exit 1
|
||
printf '%s\n' "$rust_reason"; exit 2 ;;
|
||
tools-check)
|
||
# Exit 0 when the system developer tools are ready; else 1 and the
|
||
# problem (missing, license or unsupported).
|
||
if tools_check; then exit 0; fi
|
||
printf '%s\n' "$tools_problem"; exit 1 ;;
|
||
tools-setup)
|
||
# Line-based setup of the system tools, for the Rust Builder.
|
||
if tools_check; then printf ' The system developer tools are ready.\n'; exit 0; fi
|
||
if [ "$plat" = mac ]; then
|
||
if [ "$tools_problem" = license ]; then
|
||
printf ' Xcode is installed, but its license has not been accepted yet.\n Read and accept it with: sudo xcodebuild -license\n'
|
||
else
|
||
printf " Opening Apple's installer for the command line developer tools.\n When it has finished, run the Builder again.\n"
|
||
/usr/bin/xcode-select --install >/dev/null 2>&1 || :
|
||
fi
|
||
exit 1
|
||
fi
|
||
linux_packages || die 'Install C/C++, git, curl, CMake, pkg-config and the OpenSSL, X11, Xcursor, xkbcommon, ALSA, PulseAudio, Wayland, EGL/GLX and GBM development packages, then run the Builder again.'
|
||
printf ' Makepad compiles from source and needs development packages from %s:\n\n sudo %s\n\n Press Return to run it, or Ctrl+C to stop. ' "$distro_name" "$packages_cmd"
|
||
IFS= read -r ts_answer || exit 1
|
||
# shellcheck disable=SC2086
|
||
sudo $packages_cmd
|
||
tools_check || die 'Some development packages are still missing; see the package manager output above.'
|
||
exit 0 ;;
|
||
-h | --help) sed -n '2,15p' "$self"; exit 0 ;;
|
||
'') ;;
|
||
*) die "Unknown command $1; ./makepad --help lists them." ;;
|
||
esac
|
||
|
||
# ------------------------------------------------------------ terminal UI ---
|
||
exec 3<>/dev/tty || die 'The Makepad Builder needs an interactive terminal.'
|
||
exec 1>&3
|
||
tui=1
|
||
e=$(printf '\033')
|
||
dim="${e}[2m" b="${e}[1m" ok="${e}[32m" acc="${e}[38;2;255;92;57m" warn="${e}[33m" red="${e}[31m" inv="${e}[7m" r0="${e}[0m"
|
||
# Makepad orange: the ▌ on the selected row and the filled part of progress
|
||
# bars; never a background or an action. The
|
||
# selection band and the logo behind the rows are faint greys, for a dark or
|
||
# a light background (asked once at the start, see light_background).
|
||
mark="${e}[38;2;255;92;57m"
|
||
# A key letter in the footer (c cancel, s start when done): bold orange.
|
||
key_c="${e}[1;38;2;255;92;57m"
|
||
band_dark="${e}[48;2;42;44;48m" band_light="${e}[48;2;228;229;231m"
|
||
logo_dark="${e}[38;2;44;47;52m" logo_light="${e}[38;2;226;227;229m"
|
||
band=$band_dark logo_color=$logo_dark
|
||
# The Makepad mark, large (44 x 13) and small (30 x 9), with the column each
|
||
# line's dots start at (tools/makepad_builder/logo.txt, which the Windows
|
||
# Builder draws the same way).
|
||
logo_big=' ⢀⣾⡄ ⣰⣷⡀
|
||
⢠⣿⣿⣿⣆ ⣰⣿⣿⣷⡀
|
||
⢠⣿⣿⣿⣿⣿⣆ ⣴⣿⣿⣿⣿⣿⡄
|
||
⣰⣿⣿⣿⣿⣿⣿⣿⣧ ⣼⣿⣿⣿⣿⣿⣿⣿⡄
|
||
⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧ ⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆
|
||
⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆
|
||
⡨⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⡅⢩⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⣭⢅
|
||
⢀⣼⣷⡹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟ ⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢣⣿⣧
|
||
⢀⣾⣿⣿⣿⡜⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏ ⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⢣⣿⣿⣿⣧⡀
|
||
⢠⣾⣿⣿⣿⣿⣿⣜⣿⣿⣿⣿⣿⣿⣿⠏ ⠹⣿⣿⣿⣿⣿⣿⡿⣱⣿⣿⣿⣿⣿⣷⡀
|
||
⢠⣿⣿⣿⣿⣿⣿⣿⣿⣎⢿⣿⣿⣿⣿⠋ ⠹⣿⣿⣿⣿⡿⣱⣿⣿⣿⣿⣿⣿⣿⣷⡄
|
||
⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣎⢿⣿⣿⠃ ⠘⣿⣿⡟⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄
|
||
⣰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠻⠁ ⠘⢟⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆'
|
||
logo_big_ink='13 12 11 10 9 8 7 5 4 3 2 1 0'
|
||
logo_small=' ⣼⣆ ⣼⣆
|
||
⣼⣿⣿⣧ ⣼⣿⣿⣧
|
||
⢀⣾⣿⣿⣿⣿⣧⡀ ⢀⣾⣿⣿⣿⣿⣧
|
||
⢀⣾⣿⣿⣿⣿⣿⣿⣷⡀⢀⣾⣿⣿⣿⣿⣿⣿⣷⡀
|
||
⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⡇⢼⣿⣿⣿⣿⣿⣿⣿⣿⣇⡀
|
||
⢠⣿⣯⣻⣿⣿⣿⣿⣿⣿⡟ ⠈⢻⣿⣿⣿⣿⣿⣿⣟⣾⣷⡀
|
||
⢠⣿⣿⣿⣷⣻⣿⣿⣿⣿⡟ ⢻⣿⣿⣿⣿⢟⣾⣿⣿⣿⡄
|
||
⣰⣿⣿⣿⣿⣿⣷⡿⣿⣿⠏ ⠻⣿⣿⢯⣿⣿⣿⣿⣿⣿⡄
|
||
⣰⣿⣿⣿⣿⣿⣿⣿⣿⡽⠏ ⠹⣯⣿⣿⣿⣿⣿⣿⣿⣿⣆'
|
||
logo_small_ink='9 8 6 5 4 3 2 1 0'
|
||
tty_saved=$(stty -g <&3)
|
||
# The window is titled "Makepad Builder" while it runs; the terminal's own
|
||
# title comes back afterwards.
|
||
screen_on() { printf '%s[22;0t%s]0;Makepad Builder\007%s[?1049h%s[?25l' "$e" "$e" "$e" "$e"; stty -icanon -echo min 1 time 0 <&3 2>/dev/null; }
|
||
screen_off() { stty "$tty_saved" <&3 2>/dev/null || :; printf '%s[?25h%s[?1049l%s]0;\007%s[23;0t' "$e" "$e" "$e" "$e"; }
|
||
# Background jobs ignore Ctrl+C (the shell starts them that way), so every
|
||
# exit stops them and all their children; their partial downloads stay for
|
||
# the next run.
|
||
on_exit() {
|
||
oe_status=$?
|
||
for oe_pid in $running_jobs ${b_pid:-} ${u_pid:-}; do kill_tree "$oe_pid"; done
|
||
screen_off
|
||
if [ -n "${quiet_exit:-}" ]; then printf '%s\n' "$quiet_exit"
|
||
elif [ "$oe_status" = 0 ]; then
|
||
if [ -n "$home" ] && [ -x "$home/makepad" ]; then printf 'Makepad Builder closed. Start it again with %s/makepad\n' "$home"
|
||
else printf 'Makepad Builder closed.\n'; fi
|
||
elif [ "$oe_status" != 130 ]; then
|
||
printf 'The Makepad Builder stopped unexpectedly; the details are in %s\n' "${log_file:-builder.log}" >&2
|
||
fi
|
||
}
|
||
trap on_exit EXIT
|
||
trap 'exit 130' INT TERM HUP
|
||
screen_on
|
||
|
||
# The terminal's background colour (OSC 11), or COLORFGBG: light terminals
|
||
# get light greys for the band and the logo. Dark when nobody says.
|
||
light_background() {
|
||
case "${COLORFGBG:-}" in *';7' | *';15') band=$band_light logo_color=$logo_light; return 0 ;; *';'*) return 0 ;; esac
|
||
[ -n "${BASH_VERSION:-}" ] || return 0
|
||
printf '%s]11;?\007' "$e"
|
||
lb_reply=
|
||
IFS= read -rs -d "$(printf '\007')" -t 1 lb_reply <&3 || :
|
||
case "$lb_reply" in *rgb:*) ;; *) return 0 ;; esac
|
||
lb_rgb=${lb_reply#*rgb:}
|
||
lb_r=${lb_rgb%%/*}; lb_rgb=${lb_rgb#*/}; lb_g=${lb_rgb%%/*}; lb_b=${lb_rgb#*/}
|
||
lb_r=${lb_r:0:2} lb_g=${lb_g:0:2} lb_b=${lb_b:0:2}
|
||
case "$lb_r$lb_g$lb_b" in *[!0-9a-fA-F]* | '') return 0 ;; esac
|
||
# Relative luminance on a 0-255 scale: light above the middle.
|
||
if [ $(( (2126 * 16#$lb_r + 7152 * 16#$lb_g + 722 * 16#$lb_b) / 10000 )) -gt 127 ]; then
|
||
band=$band_light logo_color=$logo_light
|
||
fi
|
||
}
|
||
light_background
|
||
|
||
size() {
|
||
set -- $(stty size <&3 2>/dev/null || echo 24 80)
|
||
height=${1:-24} width=${2:-80}
|
||
[ "$width" -le 80 ] || width=80
|
||
}
|
||
# Display width of plain text (UTF-8 continuation bytes do not count).
|
||
dwidth() { printf '%s' "$1" | LC_ALL=C tr -d '\200-\277' | wc -c | tr -d ' '; }
|
||
|
||
polling=0
|
||
key_cr=$(printf '\r') key_del=$(printf '\177') key_bs=$(printf '\010') key_nak=$(printf '\025')
|
||
# key: one key press as $key. Under bash (macOS's /bin/sh, and the Builder
|
||
# re-runs itself under /bin/bash where dash started it) the read builtin
|
||
# takes it, so no process runs while the Builder waits; only a system
|
||
# without bash at all reads one byte at a time with dd. Polling (a measurement still
|
||
# running) returns key=none after about a second.
|
||
key_read() { # key_read COUNT WAIT -> kr (WAIT: 0 blocks, tick a fifth of a second, else seconds)
|
||
if [ "$2" = tick ]; then
|
||
# bash 4 reads with a fractional timeout; bash 3.2 (macOS) and
|
||
# plain sh let the terminal time the read out instead.
|
||
if [ "${BASH_VERSINFO:-0}" -ge 4 ] 2>/dev/null; then
|
||
kr=
|
||
IFS= read -rsn "$1" -d '' -t 0.2 kr <&3; kr_status=$?
|
||
[ "$kr_status" -le 128 ] || kr_status=142
|
||
return "$kr_status"
|
||
fi
|
||
stty min 0 time 2 <&3 2>/dev/null
|
||
kr=$(dd bs=1 count="$1" <&3 2>/dev/null; printf x)
|
||
kr=${kr%x}
|
||
stty min 1 time 0 <&3 2>/dev/null
|
||
[ -n "$kr" ] || return 142
|
||
return 0
|
||
fi
|
||
if [ -n "${BASH_VERSION:-}" ]; then
|
||
kr=
|
||
if [ "$2" = 0 ]; then IFS= read -rsn "$1" -d '' kr <&3; kr_status=$?
|
||
else IFS= read -rsn "$1" -d '' -t "$2" kr <&3; kr_status=$?; fi
|
||
[ "$kr_status" -le 128 ] || kr_status=142
|
||
return "$kr_status"
|
||
fi
|
||
if [ "$2" = 0 ]; then stty min 1 time 0 <&3 2>/dev/null; else stty min 0 time $(($2 * 10)) <&3 2>/dev/null; fi
|
||
# No bash on this system at all: a byte-exact read (head would take the
|
||
# rest of an arrow key's bytes with it).
|
||
kr=$(dd bs=1 count="$1" <&3 2>/dev/null; printf x)
|
||
kr=${kr%x}
|
||
stty min 1 time 0 <&3 2>/dev/null
|
||
[ -n "$kr" ] || { [ "$2" = 0 ] && return 1; return 142; }
|
||
}
|
||
key() {
|
||
# Idle, the menu still looks up once a second (a resized window is
|
||
# drawn again); while work runs, a few times a second.
|
||
kw=0; [ "$polling" != 1 ] || kw=tick; [ "$polling" != idle ] || kw=1
|
||
# A timed wait also ends early when the window is resized (bash 3.2
|
||
# reports that like closed input); only input that keeps failing at
|
||
# once is closed.
|
||
key_read 1 "$kw" || {
|
||
kr_status=$?
|
||
if [ "$kr_status" = 142 ]; then key_fails=0; key=none; return; fi
|
||
if [ "$kw" != 0 ] && [ "${key_fails:-0}" -lt 50 ]; then key_fails=$((${key_fails:-0} + 1)); key=none; return; fi
|
||
exit 1
|
||
}
|
||
key_fails=0
|
||
k=$kr
|
||
case "$k" in
|
||
"$e")
|
||
key_read 2 1 || kr=
|
||
case "$kr" in '[A' | OA) key=up ;; '[B' | OB) key=down ;; '[C' | OC) key=right ;; '[D' | OD) key=left ;; '') key=esc ;; *) key=other ;; esac ;;
|
||
'
|
||
' | "$key_cr") key=enter ;;
|
||
"$key_del" | "$key_bs") key=backspace ;;
|
||
"$key_nak") key=clear ;;
|
||
'') key=enter ;;
|
||
*) key=$k ;;
|
||
esac
|
||
}
|
||
|
||
# ---------------------------------------------------------------- drawing ---
|
||
# A screen is a list of rows: "head|TITLE", "note|TEXT", "step|LABEL" or
|
||
# "item|ID|NAME|LICENSE|STATUS|ACTION"; ACTION "=" means the status already
|
||
# says what Return does, so only ⏎ is added.
|
||
message= choice= footer_override=
|
||
screen_meta() {
|
||
back=1 crumb= sub=
|
||
case "$screen" in
|
||
main) back=0 sub='Shipped as source code, so your coding agent can customize everything.' ;;
|
||
terms) crumb=' › License agreements' sub='Return opens an agreement in your browser.' ;;
|
||
consent) crumb=" › $consent_title" sub='Please read what you are agreeing to.' ;;
|
||
work) crumb=" › $work_title" sub=$work_sub ;;
|
||
signin) crumb=' › Log in' sub='Welcome to the Makepad Builder.' ;;
|
||
folder) crumb=' › Folder' sub='Everything the Builder installs goes into one folder.' ;;
|
||
xcode) crumb=' › Apple developer tools' sub='Needed to compile on macOS.' ;;
|
||
gpu) crumb=' › Graphics' sub='Before you continue.' ;;
|
||
packages) crumb=' › System packages' sub='Installed outside this folder, so please check them.' ;;
|
||
esac
|
||
default_footer='↑↓ move ⏎ select esc back q quit'
|
||
[ "$back" = 1 ] || default_footer='↑↓ move ⏎ select q quit'
|
||
case "$screen" in consent | gpu | xcode | packages) default_footer='↑↓ move ⏎ select esc cancel' ;; esac
|
||
}
|
||
screen_rows() {
|
||
case "$screen" in
|
||
main) main_rows ;;
|
||
terms) terms_rows ;;
|
||
consent) consent_rows ;;
|
||
work) printf 'note|\n'; printf '%s' "$work_labels" | sed 's/^/step|/' ;;
|
||
signin) signin_rows ;;
|
||
folder) folder_rows ;;
|
||
xcode) xcode_rows ;;
|
||
gpu) gpu_rows ;;
|
||
packages) packages_rows ;;
|
||
esac
|
||
}
|
||
# item_line NAME LICENSE STATUS ACTION SELECTED CHILD -> il. A row of an
|
||
# open node (CHILD 1) sits two columns in with a name column two shorter;
|
||
# on the main screen every row has the license column, so the statuses
|
||
# line up.
|
||
item_line() {
|
||
if [ "${6:-0}" = 1 ]; then il_in=' '; pad "$1" 13; else il_in=; pad "$1" 15; fi
|
||
if [ "$5" = 1 ]; then il=" ${mark}▌${r0} ${il_in}${b}${padded}${r0} "; else il=" ${il_in}${padded} "; fi
|
||
case "$2" in
|
||
commercial) il="${il}licensed " ;;
|
||
beta) il="${il}${warn}beta ${r0}" ;;
|
||
free) il="${il}${dim}free ${r0}" ;;
|
||
*) [ "$screen" != main ] || il="${il} " ;;
|
||
esac
|
||
il="${il}$3"
|
||
if [ "$5" = 1 ]; then
|
||
if [ "$4" = = ]; then il="${il} ${ok}⏎${r0}"
|
||
elif [ -n "$4" ]; then
|
||
if [ -z "$3" ]; then il="${il}${ok}$4 ⏎${r0}"; else il="${il} ${ok}$4 ⏎${r0}"; fi
|
||
fi
|
||
fi
|
||
}
|
||
spin_frame=0
|
||
# spin -> spin_c: the spinner's frame, one step per redraw.
|
||
spin() {
|
||
case $((spin_frame % 10)) in 0) spin_c=⠋ ;; 1) spin_c=⠙ ;; 2) spin_c=⠹ ;; 3) spin_c=⠸ ;; 4) spin_c=⠼ ;; 5) spin_c=⠴ ;; 6) spin_c=⠦ ;; 7) spin_c=⠧ ;; 8) spin_c=⠇ ;; *) spin_c=⠏ ;; esac
|
||
}
|
||
# doing_text STEP-FILE DEFAULT -> ls: the spinner, what the work does now,
|
||
# a short bar when its share is known and the amount (the Windows Builder
|
||
# draws the same).
|
||
doing_text() {
|
||
dt_state= dt_a=0 dt_b=0 dt_unit= dt_bytes=0 dt_doing=
|
||
IFS='|' read -r dt_state dt_a dt_b dt_unit dt_bytes dt_doing 2>/dev/null < "$1" || :
|
||
spin
|
||
ls="${acc}${spin_c}${r0} "
|
||
if [ "$dt_state" != now ]; then ls="$ls$2"; return 0; fi
|
||
case "$dt_doing" in resolving | '') dt_doing=compiling ;; downloading | compiling | finishing) ;; *) [ "$dt_unit" != MB ] || dt_doing=unpacking ;; esac
|
||
ls="$ls$dt_doing"
|
||
if [ "${dt_b:-0}" -gt 0 ] 2>/dev/null; then
|
||
dt_fill=$(( (dt_a * 12 + dt_b / 2) / dt_b )); [ "$dt_fill" -le 12 ] || dt_fill=12
|
||
dt_on= dt_off= dt_i=0
|
||
while [ "$dt_i" -lt 12 ]; do
|
||
if [ "$dt_i" -lt "$dt_fill" ]; then dt_on="${dt_on}━"; else dt_off="${dt_off}─"; fi
|
||
dt_i=$((dt_i + 1))
|
||
done
|
||
ls="$ls ${mark}${dt_on}${r0}${dim}${dt_off}${r0}"
|
||
if [ "$dt_unit" = crates ]; then ls="$ls ${dim}$dt_a / $dt_b crates${r0}"
|
||
elif [ "$dt_unit" = MB ] && [ "${dt_bytes:-0}" -gt 0 ]; then
|
||
ls="$ls ${dim}$((dt_a * dt_bytes / dt_b / 1048576)) / $((dt_bytes / 1048576)) MB${r0}"
|
||
fi
|
||
elif [ "$dt_unit" = crates ] && [ "${dt_a:-0}" -gt 0 ]; then
|
||
ls="$ls ${dim}$dt_a crates${r0}"
|
||
fi
|
||
}
|
||
# live_status ID -> ls: the row's status while background work involves it
|
||
# (compiling or downloading with its progress, its place in the queue, the
|
||
# update check); fails for any other row.
|
||
live_status() {
|
||
ls=
|
||
case "$1" in
|
||
updates) [ -n "$u_pid" ] || return 1; doing_text "$steps_dir/_update" 'checking for updates…' ;;
|
||
app:*)
|
||
if [ -n "$b_pid" ] && [ "$1" = "app:$b_app" ]; then
|
||
if [ "$b_stopping" = 1 ]; then spin; ls="${acc}${spin_c}${r0} stopping…"
|
||
else doing_text "$steps_dir/_build" "$b_starting"; fi
|
||
else
|
||
queue_place "${1#app:}" || return 1
|
||
if [ "$qp" = 0 ]; then ls="${acc}◌${r0} queued ${dim}· next${r0}"
|
||
else ls="${acc}◌${r0} queued ${dim}· $((qp + 1)) to go${r0}"; fi
|
||
fi ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
# info_line ID INFO -> li: the line under the rule when no message shows:
|
||
# what the selected row does, or, while an app compiles, what happens with it.
|
||
info_line() {
|
||
li=$2
|
||
[ -n "$b_pid" ] || return 0
|
||
if [ "$1" = "app:$b_app" ]; then
|
||
if [ "$b_stopping" = 1 ]; then li='Stopping the compiler; what was downloaded and compiled is kept.'
|
||
elif [ "$b_open" = 1 ]; then li='Compiling from source. It opens by itself when it is done.'
|
||
else li='Compiling from source. It only compiles; press s or ⏎ to open it when done.'; fi
|
||
elif case "$1" in app:*) queue_place "${1#app:}" ;; *) false ;; esac; then
|
||
if [ "$qp_open" = 1 ]; then li="Queued behind $b_title; it compiles and opens after it."
|
||
else li="Queued behind $b_title; it only compiles. Press s or ⏎ to open it when done."; fi
|
||
elif [ "$b_stopping" = 1 ]; then li="${dim}$b_title is stopping.${r0}"
|
||
elif [ "$b_open" = 1 ]; then li="${dim}$b_title opens by itself when it is done compiling.${r0}"
|
||
else li="${dim}$b_title is compiling; it only compiles and does not open.${r0}"; fi
|
||
}
|
||
# menu_keys ID -> mk: the main menu's keys; s and c only where the selected
|
||
# row has work to change, their letters in orange.
|
||
menu_keys() {
|
||
mk_open= mk_cancel=
|
||
if [ -n "$b_pid" ] && [ "$1" = "app:$b_app" ]; then
|
||
[ "$b_stopping" = 1 ] || mk_open=$b_open mk_cancel=1
|
||
elif case "$1" in app:*) queue_place "${1#app:}" ;; *) false ;; esac; then
|
||
mk_open=$qp_open mk_cancel=1
|
||
elif [ "$1" = all ]; then
|
||
queue_batch
|
||
[ "$qb" = 0 ] || mk_cancel=1
|
||
fi
|
||
mk="${dim}↑↓ move ⏎ select ${r0}"
|
||
if [ "$mk_open" = 1 ]; then mk="${mk}${key_c}s${r0}${dim} don't start ${r0}"
|
||
elif [ "$mk_open" = 0 ]; then mk="${mk}${key_c}s${r0}${dim} start when done ${r0}"; fi
|
||
[ -z "$mk_cancel" ] || mk="${mk}${key_c}c${r0}${dim} cancel ${r0}"
|
||
mk="${mk}${dim}q quit${r0}"
|
||
}
|
||
# wrap_status TEXT WIDTH -> ws_1 ws_2: a status line longer than WIDTH
|
||
# broken at a space onto a second line (after a leading ✓ or ✗ the second
|
||
# line starts under the text); what still does not fit ends in "…". Plain
|
||
# sh (no bash) leaves it as it is.
|
||
wrap_status() {
|
||
ws_1=$1 ws_2= ws_width=$2
|
||
[ -n "${BASH_VERSION:-}" ] || return 0
|
||
ws_plain=${1//$e\[*([0-9;])m/}
|
||
[ "${#ws_plain}" -gt "$2" ] || return 0
|
||
ws_hang=
|
||
case "$ws_plain" in '✓ '* | '✗ '*) ws_hang=' ' ;; esac
|
||
ws_1= ws_n=0 ws_line=1
|
||
set -f
|
||
# shellcheck disable=SC2086 # split into words on purpose
|
||
set -- $1
|
||
set +f
|
||
for ws_word in "$@"; do
|
||
ws_wp=${ws_word//$e\[*([0-9;])m/}
|
||
ws_add=${#ws_wp}; [ "$ws_n" = 0 ] || ws_add=$((ws_add + 1))
|
||
if [ "$ws_line" = 1 ] && [ $((ws_n + ws_add)) -gt "$ws_width" ] && [ "$ws_n" -gt 0 ]; then
|
||
ws_line=2 ws_n=${#ws_hang}
|
||
# The colour in force at the break carries over.
|
||
ws_tail=${ws_1##*"$e["}
|
||
ws_carry=
|
||
[ "$ws_tail" = "$ws_1" ] || { ws_code=${ws_tail%%m*}; [ "$ws_code" = 0 ] || ws_carry="$e[${ws_code}m"; }
|
||
ws_2="$ws_hang$ws_carry$ws_word" ws_n=$((ws_n + ${#ws_wp}))
|
||
continue
|
||
fi
|
||
if [ "$ws_line" = 2 ] && [ $((ws_n + ws_add)) -gt "$ws_width" ]; then ws_2="$ws_2…"; break; fi
|
||
if [ "$ws_line" = 1 ]; then
|
||
if [ "$ws_n" = 0 ]; then ws_1=$ws_word; else ws_1="$ws_1 $ws_word"; fi
|
||
else
|
||
ws_2="$ws_2 $ws_word"
|
||
fi
|
||
ws_n=$((ws_n + ws_add))
|
||
done
|
||
ws_1="$ws_1$r0" ws_2="$ws_2$r0"
|
||
}
|
||
# The line of a work step, from its state file.
|
||
step_line() { # step_line LABEL -> sl
|
||
step_key "$1"
|
||
sl_state= sl_a= sl_b= sl_unit= sl_bytes= sl_doing=
|
||
IFS='|' read -r sl_state sl_a sl_b sl_unit sl_bytes sl_doing < "$steps_dir/$step_key" 2>/dev/null || sl_state=next
|
||
pad "$1 " 16
|
||
case "$sl_state" in
|
||
done) sl=" ${ok}✓${r0} ${padded}${dim}${sl_a}${r0}" ;;
|
||
failed) sl=" ${red}✗${r0} ${padded}${red}${sl_a}${r0}" ;;
|
||
held) sl=" ${warn}◌${r0} ${padded}${dim}${sl_a}${r0}" ;;
|
||
now)
|
||
sl=" ${acc}●${r0} ${b}${padded}${r0}"
|
||
sl_detail=
|
||
if [ "${sl_b:-0}" -gt 0 ]; then
|
||
if [ "$sl_unit" = crates ]; then sl_width=30; else sl_width=16; fi
|
||
sl_fill=$((sl_a * sl_width / sl_b)); [ "$sl_fill" -le "$sl_width" ] || sl_fill=$sl_width
|
||
sl_on= sl_off= sl_i=0
|
||
while [ "$sl_i" -lt "$sl_width" ]; do
|
||
if [ "$sl_i" -lt "$sl_fill" ]; then sl_on="${sl_on}━"; else sl_off="${sl_off}─"; fi
|
||
sl_i=$((sl_i + 1))
|
||
done
|
||
sl_pct=$((sl_a * 100 / sl_b)); [ "$sl_pct" -le 100 ] || sl_pct=100
|
||
sl_p=" $sl_pct"; sl_p=${sl_p#"${sl_p%???}"}
|
||
sl="${sl}${mark}${sl_on}${r0}${dim}${sl_off}${r0} ${sl_p}%"
|
||
if [ "$sl_unit" = crates ]; then
|
||
sl_detail="$sl_a / $sl_b crates"
|
||
elif [ "$sl_unit" = MB ] && [ "${sl_bytes:-0}" -gt 0 ]; then
|
||
sl_mb_total=$((sl_bytes / 1048576))
|
||
sl_mb_done=$((sl_a * sl_mb_total / sl_b))
|
||
[ "$sl_mb_total" = 0 ] || sl_detail="$sl_mb_done/$sl_mb_total MB"
|
||
# Throughput and time left while it downloads.
|
||
sl_t=$(now)
|
||
eval "sl_t0=\${rate_t_$step_key:-} sl_b0=\${rate_b_$step_key:-}"
|
||
if [ -z "$sl_t0" ] || [ "$sl_doing" != downloading ]; then
|
||
eval "rate_t_$step_key=\$sl_t rate_b_$step_key=\$sl_mb_done"
|
||
elif [ "$sl_t" -gt "$sl_t0" ]; then
|
||
sl_rate=$(( (sl_mb_done - sl_b0) / (sl_t - sl_t0) ))
|
||
if [ "$sl_rate" -gt 0 ]; then
|
||
sl_left=$(( (sl_mb_total - sl_mb_done) / sl_rate ))
|
||
sl_detail="$sl_detail · $sl_rate MB/s"
|
||
[ "$sl_left" -lt 1 ] || sl_detail="$sl_detail · $sl_left s left"
|
||
fi
|
||
fi
|
||
fi
|
||
else
|
||
spin_frame=$((${spin_frame:-0} + 1))
|
||
case $((spin_frame % 10)) in 0) sl_c=⠋ ;; 1) sl_c=⠙ ;; 2) sl_c=⠹ ;; 3) sl_c=⠸ ;; 4) sl_c=⠼ ;; 5) sl_c=⠴ ;; 6) sl_c=⠦ ;; 7) sl_c=⠧ ;; 8) sl_c=⠇ ;; *) sl_c=⠏ ;; esac
|
||
sl="${sl}${acc}${sl_c}${r0}"
|
||
# No total yet: the crates compiled so far still count up.
|
||
[ "$sl_unit" != crates ] || [ "${sl_a:-0}" = 0 ] || sl_detail="$sl_a crates"
|
||
fi
|
||
[ -z "$sl_doing" ] || sl_detail="${sl_detail:+$sl_detail · }$sl_doing"
|
||
[ -z "$sl_detail" ] || sl="${sl} ${dim}${sl_detail}${r0}" ;;
|
||
*) sl=" ${dim}○ ${padded}${r0}" ;;
|
||
esac
|
||
}
|
||
|
||
# logo_lines LINES BAND -> ll_out: LINES as screen lines, each ending in an
|
||
# erase to the end of the line; line BAND (0-based, -1 for none) on the
|
||
# selection band. The Makepad mark goes faintly at the right of the window
|
||
# (up to 100 columns), in the middle of the stretch or as near to it as it
|
||
# fits beside every line's text with two columns of air: the large one when
|
||
# it fits somewhere, else the small one, else none, so it is never cut into.
|
||
# The selected row's "<action> ⏎" does not count (it moves with the
|
||
# selection); where it reaches into the mark, the text wins. Plain sh (no
|
||
# bash) draws no band and no mark.
|
||
logo_lines() { # logo_lines LINES BAND [SIZES] (the last line stays empty)
|
||
ll_out= ll_lines=$1 ll_band=$2 ll_sizes=${3:-big small}
|
||
if [ -z "${BASH_VERSION:-}" ]; then
|
||
while IFS= read -r ll_line; do ll_out="$ll_out$ll_line$e[K
|
||
"; done <<EOF
|
||
$ll_lines
|
||
EOF
|
||
return 0
|
||
fi
|
||
# Where each line's own text ends, plus two columns of air.
|
||
ll_clear= ll_count=0
|
||
while IFS= read -r ll_line; do
|
||
if [ "$ll_count" = "$ll_band" ]; then
|
||
case "$ll_line" in *"⏎$r0") ll_line=${ll_line%"$ok"*} ;; esac
|
||
fi
|
||
ll_plain=${ll_line//$e\[*([0-9;])m/}
|
||
ll_plain=${ll_plain%%+( )}
|
||
if [ -n "$ll_plain" ]; then ll_clear="$ll_clear $(( ${#ll_plain} + 2 ))"; else ll_clear="$ll_clear 0"; fi
|
||
ll_count=$((ll_count + 1))
|
||
done <<EOF
|
||
$ll_lines
|
||
EOF
|
||
ll_cols=$(stty size <&3 2>/dev/null); ll_cols=${ll_cols#* }; ll_cols=${ll_cols:-80}
|
||
[ "$ll_cols" -le 100 ] || ll_cols=100
|
||
ll_art= ll_y0=-1 ll_x0=0
|
||
ll_room=$((ll_count - 1))
|
||
for ll_size in $ll_sizes; do
|
||
if [ "$ll_size" = big ]; then ll_h=13 ll_w=44 ll_ink=$logo_big_ink; else ll_h=9 ll_w=30 ll_ink=$logo_small_ink; fi
|
||
ll_x0=$((ll_cols - 2 - ll_w))
|
||
[ "$ll_x0" -ge 30 ] && [ "$ll_h" -le "$ll_room" ] || continue
|
||
ll_mid=$(( (ll_room - ll_h) / 2 )) ll_d=0
|
||
while [ "$ll_d" -le "$ll_room" ]; do
|
||
for ll_y in $((ll_mid - ll_d)) $((ll_mid + ll_d)); do
|
||
[ "$ll_y" -ge 0 ] && [ $((ll_y + ll_h)) -le "$ll_room" ] || continue
|
||
# shellcheck disable=SC2086
|
||
set -- $ll_clear
|
||
shift "$ll_y"
|
||
ll_fit=1
|
||
for ll_i in $ll_ink; do
|
||
[ "$1" -le $((ll_x0 + ll_i)) ] || { ll_fit=0; break; }
|
||
shift
|
||
done
|
||
if [ "$ll_fit" = 1 ]; then ll_y0=$ll_y; break 3; fi
|
||
done
|
||
ll_d=$((ll_d + 1))
|
||
done
|
||
done
|
||
if [ "$ll_y0" -ge 0 ]; then
|
||
if [ "$ll_size" = big ]; then ll_art=$logo_big; else ll_art=$logo_small; fi
|
||
fi
|
||
ll_i=0
|
||
while IFS= read -r ll_line; do
|
||
ll_piece=
|
||
if [ -n "$ll_art" ] && [ "$ll_i" -ge "$ll_y0" ] && [ "$ll_i" -lt $((ll_y0 + ll_h)) ]; then
|
||
ll_n=$((ll_i - ll_y0))
|
||
while IFS= read -r ll_a; do
|
||
[ "$ll_n" -gt 0 ] || { ll_piece=$ll_a; break; }
|
||
ll_n=$((ll_n - 1))
|
||
done <<EOF
|
||
$ll_art
|
||
EOF
|
||
fi
|
||
ll_line=${ll_line%"$r0"}; ll_line=${ll_line%%+( )}
|
||
ll_plain=${ll_line//$e\[*([0-9;])m/}
|
||
[ "$ll_line" = "$ll_plain" ] || ll_line="$ll_line$r0"
|
||
# The mark's part of this line: from two columns after the text.
|
||
ll_at=$ll_x0
|
||
if [ -n "${ll_piece// /}" ]; then
|
||
ll_skip=$(( ${#ll_plain} + 2 - ll_x0 )); [ "$ll_skip" -ge 0 ] || ll_skip=0
|
||
ll_piece=${ll_piece:$ll_skip}; ll_at=$((ll_x0 + ll_skip))
|
||
[ -n "${ll_piece// /}" ] || ll_piece=
|
||
else
|
||
ll_piece=
|
||
fi
|
||
if [ "$ll_i" = "$ll_band" ]; then
|
||
# The band: every colour reset goes back to the band, which
|
||
# reaches the rule's right end; the mark's dots lie on it.
|
||
ll_band_end=$((width - 2))
|
||
ll_line="${ll_line:0:2}$band${ll_line:2}"
|
||
ll_line=${ll_line//$r0/$r0$band}
|
||
ll_used=${#ll_plain}
|
||
if [ -n "$ll_piece" ]; then
|
||
ll_pad=$((ll_at - ll_used)); while [ "$ll_pad" -gt 0 ]; do ll_line="$ll_line "; ll_pad=$((ll_pad - 1)); done
|
||
ll_on=$((ll_band_end - ll_at)); [ "$ll_on" -ge 0 ] || ll_on=0
|
||
ll_line="$ll_line$logo_color${ll_piece:0:$ll_on}$r0"
|
||
ll_rest=${ll_piece:$ll_on}
|
||
ll_used=$((ll_at + ${#ll_piece}))
|
||
if [ -n "$ll_rest" ]; then ll_line="$ll_line$logo_color$ll_rest"
|
||
else ll_line="$ll_line$band"; fi
|
||
fi
|
||
ll_pad=$((ll_band_end - ll_used)); while [ "$ll_pad" -gt 0 ]; do ll_line="$ll_line "; ll_pad=$((ll_pad - 1)); done
|
||
ll_line="$ll_line$r0"
|
||
elif [ -n "$ll_piece" ]; then
|
||
ll_pad=$((ll_at - ${#ll_plain})); while [ "$ll_pad" -gt 0 ]; do ll_line="$ll_line "; ll_pad=$((ll_pad - 1)); done
|
||
ll_line="$ll_line$logo_color$ll_piece$r0"
|
||
fi
|
||
ll_out="$ll_out$ll_line$e[K
|
||
"
|
||
ll_i=$((ll_i + 1))
|
||
done <<EOF
|
||
$ll_lines
|
||
EOF
|
||
}
|
||
draw() {
|
||
size
|
||
screen_meta
|
||
rows_out=$(screen_rows)
|
||
spin_frame=$((spin_frame + 1))
|
||
# The header, then the rows: everything above the footer scrolls as one.
|
||
# The email shows on the Account row, not in the header.
|
||
body="
|
||
${b}Makepad${r0} Apps${crumb}
|
||
${dim}${sub}${r0}
|
||
" nbody=3 sel_line=-1 d_item=0 d_first=1 first_item=-1 item_lines= d_sel_id= d_info=
|
||
while IFS='|' read -r d_kind d_1 d_2 d_3 d_4 d_5 d_6; do
|
||
case "$d_kind" in
|
||
head)
|
||
if [ "$d_first" = 0 ] || [ "$back" = 0 ]; then body="$body
|
||
"; nbody=$((nbody + 1)); fi
|
||
body="$body ${dim}${d_1}${r0}
|
||
"; nbody=$((nbody + 1)) ;;
|
||
note) body="$body ${d_1}
|
||
"; nbody=$((nbody + 1)) ;;
|
||
step) step_line "$d_1"; body="$body$sl
|
||
"; nbody=$((nbody + 1)) ;;
|
||
item | sub)
|
||
if [ "$d_item" = 0 ] && [ "$back" = 1 ]; then body="$body
|
||
"; nbody=$((nbody + 1)); fi
|
||
[ "$first_item" -ge 0 ] || first_item=$nbody
|
||
item_lines="$item_lines $nbody"
|
||
d_sel=0; [ "$d_item" != "$sel" ] || { d_sel=1; sel_line=$nbody; d_sel_id=$d_1 d_info=$d_6; }
|
||
# Work in progress on the row: no action; its keys are in the footer.
|
||
if [ "$screen" = main ] && live_status "$d_1"; then d_4=$ls d_5=; fi
|
||
d_child=0; [ "$d_kind" != sub ] || d_child=1
|
||
item_line "$d_2" "$d_3" "$d_4" "$d_5" "$d_sel" "$d_child"
|
||
body="$body$il
|
||
"; nbody=$((nbody + 1)); d_item=$((d_item + 1)) ;;
|
||
esac
|
||
d_first=0
|
||
done <<EOF
|
||
$rows_out
|
||
EOF
|
||
items=$d_item
|
||
# The footer (the rule, two status lines, the keys) keeps the last four
|
||
# lines; one line above it stays empty or says what is scrolled away.
|
||
room=$((height - 5))
|
||
[ "$room" -ge 3 ] || room=3
|
||
if [ "$nbody" -le "$room" ]; then top=0; else
|
||
if [ "$sel_line" -ge 0 ]; then
|
||
# The first row brings the header back into view.
|
||
[ "$sel_line" -gt "$first_item" ] || top=0
|
||
[ "$sel_line" -ge "$top" ] || top=$sel_line
|
||
[ "$sel_line" -lt $((top + room)) ] || top=$((sel_line - room + 1))
|
||
fi
|
||
[ "$top" -le $((nbody - room)) ] || top=$((nbody - room))
|
||
fi
|
||
# The lines shown; the mark lies beside the rows, not the header.
|
||
d_i=0 d_shown=0 d_head= d_lines= d_band=-1 d_region=0
|
||
while IFS= read -r d_line; do
|
||
if [ "$d_i" -ge "$top" ] && [ "$d_shown" -lt "$room" ] && [ "$d_i" -lt "$nbody" ]; then
|
||
if [ "$top" = 0 ] && [ "$d_i" -lt 3 ]; then
|
||
d_head="$d_head$d_line${e}[K
|
||
"
|
||
else
|
||
[ "$d_i" != "$sel_line" ] || d_band=$d_region
|
||
d_lines="$d_lines$d_line
|
||
"
|
||
d_region=$((d_region + 1))
|
||
fi
|
||
d_shown=$((d_shown + 1))
|
||
fi
|
||
d_i=$((d_i + 1))
|
||
done <<EOF
|
||
$body
|
||
EOF
|
||
d_last=' '
|
||
if [ "$nbody" -gt "$room" ]; then
|
||
d_above=0 d_below=0
|
||
for d_n in $item_lines; do
|
||
if [ "$d_n" -lt "$top" ]; then d_above=$((d_above + 1))
|
||
elif [ "$d_n" -ge $((top + room)) ]; then d_below=$((d_below + 1)); fi
|
||
done
|
||
if [ "$d_above" = 0 ]; then d_last=" ${dim}↓ $d_below more${r0}"
|
||
elif [ "$d_below" = 0 ]; then d_last=" ${dim}↑ $d_above above${r0}"
|
||
else d_last=" ${dim}↑ $d_above above · ↓ $d_below more${r0}"; fi
|
||
fi
|
||
# The mark sits beside the rows where the large or the small one fits,
|
||
# else the rule moves down just far enough for the small one (a short
|
||
# page like Log in gets it under its text), else there is none.
|
||
logo_lines "$d_lines$d_last" "$d_band"
|
||
if [ -n "${BASH_VERSION:-}" ] && [ -z "$ll_art" ]; then
|
||
d_more=$d_lines d_n=$d_shown
|
||
while [ "$d_n" -lt "$room" ]; do
|
||
d_more="$d_more
|
||
"; d_n=$((d_n + 1))
|
||
logo_lines "$d_more$d_last" "$d_band" small
|
||
[ -z "$ll_art" ] || break
|
||
done
|
||
[ -n "$ll_art" ] || logo_lines "$d_lines$d_last" "$d_band"
|
||
fi
|
||
d_rule=; d_i=4; while [ "$d_i" -lt "$width" ]; do d_rule="${d_rule}─"; d_i=$((d_i + 1)); done
|
||
# A status message longer than the line goes on onto the choice line
|
||
# when that is free. With no message, the main menu says what the
|
||
# selected row does; its keys depend on the row too.
|
||
d_message=$message d_footer=${footer_override:-$default_footer}
|
||
if [ "$screen" = main ] && [ -z "$footer_override" ]; then
|
||
[ -n "$message" ] || [ -n "$choice" ] || { info_line "$d_sel_id" "$d_info"; d_message=$li; }
|
||
menu_keys "$d_sel_id"; d_footer=$mk
|
||
else
|
||
d_footer="${dim}${d_footer}${r0}"
|
||
fi
|
||
d_choice=$choice
|
||
if [ -z "$choice" ]; then wrap_status "$d_message" $((width - 4)); d_message=$ws_1 d_choice=$ws_2; fi
|
||
printf '%s' "${e}[H$d_head$ll_out ${dim}${d_rule}${r0}${e}[K
|
||
${d_message}${e}[K
|
||
${d_choice}${e}[K
|
||
${d_footer}${e}[K${e}[J"
|
||
}
|
||
# The work page's steps, redrawn in place about ten times a second.
|
||
work_redraw() {
|
||
[ "$screen" = work ] || return 0
|
||
wr_row=5 wr_out=
|
||
while IFS= read -r wr_label; do
|
||
[ -n "$wr_label" ] || continue
|
||
step_line "$wr_label"
|
||
wr_out="$wr_out${e}[${wr_row};1H$sl${e}[K"
|
||
wr_row=$((wr_row + 1))
|
||
done <<EOF
|
||
$work_labels
|
||
EOF
|
||
printf '%s' "$wr_out"
|
||
}
|
||
busy() { message="${acc}⠋${r0} $1"; draw; }
|
||
|
||
# choose QUESTION NOTE DEFAULT OPTION...: the question on the status line and
|
||
# the options under it; ←→ or a first letter picks, ⏎ accepts, esc backs out.
|
||
choose() {
|
||
ch_q=$1 ch_note=$2 ch_pick=$3; shift 3
|
||
ch_saved_footer=$footer_override
|
||
while :; do
|
||
choice=
|
||
for ch_opt in "$@"; do
|
||
if [ "$ch_opt" = "$ch_pick" ]; then choice="${choice}${inv} ${ch_opt} ${r0} "; else choice="${choice} ${ch_opt} "; fi
|
||
done
|
||
[ -z "$ch_note" ] || choice="${choice} ${dim}${ch_note}${r0}"
|
||
message=$ch_q footer_override='←→ choose ⏎ accept esc back'
|
||
draw
|
||
key
|
||
case "$key" in
|
||
enter) chosen=$ch_pick; break ;;
|
||
esc | q) chosen=; break ;;
|
||
left | up | right | down)
|
||
ch_prev= ch_next= ch_found=
|
||
for ch_opt in "$@"; do
|
||
if [ -n "$ch_found" ] && [ -z "$ch_next" ]; then ch_next=$ch_opt; fi
|
||
[ "$ch_opt" != "$ch_pick" ] || ch_found=1
|
||
[ -n "$ch_found" ] || ch_prev=$ch_opt
|
||
done
|
||
case "$key" in left | up) [ -z "$ch_prev" ] || ch_pick=$ch_prev ;; *) [ -z "$ch_next" ] || ch_pick=$ch_next ;; esac ;;
|
||
*)
|
||
ch_hit=
|
||
for ch_opt in "$@"; do
|
||
case "$ch_opt" in "$key"*) ch_hit=$ch_opt; break ;; esac
|
||
done
|
||
if [ -n "$ch_hit" ]; then chosen=$ch_hit; break; fi ;;
|
||
esac
|
||
done
|
||
message= choice= footer_override=$ch_saved_footer
|
||
[ -n "$chosen" ]
|
||
}
|
||
|
||
# edit PROMPT INITIAL HINT FOOTER: a one-line editor on the status line.
|
||
# Escape returns 1. Sets edited.
|
||
edit() {
|
||
edited=$2
|
||
ed_saved_footer=$footer_override
|
||
while :; do
|
||
message="$1 ${b}${edited}${r0}${inv} ${r0}" choice=$3 footer_override=$4
|
||
draw
|
||
key
|
||
case "$key" in
|
||
enter) break ;;
|
||
esc) edited=; message= choice= footer_override=$ed_saved_footer; return 1 ;;
|
||
backspace) edited=$(printf '%s' "$edited" | sed 's/.$//') ;;
|
||
clear) edited= ;;
|
||
up | down | left | right | other) ;;
|
||
*) [ "${#edited}" -ge 200 ] || edited="$edited$key" ;;
|
||
esac
|
||
done
|
||
edited=$(printf '%s' "$edited" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||
message= choice= footer_override=$ed_saved_footer
|
||
return 0
|
||
}
|
||
select_row() {
|
||
sr_row=$(printf '%s\n' "$rows_out" | grep -E '^(item|sub)\|' | sed -n "$((sel + 1))p")
|
||
IFS='|' read -r sr_kind id title sr_lic sr_status action sr_info <<EOF
|
||
$sr_row
|
||
EOF
|
||
}
|
||
# Hand the terminal to Apple, sudo, an agent or a shell, and take it back.
|
||
pause_screen() { screen_off; }
|
||
resume_screen() { screen_on; }
|
||
|
||
# ------------------------------------------------------------ work pages ---
|
||
work_begin() { # work_begin TITLE SUBTITLE STEP...
|
||
w_screen=$screen w_sel=$sel w_top=$top
|
||
work_title=$1 work_sub=$2; shift 2
|
||
work_labels= work_labels_keys=
|
||
for wb_label in "$@"; do work_add "$wb_label"; done
|
||
screen=work message= choice= footer_override='working · ctrl+c stops' top=0
|
||
draw
|
||
}
|
||
work_add() {
|
||
work_labels="$work_labels$1
|
||
"
|
||
step_key "$1"; work_labels_keys="$work_labels_keys $step_key"
|
||
put "$steps_dir/$step_key" next
|
||
[ "$screen" != work ] || draw
|
||
}
|
||
# Keys pressed and trackpad scrolls (arrow keys in a terminal) during work
|
||
# are not answers: throw away what is waiting, so the menu keeps its row.
|
||
drain_keys() {
|
||
stty min 0 time 0 <&3 2>/dev/null || return 0
|
||
dd bs=256 count=64 <&3 >/dev/null 2>&1 || :
|
||
stty min 1 time 0 <&3 2>/dev/null || :
|
||
}
|
||
# work_end ok|failed MESSAGE: a failure stays on the page, marked on its
|
||
# step, until Return or Escape.
|
||
work_end() {
|
||
drain_keys
|
||
if [ "$1" != ok ]; then
|
||
footer_override='⏎ back'
|
||
draw
|
||
while :; do key; case "$key" in enter | esc | q) break ;; esac; done
|
||
fi
|
||
footer_override= screen=$w_screen sel=$w_sel top=$w_top message=$2 choice=
|
||
}
|
||
# The first failed step's reason, for the status line.
|
||
work_failure() {
|
||
wf=
|
||
for wf_key in $work_labels_keys; do
|
||
IFS='|' read -r wf_state wf_reason wf_rest < "$steps_dir/$wf_key" 2>/dev/null || continue
|
||
if [ "$wf_state" = failed ]; then wf=$wf_reason; return; fi
|
||
done
|
||
}
|
||
|
||
# ---------------------------------------------------------------- screens ---
|
||
# wrap STYLE TEXT: note rows of at most 74 columns.
|
||
wrap() {
|
||
printf '%s\n' "$2" | awk -v style="$1" -v r0="$r0" -v w=74 '{
|
||
n = split($0, a, " "); line = ""
|
||
for (i = 1; i <= n; i++) {
|
||
if (line != "" && length(line) + 1 + length(a[i]) > w) { print "note|" style line (style == "" ? "" : r0); line = a[i] }
|
||
else line = (line == "" ? a[i] : line " " a[i])
|
||
}
|
||
if (line != "") print "note|" style line (style == "" ? "" : r0)
|
||
}'
|
||
}
|
||
signin_rows() {
|
||
printf 'note|\n'
|
||
wrap '' 'Enter the email address you bought your Makepad product with, or have beta access for. Leave it empty for the free apps.'
|
||
}
|
||
folder_rows() {
|
||
printf 'note|\n'
|
||
wrap '' "The apps' sources, their build output and a private Rust go into one folder of their own. Nothing outside it changes, except Apple's developer tools or your distribution's packages, which the Builder asks about first."
|
||
printf 'note|\n'
|
||
wrap "$dim" 'A folder the Builder used before is reused as it is: its email, apps and choices stay.'
|
||
}
|
||
gpu_notice='Makepad relies on your GPU to draw its UI and implement AI functionality just like a videogame does. Old hardware and broken drivers can cause your computer to reboot unexpectedly.'
|
||
gpu_rows() {
|
||
printf 'note|\n'
|
||
wrap '' "$gpu_notice"
|
||
printf '%s\n' 'item|continue|I understand|||' 'item|cancel|Cancel|||'
|
||
}
|
||
xcode_rows() {
|
||
printf 'note|\n'
|
||
if [ "$tools_problem" = license ]; then
|
||
wrap '' "Xcode is installed, but its license has not been accepted yet, so Apple's compiler will not run."
|
||
printf 'note|\n'
|
||
wrap "$dim" 'Apple shows the license here in the terminal and asks for your password; type agree at the end to accept it.'
|
||
pad 'Read and accept the Xcode license' 34
|
||
printf 'item|license|%s|||sudo xcodebuild -license\n' "$padded"
|
||
else
|
||
wrap '' "Makepad compiles with Apple's command line developer tools: clang, the macOS SDK and git. They are not installed on this Mac yet."
|
||
printf 'note|\n'
|
||
wrap "$dim" "Apple's installer opens in its own window (about 1 GB); come back here when it has finished."
|
||
pad 'Install the developer tools' 34
|
||
printf "item|install|%s|||opens Apple's installer\n" "$padded"
|
||
fi
|
||
printf '%s\n' 'item|check|Check again|||' 'item|cancel|Cancel|||'
|
||
}
|
||
packages_rows() {
|
||
printf 'note|\n'
|
||
set -- $packages_cmd
|
||
pr_manager=$1 pr_list= pr_count=0 pr_on=
|
||
for pr_word in "$@"; do
|
||
case "$pr_word" in
|
||
install | -S) pr_on=1 ;;
|
||
-*) ;;
|
||
*) if [ -n "$pr_on" ]; then pr_list="$pr_list $pr_word"; pr_count=$((pr_count + 1)); fi ;;
|
||
esac
|
||
done
|
||
wrap '' "Makepad compiles from source and needs $pr_count development packages from $distro_name. They are installed system-wide by your package manager:"
|
||
printf 'note|\n'
|
||
printf '%s\n' $pr_list | paste -d' ' - - - - - | sed "s/^/note|${dim} /; s/\$/${r0}/"
|
||
printf 'note|\n'
|
||
printf 'item|install|Install with sudo|||sudo %s\n' "$pr_manager"
|
||
printf 'item|cancel|Cancel|||\n'
|
||
}
|
||
|
||
# ------------------------------------------------------------ agreements ---
|
||
agreements='makepad|Makepad commercial license|https://makepad.nl/commercial-license
|
||
rust|Rust|https://www.rust-lang.org/policies/licenses'
|
||
accepted_agreements() {
|
||
if [ -f "$root/agreements-accepted" ]; then
|
||
accepted=$(tr '\n' ' ' < "$root/agreements-accepted")
|
||
elif [ "${m_rust_ok:-0}" = 1 ] && [ -z "${tools_problem:-}" ]; then
|
||
# Folders set up before this record accepted them when installing.
|
||
accepted='makepad rust'
|
||
else
|
||
accepted=
|
||
fi
|
||
}
|
||
is_accepted() { case " $accepted " in *" $1 "*) return 0 ;; esac; return 1; }
|
||
record_agreements() { put "$root/agreements-accepted" "$(printf '%s\n' "$@")"; accepted="$*"; }
|
||
host_of() { host=${1#https://}; host=${host%%/*}; }
|
||
open_url() {
|
||
if [ "$plat" = mac ]; then open "$1" >/dev/null 2>&1 || return 1
|
||
else xdg-open "$1" >/dev/null 2>&1 || return 1; fi
|
||
}
|
||
open_agreement() {
|
||
oa_url=$(printf '%s\n' "$agreements" | awk -F'|' -v id="$1" '$1 == id { print $3 }')
|
||
if open_url "$oa_url"; then message="${ok}✓${r0} Opened ${oa_url#https://} in your browser."
|
||
else message="${warn}Could not open $oa_url${r0}"; fi
|
||
}
|
||
terms_rows() {
|
||
accepted_agreements
|
||
while IFS='|' read -r tr_id tr_name tr_url; do
|
||
host_of "$tr_url"; pad "$tr_name" 36
|
||
if is_accepted "$tr_id"; then tr_state="${ok}✓${r0} accepted ${dim}${host}${r0}"; else tr_state="${warn}not accepted ${r0}${dim}${host}${r0}"; fi
|
||
printf 'item|url:%s|%s||%s|open in browser\n' "$tr_id" "$padded" "$tr_state"
|
||
done <<EOF
|
||
$agreements
|
||
EOF
|
||
printf '%s\n' 'note|' 'item|agree|Agree to all|||accept every agreement above' 'item|disagree|Disagree|||withdraw acceptance'
|
||
}
|
||
terms_screen() {
|
||
t_screen=$screen t_sel=$sel t_top=$top
|
||
screen=terms sel=0 top=0 message=
|
||
while :; do
|
||
draw; key; message=
|
||
case "$key" in
|
||
up) sel=$(( (sel + items - 1) % items )) ;;
|
||
down) sel=$(( (sel + 1) % items )) ;;
|
||
esc) break ;;
|
||
q) exit 0 ;;
|
||
enter)
|
||
select_row
|
||
case "$id" in
|
||
agree) record_agreements makepad rust; message="${ok}✓${r0} Agreements accepted." ;;
|
||
disagree) record_agreements; message="${warn}Agreements withdrawn: installing or updating build tools will ask again.${r0}" ;;
|
||
url:*) open_agreement "${id#url:}" ;;
|
||
esac ;;
|
||
esac
|
||
done
|
||
t_message=$message
|
||
screen=$t_screen sel=$t_sel top=$t_top message=$t_message
|
||
}
|
||
# consent TITLE INTRO DETAIL ACTION ID...: one screen for the terms a step
|
||
# needs; Return opens an agreement, "Agree to all" (selected at the start)
|
||
# records them, Cancel or Escape backs out. Already accepted: no screen.
|
||
consent() {
|
||
accepted_agreements
|
||
c_title=$1 c_intro=$2 c_detail=$3 c_action=$4; shift 4
|
||
c_missing=
|
||
for c_id in "$@"; do is_accepted "$c_id" || c_missing="$c_missing $c_id"; done
|
||
[ -n "$c_missing" ] || return 0
|
||
c_screen=$screen c_sel=$sel c_top=$top c_message=$message
|
||
consent_title=$c_title consent_ids="$*"
|
||
screen=consent top=0 message=
|
||
sel=$#
|
||
c_result=1
|
||
while :; do
|
||
draw; key; message=
|
||
case "$key" in
|
||
up) sel=$(( (sel + items - 1) % items )) ;;
|
||
down) sel=$(( (sel + 1) % items )) ;;
|
||
esc | q) break ;;
|
||
enter)
|
||
select_row
|
||
case "$id" in
|
||
agree) record_agreements $accepted $c_missing; c_result=0; break ;;
|
||
cancel) break ;;
|
||
url:*) open_agreement "${id#url:}" ;;
|
||
esac ;;
|
||
esac
|
||
done
|
||
screen=$c_screen sel=$c_sel top=$c_top message=$c_message
|
||
return "$c_result"
|
||
}
|
||
consent_rows() {
|
||
printf 'note|\n'
|
||
wrap '' "$c_intro"
|
||
[ -z "$c_detail" ] || printf 'note|%s%s%s\n' "$dim" "$c_detail" "$r0"
|
||
printf 'head|READ THE AGREEMENTS\n'
|
||
while IFS='|' read -r cr_id cr_name cr_url; do
|
||
case " $consent_ids " in *" $cr_id "*) ;; *) continue ;; esac
|
||
host_of "$cr_url"; pad "$cr_name" 36
|
||
printf 'item|url:%s|%s||%s%s%s|open in browser\n' "$cr_id" "$padded" "$dim" "$host" "$r0"
|
||
done <<EOF
|
||
$agreements
|
||
EOF
|
||
printf 'note|\nitem|agree|Agree to all|||%s\nitem|cancel|Cancel|||\n' "$c_action"
|
||
}
|
||
|
||
# --------------------------------------------------------------- model ---
|
||
# scan: every row's state, read from the folder (after each action).
|
||
scan() {
|
||
rust_version
|
||
m_rust_ok=0 m_rust_external=
|
||
if [ -n "$rust_version" ] && rust_ready "$rust_version"; then
|
||
m_rust_ok=1
|
||
case "$selected_rust" in /*) m_rust_external=$rust_sysroot ;; esac
|
||
fi
|
||
m_license_rows=
|
||
if [ "$lic_state" = ok ] || [ -z "$email" ]; then sc_ids=$licensed_ids
|
||
else
|
||
sc_ids=
|
||
for sc_file in "$root"/available/*.json; do
|
||
[ -f "$sc_file" ] || continue
|
||
rel_load "$sc_file" 2>/dev/null || continue
|
||
[ "$rel_public" = true ] || sc_ids="$sc_ids $rel_id"
|
||
done
|
||
fi
|
||
for sc_id in $sc_ids; do
|
||
app_state "$sc_id"
|
||
rel_load "$root/available/$sc_id.json" 2>/dev/null || continue
|
||
state_texts "$state" "$rel_bytes"
|
||
if ! rel_supported; then st_text="${dim}not available for this platform yet${r0}" st_act=; fi
|
||
if is_stopped "$sc_id"; then st_text="${warn}stopped · continue${r0}" st_act='compile and run'; fi
|
||
app_info "$sc_id" "$state"
|
||
sc_lic=commercial; [ "$rel_license" != beta ] || sc_lic=beta
|
||
m_license_rows="${m_license_rows}item|app:$sc_id|$rel_title|$sc_lic|$st_text|$st_act|$ai
|
||
"
|
||
done
|
||
m_free_rows= m_free_total=0 m_free_ready=0 m_free_todo=
|
||
sc_free=$(free_apps)
|
||
while IFS='|' read -r sc_id sc_title; do
|
||
[ -n "$sc_id" ] || continue
|
||
app_state "$sc_id"
|
||
m_free_total=$((m_free_total + 1))
|
||
# One shared Makepad download: rows show only readiness; the action
|
||
# appears on the selected row.
|
||
case "$state" in
|
||
ready) m_free_ready=$((m_free_ready + 1)); sc_st="${ok}✓${r0} ready" sc_act=run ;;
|
||
compile) sc_st='needs compiling' sc_act=compile ;;
|
||
merge) sc_st="${warn}updated · your changes to merge${r0}" sc_act='merge with agent' ;;
|
||
*) sc_st= sc_act=download ;;
|
||
esac
|
||
if is_stopped "$sc_id"; then sc_st="${warn}stopped · continue${r0}" sc_act='compile and run'; fi
|
||
# What Compile all queues: every one that is not ready.
|
||
case "$state" in
|
||
ready | merge) if is_stopped "$sc_id"; then m_free_todo="$m_free_todo$sc_id|$sc_title
|
||
"; fi ;;
|
||
*) m_free_todo="$m_free_todo$sc_id|$sc_title
|
||
" ;;
|
||
esac
|
||
app_info "$sc_id" "$state"
|
||
m_free_rows="${m_free_rows}sub|app:$sc_id|$sc_title||$sc_st|$sc_act|$ai
|
||
"
|
||
done <<EOF
|
||
$sc_free
|
||
EOF
|
||
m_public_ready=0
|
||
if rel_load "$root/available/makepad.json" 2>/dev/null; then
|
||
rel_dir; m_public_mb=$(( (rel_bytes + 1048575) / 1048576 ))
|
||
! rel_installed || m_public_ready=1
|
||
fi
|
||
}
|
||
# The status column says where an app is, in a word or two; the action on
|
||
# the selected row says what Return does (each one ends with the app
|
||
# running).
|
||
state_texts() { # state_texts STATE BYTES -> st_text st_act (menu.txt)
|
||
st_mb=$(( (${2:-0} + 1048575) / 1048576 ))
|
||
case "$1" in
|
||
new) if [ "$st_mb" -gt 0 ]; then st_text="${dim}$st_mb MB${r0}"; else st_text="${dim}not downloaded${r0}"; fi; st_act=download ;;
|
||
partial) st_text="${warn}download stopped${r0}" st_act=resume ;;
|
||
compile) st_text='needs compiling' st_act=compile ;;
|
||
update) st_text="${warn}update available${r0}" st_act=update ;;
|
||
ready) st_text="${ok}✓${r0} ready" st_act=run ;;
|
||
merge) st_text="${warn}updated · your changes to merge${r0}" st_act='merge with agent' ;;
|
||
esac
|
||
}
|
||
main_rows() {
|
||
# Update sits above everything; the menu still opens on the first app.
|
||
printf 'note|\n'
|
||
case "$checked" in
|
||
'') printf 'item|updates|Update||%scheck for updates%s|=\n' "$dim" "$r0" ;;
|
||
0*) printf 'item|updates|Update||%s✓%s up to date · %s|check again\n' "$ok" "$r0" "${checked#* }" ;;
|
||
*) printf 'item|updates|Update||updates downloaded · %s|check again\n' "${checked#* }" ;;
|
||
esac
|
||
printf 'head|YOUR APPS\n'
|
||
if [ -z "$email" ]; then printf 'item|login|Log in with your email address|||to see your licenses\n'
|
||
else
|
||
case "$lic_state" in
|
||
checking) printf 'note|%schecking licenses for %s…%s\n' "$dim" "$email" "$r0" ;;
|
||
ok) if [ -n "$m_license_rows" ]; then printf '%s' "$m_license_rows"; else printf 'note|%snone on this email yet · buy or request beta access at makepad.nl%s\n' "$dim" "$r0"; fi ;;
|
||
*) printf 'note|%slicenses not checked: %s%s\n' "$warn" "$lic_error" "$r0"; printf '%s' "$m_license_rows" ;;
|
||
esac
|
||
fi
|
||
printf 'head|MAKEPAD EXPERIMENTS\n'
|
||
printf 'item|free|Experiments|free|%s · %s ready|open list\n' "$m_free_total" "$m_free_ready"
|
||
printf 'head|CODING AGENTS\n'
|
||
[ -z "$agents" ] || printf '%s\n' "$agents" | while IFS='|' read -r mr_cmd mr_title; do printf 'item|agent-%s|%s|||open\n' "$mr_cmd" "$mr_title"; done
|
||
printf "item|agent-shell|Shell||%swith this folder's Rust on PATH%s|open\n" "$dim" "$r0"
|
||
printf 'head|SETUP\n'
|
||
if [ -z "$email" ]; then printf 'item|account|Account||%snot logged in%s|log in\n' "$warn" "$r0"
|
||
else
|
||
case "$lic_state" in
|
||
ok) printf 'item|account|Account||%s✓%s %s|switch or log out\n' "$ok" "$r0" "$email" ;;
|
||
checking) printf 'item|account|Account||%s %schecking…%s|switch or log out\n' "$email" "$dim" "$r0" ;;
|
||
*) printf 'item|account|Account||%s %s· %s%s|switch or log out\n' "$email" "$warn" "$lic_error" "$r0" ;;
|
||
esac
|
||
fi
|
||
if [ "$plat" = linux ]; then
|
||
if [ -f "$root/graphics-notice-read" ]; then printf 'item|gpu|Graphics||%s✓%s driver notice read|\n' "$ok" "$r0"
|
||
else printf 'item|gpu|Graphics||%sread the driver notice%s|read\n' "$warn" "$r0"; fi
|
||
fi
|
||
accepted_agreements
|
||
if is_accepted makepad && is_accepted rust; then printf 'item|terms|Agreements||%s✓%s accepted %s· Makepad, Rust%s|read\n' "$ok" "$r0" "$dim" "$r0"
|
||
else printf 'item|terms|Agreements||%snot accepted%s|read\n' "$warn" "$r0"; fi
|
||
if [ -f "$scratch/disk" ]; then
|
||
read -r mr_total mr_build < "$scratch/disk"
|
||
mr_gb=$(awk -v t="$mr_total" -v b="$mr_build" 'BEGIN { printf "%.1f %.1f", t / 1048576, b / 1048576 }')
|
||
if [ "${mr_build:-0}" -gt 0 ]; then printf 'item|disk|Disk||%s GB used %s· build %s GB%s|clear build data\n' "${mr_gb% *}" "$dim" "${mr_gb#* }" "$r0"
|
||
else printf 'item|disk|Disk||%s GB used|\n' "${mr_gb% *}"; fi
|
||
else
|
||
printf 'item|disk|Disk||%smeasuring…%s|\n' "$dim" "$r0"
|
||
fi
|
||
}
|
||
main_rows() {
|
||
# Update sits above everything; the menu still opens on the first app.
|
||
printf 'note|\n'
|
||
mr_info='Checks your licenses and downloads newer sources of your apps.'
|
||
case "$checked" in
|
||
'') printf 'item|updates|Update||%scheck for updates%s|=|%s\n' "$dim" "$r0" "$mr_info" ;;
|
||
0*) printf 'item|updates|Update||%s✓%s up to date · %s|check again|%s\n' "$ok" "$r0" "${checked#* }" "$mr_info" ;;
|
||
*) printf 'item|updates|Update||updates downloaded · %s|check again|%s\n' "${checked#* }" "$mr_info" ;;
|
||
esac
|
||
printf 'head|YOUR APPS\n'
|
||
if [ -z "$email" ]; then printf 'item|login|Log in with your email address|||to see your licenses\n'
|
||
else
|
||
case "$lic_state" in
|
||
checking) printf 'note|%schecking licenses for %s…%s\n' "$dim" "$email" "$r0" ;;
|
||
ok) if [ -n "$m_license_rows" ]; then printf '%s' "$m_license_rows"; else printf 'note|%snone on this email yet · buy or request beta access at makepad.nl%s\n' "$dim" "$r0"; fi ;;
|
||
*) printf 'note|%slicenses not checked: %s%s\n' "$warn" "$lic_error" "$r0"; printf '%s' "$m_license_rows" ;;
|
||
esac
|
||
fi
|
||
printf 'head|MAKEPAD EXPERIMENTS\n'
|
||
# The Experiments node; open, Compile all and every experiment under it.
|
||
mr_about='Small open-source Makepad apps.'
|
||
[ "$m_public_ready" = 1 ] || mr_about="$mr_about One ${m_public_mb:-0} MB download covers them all."
|
||
mr_about="$mr_about ⏎ opens or closes the list."
|
||
if [ "$free_open" = 1 ]; then
|
||
printf 'item|free|▾ Experiments|free||close|%s\n' "$mr_about"
|
||
queue_batch
|
||
mr_batch=$qb
|
||
[ -z "$b_pid" ] || [ "$b_batch" != 1 ] || [ "$b_open" = 1 ] || mr_batch=$((mr_batch + 1))
|
||
mr_info='Queues every experiment that is not ready; each is compiled, not opened.'
|
||
if [ "$mr_batch" -gt 0 ]; then printf 'sub|all|Compile all||%s%s queued%s||%s\n' "$dim" "$mr_batch" "$r0" "$mr_info"
|
||
else printf 'sub|all|Compile all||%s%s not ready%s|queue all|%s\n' "$dim" "$((m_free_total - m_free_ready))" "$r0" "$mr_info"; fi
|
||
printf '%s' "$m_free_rows"
|
||
else
|
||
printf 'item|free|▸ Experiments|free|%s%s apps · %s ready%s|open|%s\n' "$dim" "$m_free_total" "$m_free_ready" "$r0" "$mr_about"
|
||
fi
|
||
printf 'head|CODING AGENTS\n'
|
||
[ -z "$agents" ] || printf '%s\n' "$agents" | while IFS='|' read -r mr_cmd mr_title; do printf 'item|agent-%s|%s|||open|Ask it to change an app; it can rebuild them here.\n' "$mr_cmd" "$mr_title"; done
|
||
printf "item|agent-shell|Shell||%swith this folder's Rust on PATH%s|open|A shell in this folder; type exit to come back.\n" "$dim" "$r0"
|
||
printf 'head|SETUP\n'
|
||
mr_info='Log in, switch or log out.'
|
||
if [ -z "$email" ]; then printf 'item|account|Account||%snot logged in%s|log in|%s\n' "$warn" "$r0" "$mr_info"
|
||
else
|
||
case "$lic_state" in
|
||
ok) printf 'item|account|Account||%s✓%s %s|switch or log out|%s\n' "$ok" "$r0" "$email" "$mr_info" ;;
|
||
checking) printf 'item|account|Account||%s %schecking…%s|switch or log out|%s\n' "$email" "$dim" "$r0" "$mr_info" ;;
|
||
*) printf 'item|account|Account||%s %s· %s%s|switch or log out|%s\n' "$email" "$warn" "$lic_error" "$r0" "$mr_info" ;;
|
||
esac
|
||
fi
|
||
if [ "$plat" = linux ]; then
|
||
if [ -f "$root/graphics-notice-read" ]; then printf 'item|gpu|Graphics||%s✓%s driver notice read||The GPU driver notice.\n' "$ok" "$r0"
|
||
else printf 'item|gpu|Graphics||%sread the driver notice%s|read|The GPU driver notice.\n' "$warn" "$r0"; fi
|
||
fi
|
||
accepted_agreements
|
||
mr_info='The licenses you accepted; ⏎ opens them.'
|
||
if is_accepted makepad && is_accepted rust; then printf 'item|terms|Agreements||%s✓%s accepted %s· Makepad, Rust%s|read|%s\n' "$ok" "$r0" "$dim" "$r0" "$mr_info"
|
||
else printf 'item|terms|Agreements||%snot accepted%s|read|%s\n' "$warn" "$r0" "$mr_info"; fi
|
||
mr_info='Clearing build data keeps your apps; the next compile starts from scratch.'
|
||
if [ -f "$scratch/disk" ]; then
|
||
read -r mr_total mr_build < "$scratch/disk"
|
||
mr_gb=$(awk -v t="$mr_total" -v b="$mr_build" 'BEGIN { printf "%.1f %.1f", t / 1048576, b / 1048576 }')
|
||
if [ "${mr_build:-0}" -gt 0 ]; then printf 'item|disk|Disk||%s GB used %s· build %s GB%s|clear build data|%s\n' "${mr_gb% *}" "$dim" "${mr_gb#* }" "$r0" "$mr_info"
|
||
else printf 'item|disk|Disk||%s GB used||%s\n' "${mr_gb% *}" "$mr_info"; fi
|
||
else
|
||
printf 'item|disk|Disk||%smeasuring…%s||%s\n' "$dim" "$r0" "$mr_info"
|
||
fi
|
||
}
|
||
# app_info ID STATE -> ai: what Return does on an app's row.
|
||
app_info() {
|
||
if is_stopped "$1"; then ai='Stopped; what was downloaded and compiled is kept. ⏎ continues.'; return 0; fi
|
||
case "$2" in
|
||
compile) ai='Compiles it from source and opens it.' ;;
|
||
update) ai='Downloads the new source, compiles it and opens it.' ;;
|
||
ready) ai='Built; ⏎ opens it.' ;;
|
||
merge) ai='Updated; a coding agent reapplies your saved changes.' ;;
|
||
*) ai='Downloads its source, compiles it and opens it.' ;;
|
||
esac
|
||
}
|
||
# The index of the item row with ID on the current screen.
|
||
select_id() {
|
||
si_n=$(screen_rows | grep -E '^(item|sub)\|' | grep -nE "^(item|sub)\|$1\|" | head -n 1 | cut -d: -f1)
|
||
[ -z "$si_n" ] || sel=$((si_n - 1))
|
||
}
|
||
# The menu starts on the first row of YOUR APPS. awk reads all of main_rows'
|
||
# output (a reader that stops early would break its pipe).
|
||
first_license_row() {
|
||
sel=$(main_rows | awk '/^head\|YOUR APPS/ { found = 1 } !found && /^(item|sub)\|/ { n++ } END { print n + 0 }')
|
||
}
|
||
measure_disk() {
|
||
rm -f "$scratch/disk"
|
||
(
|
||
md_total=$(du -sk "$root" 2>/dev/null | cut -f1)
|
||
md_build=0
|
||
[ ! -d "$root/target" ] || md_build=$(du -sk "$root/target" 2>/dev/null | cut -f1)
|
||
put "$scratch/disk" "${md_total:-0} ${md_build:-0}"
|
||
) </dev/null >/dev/null 2>&1 &
|
||
}
|
||
|
||
# -------------------------------------------------------------- account ---
|
||
# Checked on its own page or row: a refused email comes straight back to the
|
||
# editor with the reason; a network failure keeps the email.
|
||
licenses_now() {
|
||
lic_state=checking
|
||
draw
|
||
busy "Checking licenses for $email"
|
||
check_licenses || :
|
||
message=
|
||
}
|
||
# log_out: forget the email in this folder. Only the Makepad experiments
|
||
# show from then on; licensed apps already built stay on disk untouched and
|
||
# come back when you log in again.
|
||
log_out() {
|
||
la_was=$email
|
||
email= licensed_ids= lic_state=none lic_error=
|
||
save_email
|
||
: > "$root/.login-asked"
|
||
: > "$scratch/licensed"
|
||
scan
|
||
message="${ok}✓${r0} Logged out of $la_was. ${dim}Only the Makepad experiments are shown; log in again from the Account row.${r0}"
|
||
}
|
||
signin_screen() {
|
||
s_screen=${screen:-main}
|
||
screen=signin sel=0 top=0 message=
|
||
si_hint= si_typed=${builder_email:-}
|
||
while :; do
|
||
edit 'Email:' "$si_typed" "$si_hint" 'type your email ⏎ continue' || { email=; break; }
|
||
[ -n "$edited" ] || { email=${saved_email:-}; break; }
|
||
si_typed=$edited
|
||
if ! check_email "$edited"; then si_hint="${warn}That does not look like an email address.${r0}"; continue; fi
|
||
email=$email_ok
|
||
licenses_now
|
||
if [ "$lic_state" = rejected ]; then
|
||
si_hint="${warn}Email not recognised: $si_typed · fix any typo, then ⏎${r0}"
|
||
email=
|
||
continue
|
||
fi
|
||
break
|
||
done
|
||
screen=$s_screen message=
|
||
}
|
||
switch_account() {
|
||
if [ -n "$email" ]; then
|
||
choose "Logged in as $email." '' 'switch email' 'switch email' 'log out' || return 0
|
||
if [ "$chosen" = 'log out' ]; then log_out; return 0; fi
|
||
edit 'Email for your licenses:' '' "${dim}⏎ keeps $email${r0}" '⏎ log in esc back' || return 0
|
||
else
|
||
edit 'Email you bought a Makepad app with:' '' '' '⏎ log in esc back' || return 0
|
||
fi
|
||
[ -n "$edited" ] && [ "$edited" != "$email" ] || return 0
|
||
check_email "$edited" || { message="${warn}That does not look like an email address.${r0}"; return 0; }
|
||
sa_before=$email sa_typed=$edited
|
||
email=$email_ok
|
||
while :; do
|
||
licenses_now
|
||
[ "$lic_state" = rejected ] || break
|
||
edit 'Email for your licenses:' "$sa_typed" "${warn}Email not recognised: $sa_typed · fix any typo, then ⏎${r0}" '⏎ check again esc keep' || {
|
||
email=$sa_before; licenses_now; scan; return 0
|
||
}
|
||
sa_typed=$edited
|
||
check_email "$edited" || continue
|
||
email=$email_ok
|
||
done
|
||
save_email
|
||
: > "$root/.login-asked"
|
||
scan
|
||
license_count
|
||
sa_s=s; [ "$license_count" != 1 ] || sa_s=
|
||
if [ "$lic_state" = ok ]; then message="${ok}✓${r0} Switched to $email · $license_count license$sa_s."
|
||
else message="${warn}Switched to $email; the licenses could not be checked: $lic_error${r0}"; fi
|
||
}
|
||
|
||
# --------------------------------------------------------------- folder ---
|
||
# first_folder: asks where to install, reuses a folder the Builder made, and
|
||
# copies this script there as the `makepad` command.
|
||
folder_screen() {
|
||
screen=folder sel=0 top=0 message=
|
||
fs_default=${MAKEPAD_LOADER_ROOT:-$HOME/makepad-commercial}
|
||
fs_home=$(cd -P -- "$HOME" && pwd -P)
|
||
while :; do
|
||
choose "Install into $(short "$fs_default")?" '' install install 'other folder' quit || exit 0
|
||
fs_folder=$fs_default
|
||
case "$chosen" in
|
||
quit) exit 0 ;;
|
||
'other folder')
|
||
edit 'Folder:' "$(short "$fs_default")" '' '⏎ use this folder esc back' || continue
|
||
fs_folder=$edited ;;
|
||
esac
|
||
case "$fs_folder" in
|
||
'~') fs_folder=$HOME ;;
|
||
'~/'*) fs_folder=$HOME/${fs_folder#'~/'} ;;
|
||
/*) ;;
|
||
*) fs_folder=$(pwd -P)/$fs_folder ;;
|
||
esac
|
||
if mkdir -p -- "$fs_folder" 2>/dev/null && fs_root=$(cd -P -- "$fs_folder" && pwd -P); then
|
||
if [ "$fs_root" = / ] || [ "$fs_root" = "$fs_home" ]; then
|
||
message="${warn}Please choose a folder of its own, such as $(short "$HOME/makepad-commercial").${r0}"
|
||
elif [ -e "$fs_root/builder/makepad-builder.json" ] || [ -e "$fs_root/makepad-builder.json" ] || [ -e "$fs_root/makepad-loader.json" ] || [ -z "$(ls -A "$fs_root")" ]; then
|
||
home=$fs_root root=$fs_root/builder
|
||
migrate_layout
|
||
break
|
||
else
|
||
message="${warn}$(short "$fs_root") already holds other files; please choose an empty or new folder.${r0}"
|
||
fi
|
||
else
|
||
message="${warn}Could not create $(short "$fs_folder"); please choose another folder.${r0}"
|
||
fi
|
||
fs_default=$fs_folder
|
||
done
|
||
message=
|
||
}
|
||
install_self() {
|
||
cp "$self" "$home/makepad.next"
|
||
chmod 755 "$home/makepad.next"
|
||
mv -f "$home/makepad.next" "$home/makepad"
|
||
}
|
||
|
||
# ------------------------------------------------------------- graphics ---
|
||
gpu_screen() { # 0 when read
|
||
g_screen=$screen g_sel=$sel
|
||
screen=gpu sel=0 top=0 message=
|
||
g_ok=1
|
||
while :; do
|
||
draw; key
|
||
case "$key" in
|
||
up) sel=0 ;;
|
||
down) sel=1 ;;
|
||
enter) [ "$sel" != 0 ] || g_ok=0; break ;;
|
||
esc | q) break ;;
|
||
esac
|
||
done
|
||
screen=$g_screen sel=$g_sel
|
||
[ "$g_ok" = 0 ] || return 1
|
||
put "$root/graphics-notice-read" read
|
||
}
|
||
|
||
# ----------------------------------------------------------- build tools ---
|
||
# choose_rust VERSION: the one-time offer of a compatible installed Rust, or
|
||
# repairing a recorded one that stopped validating. Private is the default.
|
||
choose_rust() {
|
||
recorded_rust
|
||
cr_stale=
|
||
case "$selected_rust" in
|
||
private) return 0 ;;
|
||
/*) rust_check --validate "$1" "$selected_rust" && return 0; cr_stale=$selected_rust; log "The selected Rust at $selected_rust cannot be used: $rust_reason" ;;
|
||
esac
|
||
if rust_check --probe "$1"; then
|
||
cr_candidate=$rust_sysroot
|
||
choose "Build with a private Rust $1 in this folder, or your system Rust?" "system: $(short "$cr_candidate") (not modified)" private private system || return 1
|
||
if [ "$chosen" = system ]; then record_rust "$cr_candidate"; log 'Using the installed Rust.'; else record_rust private; log 'Using a private Rust in this folder.'; fi
|
||
elif [ -n "$cr_stale" ]; then
|
||
choose "The selected Rust at $(short "$cr_stale") cannot be used. Switch to a private Rust in this folder?" '' switch switch keep || return 1
|
||
[ "$chosen" = switch ] || { message="${warn}Kept the selected Rust at $(short "$cr_stale"). Make it available again, or select the app again to switch.${r0}"; return 1; }
|
||
record_rust private
|
||
else
|
||
log "Installed Rust not used: $rust_reason"
|
||
record_rust private
|
||
fi
|
||
}
|
||
change_rust() {
|
||
rust_version
|
||
if ! rust_check --probe "$rust_version"; then
|
||
message="${dim}No other compatible installed Rust was found: $rust_reason${r0}"
|
||
return 0
|
||
fi
|
||
cr_candidate=$rust_sysroot
|
||
recorded_rust
|
||
cr_pick=private; case "$selected_rust" in /*) cr_pick=system ;; esac
|
||
choose "Build with a private Rust $rust_version in this folder, or your system Rust?" "system: $(short "$cr_candidate") (not modified)" "$cr_pick" private system || return 0
|
||
if [ "$chosen" = system ]; then record_rust "$cr_candidate"; else record_rust private; fi
|
||
scan
|
||
if [ "$m_rust_ok" = 1 ]; then message="${ok}✓${r0} Rust changed; the next compile uses it."; else install_build_tools || :; fi
|
||
}
|
||
# xcode_screen: Apple's installer or its license prompt, Check again or
|
||
# Cancel. 0 when the tools are ready or Apple's installer was started.
|
||
xcode_screen() {
|
||
x_screen=$screen x_sel=$sel
|
||
screen=xcode sel=0 top=0 message=
|
||
x_result=1
|
||
while :; do
|
||
draw; key; message=
|
||
case "$key" in
|
||
up) sel=$(( (sel + items - 1) % items )) ;;
|
||
down) sel=$(( (sel + 1) % items )) ;;
|
||
esc | q) break ;;
|
||
enter)
|
||
select_row
|
||
case "$id" in
|
||
install) /usr/bin/xcode-select --install >/dev/null 2>&1 || :; x_result=0; xcode_waiting=1; break ;;
|
||
license)
|
||
pause_screen
|
||
printf "\n Apple's Xcode license follows. Type agree at the end to accept it.\n\n"
|
||
sudo xcodebuild -license </dev/tty || :
|
||
resume_screen
|
||
if tools_check; then x_result=0; break; fi
|
||
message="${warn}Still not ready.${r0}" ;;
|
||
check)
|
||
busy 'Checking clang, the macOS SDK, the linker and git'
|
||
if tools_check; then x_result=0; break; fi
|
||
message="${warn}Still not ready.${r0}" ;;
|
||
cancel) break ;;
|
||
esac ;;
|
||
esac
|
||
done
|
||
screen=$x_screen sel=$x_sel
|
||
return "$x_result"
|
||
}
|
||
packages_screen() { # 0 when sudo is ready to install the packages
|
||
p_screen=$screen p_sel=$sel
|
||
screen=packages sel=0 top=0 message=
|
||
p_result=1
|
||
while :; do
|
||
draw; key
|
||
case "$key" in
|
||
up) sel=0 ;;
|
||
down) sel=1 ;;
|
||
enter) [ "$sel" != 0 ] || p_result=0; break ;;
|
||
esc | q) break ;;
|
||
esac
|
||
done
|
||
screen=$p_screen sel=$p_sel
|
||
[ "$p_result" = 0 ] || return 1
|
||
# sudo asks for the password on the real terminal; the packages then
|
||
# install beside the Rust download.
|
||
pause_screen
|
||
printf '\n Makepad installs development packages from %s with:\n\n sudo %s\n\n' "$distro_name" "$packages_cmd"
|
||
if sudo -v </dev/tty; then resume_screen; return 0; fi
|
||
resume_screen
|
||
message="${warn}sudo did not run; nothing was installed.${r0}"
|
||
return 1
|
||
}
|
||
job_xcode() { # waits for Apple's installer, checking every few seconds
|
||
while ! tools_check; do
|
||
step "$1" "now|0|0|none|0|waiting for Apple's installer · esc stops waiting"
|
||
sleep 3
|
||
done
|
||
step "$1" 'done|clang, SDK, git'
|
||
}
|
||
job_packages() {
|
||
step "$1" "now|0|0|none|0|installing with ${packages_cmd%% *}"
|
||
# shellcheck disable=SC2086
|
||
if ! sudo -n $packages_cmd >> "$log_file" 2>&1; then step_fail "$1" 'The package manager failed (see builder.log)'; return 1; fi
|
||
tools_check || { step_fail "$1" 'Some development packages are still missing (see builder.log)'; return 1; }
|
||
step "$1" 'done|compiler, linker, git'
|
||
}
|
||
# install_build_tools: what compiling needs, as the Windows Builder does it:
|
||
# one consent screen, then every missing piece side by side on one page.
|
||
install_build_tools() {
|
||
rust_version
|
||
if [ -z "$rust_version" ]; then
|
||
busy 'Checking the Makepad release'
|
||
fetch_public || { message="${warn}The Makepad release could not be checked: $public_error${r0}"; return 1; }
|
||
rust_version
|
||
fi
|
||
ib_version=$rust_version
|
||
ib_tools=0; tools_check && ib_tools=1
|
||
ib_rust=0; rust_ready "$ib_version" && ib_rust=1
|
||
if [ "$ib_rust" = 0 ]; then
|
||
choose_rust "$ib_version" || { message="${dim}Nothing was installed.${r0}"; return 1; }
|
||
rust_ready "$ib_version" && ib_rust=1
|
||
fi
|
||
[ "$ib_tools" = 0 ] || [ "$ib_rust" = 0 ] || return 0
|
||
if [ "$ib_tools" = 1 ]; then ib_intro='Makepad compiles from source, so it needs a compiler.'
|
||
elif [ "$plat" = mac ]; then ib_intro="Makepad compiles from source, so it needs a compiler. Apple's developer tools come from Apple's installer, which shows its own license."
|
||
else ib_intro="Makepad compiles from source, so it needs a compiler. Development packages come from your distribution's package manager."; fi
|
||
ib_detail=; [ "$ib_rust" = 1 ] || ib_detail="Installed in this folder only: Rust $ib_version"
|
||
ib_action='install Rust'; [ "$ib_tools" = 1 ] || ib_action='install the build tools and Rust'
|
||
[ "$ib_rust" = 0 ] || ib_action='install the build tools'
|
||
consent 'Install build tools' "$ib_intro" "$ib_detail" "$ib_action" makepad rust || { message="${dim}Nothing was installed.${r0}"; return 1; }
|
||
ib_steps= xcode_waiting=
|
||
if [ "$ib_tools" = 0 ]; then
|
||
if [ "$plat" = mac ]; then
|
||
xcode_screen || { message="${dim}Nothing was installed.${r0}"; return 1; }
|
||
[ -z "$xcode_waiting" ] || ib_steps='Xcode tools'
|
||
else
|
||
[ "$tools_problem" != unsupported ] || { message="${warn}The Builder needs glibc Linux (x86_64 or ARM64).${r0}"; return 1; }
|
||
linux_packages || { message="${warn}Install the development packages with your package manager, then choose System packages.${r0}"; return 1; }
|
||
packages_screen || { message="${message:-${dim}Nothing was installed.${r0}}"; return 1; }
|
||
ib_steps='System packages'
|
||
fi
|
||
fi
|
||
[ "$ib_rust" = 1 ] || ib_steps="$ib_steps${ib_steps:+|}Rust"
|
||
if [ -n "$ib_steps" ]; then
|
||
IFS='|'; set -- $ib_steps; IFS=$ifs_default
|
||
work_begin 'Install build tools' 'This takes a few minutes; everything goes into this folder.' "$@"
|
||
for ib_step in "$@"; do
|
||
case "$ib_step" in
|
||
'Xcode tools') job_start job_xcode 'Xcode tools'; xcode_job=${running_jobs##* } ;;
|
||
'System packages') job_start job_packages 'System packages' ;;
|
||
Rust) job_start job_rust Rust "$ib_version" ;;
|
||
esac
|
||
done
|
||
if jobs_wait; then
|
||
work_end ok "${ok}✓${r0} Rust $ib_version is ready. Caches and builds stay in $(short "$root")."
|
||
else
|
||
work_failure
|
||
work_end failed "${warn}${wf:-Setup stopped.}${r0}"
|
||
scan; tools_check || :
|
||
return 1
|
||
fi
|
||
fi
|
||
tools_check || :
|
||
measure_disk
|
||
scan
|
||
}
|
||
|
||
# ------------------------------------------------------------------ apps ---
|
||
# ------------------------------------------------------ background work ---
|
||
# An app downloads and compiles in the background, on its own row, while
|
||
# the menu stays usable: one at a time, the others queued ("app|open|batch|
|
||
# title" lines; open: it opens when done, set by Return on its row and
|
||
# turned off by s; batch: Compile all queued it). c cancels: the build's
|
||
# process tree stops, what it downloaded and compiled stays, and the row
|
||
# says "stopped · continue". The update check runs beside it.
|
||
b_pid= b_app= b_title= b_open=0 b_batch=0 b_stopping=0 b_starting=
|
||
u_pid= queue= stopped_apps= free_open=0
|
||
is_stopped() { case " $stopped_apps " in *" $1 "*) return 0 ;; esac; return 1; }
|
||
# queue_place APP -> qp (0 is next) and qp_open; fails when not queued.
|
||
queue_place() {
|
||
qp=0
|
||
while IFS='|' read -r qp_app qp_open qp_batch qp_title; do
|
||
[ -n "$qp_app" ] || continue
|
||
[ "$qp_app" != "$1" ] || return 0
|
||
qp=$((qp + 1))
|
||
done <<EOF
|
||
$queue
|
||
EOF
|
||
return 1
|
||
}
|
||
# queue_batch -> qb: Compile all's apps that still wait to compile only.
|
||
queue_batch() {
|
||
qb=0
|
||
while IFS='|' read -r qb_app qb_open qb_batch qb_title; do
|
||
[ "$qb_batch|$qb_open" != '1|0' ] || qb=$((qb + 1))
|
||
done <<EOF
|
||
$queue
|
||
EOF
|
||
}
|
||
# queue_edit APP open|toggle|remove: set its open flag (and it is no longer
|
||
# part of Compile all's batch), flip it, or take it out.
|
||
queue_edit() {
|
||
qe_new=
|
||
while IFS='|' read -r qe_app qe_open qe_batch qe_title; do
|
||
[ -n "$qe_app" ] || continue
|
||
if [ "$qe_app" = "$1" ]; then
|
||
case "$2" in
|
||
open) qe_open=1 qe_batch=0 ;;
|
||
toggle) qe_open=$((1 - qe_open)) ;;
|
||
remove) qe_removed=$qe_title; continue ;;
|
||
esac
|
||
fi
|
||
qe_new="$qe_new$qe_app|$qe_open|$qe_batch|$qe_title
|
||
"
|
||
done <<EOF
|
||
$queue
|
||
EOF
|
||
queue=$qe_new
|
||
}
|
||
app_title() { # app_title ID -> at
|
||
at=$(free_apps | awk -F'|' -v id="$1" '$1 == id { print $2 }')
|
||
[ -n "$at" ] || { rel_load "$root/available/$1.json" 2>/dev/null && at=$rel_title; } || at=$1
|
||
}
|
||
|
||
# open_app ID: merge saved changes, run a built app, or download, compile
|
||
# and open it in the background. Return on a row that is compiling or
|
||
# queued asks it to open when it is done.
|
||
open_app() {
|
||
oa_id=$1
|
||
if [ -n "$b_pid" ] && [ "$b_app" = "$oa_id" ]; then b_open=1; return 0; fi
|
||
if queue_place "$oa_id"; then queue_edit "$oa_id" open; return 0; fi
|
||
app_state "$oa_id"
|
||
if [ "$state" = merge ]; then merge_changes "$oa_id"; return; fi
|
||
if [ "$state" = ready ] && ! is_stopped "$oa_id"; then
|
||
rel_load "$root/installed/$oa_id.json" && rel_dir
|
||
busy "Opening $rel_title"
|
||
launch_app
|
||
launched_message "$rel_title"
|
||
return
|
||
fi
|
||
# One compiles at a time; the others wait their turn.
|
||
if [ -n "$b_pid" ]; then
|
||
app_title "$oa_id"
|
||
queue="$queue$oa_id|1|0|$at
|
||
"
|
||
return 0
|
||
fi
|
||
start_build "$oa_id" 1 0
|
||
}
|
||
# compile_all: every experiment that is not ready joins the queue, to be
|
||
# compiled only (Return on one of them makes it open when done).
|
||
compile_all() {
|
||
ca_count=0
|
||
while IFS='|' read -r ca_id ca_title; do
|
||
[ -n "$ca_id" ] || continue
|
||
[ "$ca_id" != "$b_app" ] || [ -z "$b_pid" ] || continue
|
||
! queue_place "$ca_id" || continue
|
||
queue="$queue$ca_id|0|1|$ca_title
|
||
"
|
||
ca_count=$((ca_count + 1))
|
||
done <<EOF
|
||
$m_free_todo
|
||
EOF
|
||
if [ "$ca_count" = 0 ]; then message="${dim}Every experiment is ready or already queued.${r0}"; return 0; fi
|
||
if [ "$ca_count" = 1 ]; then message="${dim}Queued 1 experiment; each is compiled, not opened.${r0}"
|
||
else message="${dim}Queued $ca_count experiments; each is compiled, not opened.${r0}"; fi
|
||
}
|
||
# background_key c|s ID: c cancels the selected row's build or its place in
|
||
# the queue (on Compile all, its queued apps); s flips whether it opens.
|
||
background_key() {
|
||
bk_app=${2#app:}
|
||
case "$1" in
|
||
s)
|
||
if [ -n "$b_pid" ] && [ "$b_app" = "$bk_app" ] && [ "$b_stopping" = 0 ]; then b_open=$((1 - b_open))
|
||
elif queue_place "$bk_app"; then queue_edit "$bk_app" toggle; fi ;;
|
||
c)
|
||
if [ "$2" = all ]; then
|
||
bk_new= bk_gone=0
|
||
while IFS='|' read -r bk_q bk_open bk_batch bk_title; do
|
||
[ -n "$bk_q" ] || continue
|
||
if [ "$bk_batch|$bk_open" = '1|0' ]; then bk_gone=$((bk_gone + 1)); continue; fi
|
||
bk_new="$bk_new$bk_q|$bk_open|$bk_batch|$bk_title
|
||
"
|
||
done <<EOF
|
||
$queue
|
||
EOF
|
||
queue=$bk_new
|
||
if [ "$bk_gone" = 1 ]; then message="${dim}1 experiment is out of the queue.${r0}"
|
||
elif [ "$bk_gone" -gt 1 ]; then message="${dim}$bk_gone experiments are out of the queue.${r0}"; fi
|
||
elif [ -n "$b_pid" ] && [ "$b_app" = "$bk_app" ] && [ "$b_stopping" = 0 ]; then
|
||
# Its downloads and Cargo stop; the next queued app starts
|
||
# once they have.
|
||
b_stopping=1
|
||
kill_tree "$b_pid"
|
||
log "$b_title cancelled."
|
||
message="${warn}$b_title cancelled. What was downloaded and compiled is kept; select it to continue.${r0}"
|
||
elif queue_place "$bk_app"; then
|
||
queue_edit "$bk_app" remove
|
||
message="${dim}$qe_removed is out of the queue.${r0}"
|
||
fi ;;
|
||
esac
|
||
}
|
||
# start_build ID OPEN BATCH: first, here, what may need the person (the
|
||
# build tools with their consent screens, the release, the email); then the
|
||
# download and the compile as a background job.
|
||
start_build() {
|
||
sb_app=$1
|
||
stopped_apps=$(printf '%s\n' $stopped_apps | grep -vx "$sb_app" | tr '\n' ' ' || :)
|
||
install_build_tools || return 0
|
||
app_release_file "$sb_app" || release_file="$root/available/makepad.json"
|
||
rel_load "$release_file" || { message="${warn}$sb_app has no usable release; choose Update.${r0}"; return 0; }
|
||
rel_supported || { message="${warn}$rel_title is not available for this platform yet.${r0}"; return 0; }
|
||
if [ "$rel_public" != true ]; then
|
||
[ -n "$email" ] || { message="${warn}Log in with the email that has this app's license.${r0}"; return 0; }
|
||
email_headers
|
||
fi
|
||
sb_title=$rel_title
|
||
[ "$rel_id" != makepad ] || sb_title=$(free_apps | awk -F'|' -v id="$sb_app" '$1 == id { print $2 }')
|
||
rust_ready "$rel_rust" || { message="${warn}The latest source requires Rust $rel_rust; it could not be set up.${r0}"; return 0; }
|
||
source_labels
|
||
if [ -n "$source_labels" ]; then b_starting=downloading; else b_starting=compiling; fi
|
||
# A stale checkout lock (a Builder that was closed mid-download) goes
|
||
# when nothing else runs.
|
||
[ -n "$u_pid" ] || rm -rf "$root/tmp/checkout.lock"
|
||
rm -f "$steps_dir/_build"
|
||
log "$sb_title downloads and compiles in the background."
|
||
( build_job "$sb_app" ) </dev/null >/dev/null 2>>"$log_file" &
|
||
b_pid=$! b_app=$sb_app b_title=$sb_title b_open=$2 b_batch=$3 b_stopping=0
|
||
}
|
||
# checkout_wait STEP: download and check out the loaded release's missing
|
||
# repositories (the job_pack jobs), one checkout at a time in the folder
|
||
# (a build and an update may both want the same snapshot), with all of them
|
||
# as one bar on STEP.
|
||
checkout_wait() {
|
||
mkdir -p "$root/tmp"
|
||
until mkdir "$root/tmp/checkout.lock" 2>/dev/null; do
|
||
step "$1" 'now|0|0|MB|0|waiting'
|
||
sleep 0.3
|
||
done
|
||
rel_dir
|
||
cw_list=
|
||
while IFS='|' read -r v_name v_path v_commit v_sha v_bytes; do
|
||
[ -n "$v_name" ] || continue
|
||
repo_installed "$rel_directory" "$v_name" "$v_path" "$v_commit" && continue
|
||
cw_title=$(printf '%s' "$v_name" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }')
|
||
step_key "$cw_title source"
|
||
rm -f "$steps_dir/$step_key"
|
||
cw_list="$cw_list$step_key|$v_bytes
|
||
"
|
||
done <<EOF
|
||
$rel_repos
|
||
EOF
|
||
running_jobs=
|
||
checkout_jobs
|
||
cw_status=0
|
||
while :; do
|
||
cw_alive=
|
||
for cw_pid in $running_jobs; do kill -0 "$cw_pid" 2>/dev/null && cw_alive=1; done
|
||
cw_done=0 cw_total=0 cw_doing=unpacking cw_failed=
|
||
while IFS='|' read -r cw_key cw_bytes; do
|
||
[ -n "$cw_key" ] || continue
|
||
cw_state= cw_a=0
|
||
IFS='|' read -r cw_state cw_a cw_b cw_unit cw_mb cw_now 2>/dev/null < "$steps_dir/$cw_key" || :
|
||
cw_total=$((cw_total + 2 * cw_bytes))
|
||
case "$cw_state" in
|
||
now) cw_done=$((cw_done + cw_a)); [ "$cw_now" != downloading ] || cw_doing=downloading ;;
|
||
done) cw_done=$((cw_done + 2 * cw_bytes)) ;;
|
||
failed | held) cw_failed=${cw_failed:-$cw_a} ;;
|
||
esac
|
||
done <<EOF
|
||
$cw_list
|
||
EOF
|
||
step "$1" "now|$cw_done|$cw_total|MB|$((cw_total / 2))|$cw_doing"
|
||
[ -n "$cw_alive" ] || break
|
||
sleep 0.2
|
||
done
|
||
for cw_pid in $running_jobs; do wait "$cw_pid" || cw_status=1; done
|
||
running_jobs=
|
||
rmdir "$root/tmp/checkout.lock" 2>/dev/null || :
|
||
if [ -n "$cw_failed" ] || [ "$cw_status" != 0 ]; then
|
||
step_fail "$1" "${cw_failed:-The download stopped (see builder.log).}"
|
||
return 1
|
||
fi
|
||
}
|
||
# build_job ID (background): the loaded release's source when it is not
|
||
# here yet, then Cargo, with its progress on the step _build.
|
||
build_job() {
|
||
bj_app=$1
|
||
log "$(describe_sources)"
|
||
keep_edits "$bj_app" "$release_file"
|
||
rel_load "$release_file"
|
||
checkout_wait _build || exit 1
|
||
# A free app's package comes from the registry of the source it builds.
|
||
if [ "$rel_id" = makepad ]; then
|
||
load_registry
|
||
app_release_file "$bj_app" || { step_fail _build "$bj_app is not in this Makepad release"; exit 1; }
|
||
rel_load "$release_file"
|
||
fi
|
||
if [ "$release_file" = "$root/tmp/projected-$bj_app.json" ]; then
|
||
mkdir -p "$root/available"; cp "$release_file" "$root/available/$bj_app.json"; release_file="$root/available/$bj_app.json"
|
||
fi
|
||
rel_dir
|
||
prepare_target
|
||
job_compile _build "$bj_app" || exit 1
|
||
prune_snapshots || :
|
||
}
|
||
# finish_build: the background build ended; open the app when it was asked
|
||
# to, say so, and keep a cancelled one's row at "stopped · continue".
|
||
finish_build() {
|
||
wait "$b_pid" 2>/dev/null || :
|
||
fb_state= fb_reason=
|
||
IFS='|' read -r fb_state fb_reason fb_rest 2>/dev/null < "$steps_dir/_build" || :
|
||
fb_app=$b_app fb_title=$b_title fb_open=$b_open fb_stopping=$b_stopping
|
||
b_pid= b_app= b_title= b_open=0 b_batch=0 b_stopping=0
|
||
measure_disk
|
||
if [ "$fb_stopping" = 1 ]; then
|
||
stopped_apps="$stopped_apps $fb_app"
|
||
log "$fb_title stopped; its downloads and compiled crates are kept."
|
||
elif [ "$fb_state" != done ]; then
|
||
message="${warn}$fb_title: ${fb_reason:-it stopped unexpectedly (see builder.log)}${r0}"
|
||
else
|
||
# Just downloaded and built: that is up to date, unless an update
|
||
# check meanwhile downloaded newer sources.
|
||
case "$checked" in 1*) ;; *) checked="0 $(date +%H:%M)" ;; esac
|
||
if [ "$fb_open" = 1 ]; then
|
||
rel_load "$root/installed/$fb_app.json" && rel_dir
|
||
launch_app
|
||
launched_message "$rel_title"
|
||
if [ -f "$root/changes/$fb_app.merge" ]; then message="${warn}$rel_title is updated. Your edits are in changes/; select it to merge.${r0}"; fi
|
||
else
|
||
message="${ok}✓${r0} $fb_title is compiled."
|
||
fi
|
||
fi
|
||
scan
|
||
}
|
||
# service_background: work that ended reports on the status line, then the
|
||
# next queued app starts (its preparation may ask something on the way).
|
||
service_background() {
|
||
if [ -n "$u_pid" ] && ! kill -0 "$u_pid" 2>/dev/null; then finish_update; fi
|
||
if [ -n "$b_pid" ] && ! kill -0 "$b_pid" 2>/dev/null; then finish_build; fi
|
||
while [ -z "$b_pid" ] && [ -n "$queue" ]; do
|
||
IFS='|' read -r sv_app sv_open sv_batch sv_title <<EOF
|
||
$queue
|
||
EOF
|
||
queue=${queue#*"$nl"}
|
||
start_build "$sv_app" "$sv_open" "$sv_batch"
|
||
done
|
||
}
|
||
# One line for the log saying where the sources come from, so a full
|
||
# recompile can be told apart from a shared build.
|
||
describe_sources() {
|
||
rel_dir
|
||
if [ "$rel_directory" = "$root/sources/$rel_release" ]; then printf 'Sources for %s: %s' "$rel_title" "$rel_directory"
|
||
else printf 'Sources for %s shared with %s (same commits, same build artifacts)' "$rel_title" "$rel_directory"; fi
|
||
}
|
||
|
||
merge_changes() { # merge_changes APP
|
||
mc_app=$1
|
||
mc_diff=$(cat "$root/changes/$mc_app.merge" 2>/dev/null || :)
|
||
mc_agents=$(printf '%s\n' "$agents" | grep -v '^grok|' | grep . || :)
|
||
mc_count=$(printf '%s\n' "$mc_agents" | grep -c . || :)
|
||
case "$mc_count" in
|
||
0) message="${warn}Install Claude Code or Codex to merge, or apply changes/$mc_diff yourself.${r0}"; return 0 ;;
|
||
1) mc_agent=${mc_agents%%|*} ;;
|
||
*)
|
||
set -- $(printf '%s\n' "$mc_agents" | cut -d'|' -f1)
|
||
choose 'Merge your changes with which agent?' '' "$1" "$@" || return 0
|
||
mc_agent=$chosen ;;
|
||
esac
|
||
mc_title=$(printf '%s\n' "$agents" | awk -F'|' -v id="$mc_agent" '$1 == id { print $2 }')
|
||
launch_agent "$mc_agent" "Reapply my changes saved in changes/$mc_diff in this folder onto the new $mc_app source. Keep the intent of each change where the new release moved code, rebuild it with ./makepad build $mc_app until it compiles, and report any change that no longer applies." || return 0
|
||
rm -f "$root/changes/$mc_app.merge"
|
||
message="${ok}✓${r0} $mc_title finished merging. Select the app to run it."
|
||
scan
|
||
}
|
||
# real_tty: the terminal's own device (/dev/ttys003), for programs started in
|
||
# it. Handed /dev/tty, Claude Code (Bun) stops at once on macOS with "EINVAL:
|
||
# invalid argument, kqueue", since kqueue cannot watch that alias.
|
||
real_tty() {
|
||
rt_name=$(ps -o tty= -p $$ 2>/dev/null | tr -d ' ')
|
||
case "$rt_name" in
|
||
'' | '?' | '??') real_tty=/dev/tty ;;
|
||
/dev/*) real_tty=$rt_name ;;
|
||
*) real_tty=/dev/$rt_name ;;
|
||
esac
|
||
[ -c "$real_tty" ] && [ -r "$real_tty" ] && [ -w "$real_tty" ] || real_tty=/dev/tty
|
||
}
|
||
# launch_agent NAME [TASK]: the agent in this folder, knowing how to change
|
||
# and rebuild the apps, with this folder's Rust on PATH.
|
||
launch_agent() {
|
||
agent_release || { message="${warn}Download an app first; coding agents work in its source.${r0}"; return 1; }
|
||
rust_ready "$rel_rust" && tools_check || { message="${warn}Set up the build tools first, so the agent can rebuild apps.${r0}"; return 1; }
|
||
write_agent_files
|
||
la_title=$(printf '%s\n' "$agents" | awk -F'|' -v id="$1" '$1 == id { print $2 }')
|
||
real_tty
|
||
pause_screen
|
||
# The agent gets the terminal as it is, with its own UI. Keys pressed
|
||
# before it is up are dropped, so a second Return cannot answer its first
|
||
# question (Claude Code's "trust this folder?" has "No, exit" selected).
|
||
printf '\033[H\033[2J%s\n\n' "Starting ${la_title:-$1} in $(short "$root")…"
|
||
stty -icanon min 0 time 0 </dev/tty 2>/dev/null && dd bs=4096 count=1 </dev/tty >/dev/null 2>&1 || :
|
||
stty "$tty_saved" <&3 2>/dev/null || :
|
||
la_started=$(now)
|
||
la_status=0
|
||
(
|
||
cargo_env "$rel_label" "$rel_id" >/dev/null
|
||
export MAKEPAD_AGENT_CONTEXT="$root/agent-context.txt"
|
||
cd "$root"
|
||
case "$1" in
|
||
claude) if [ -n "${2:-}" ]; then exec claude --append-system-prompt "$agent_directive" "$2"; else exec claude --append-system-prompt "$agent_directive"; fi ;;
|
||
codex) if [ -n "${2:-}" ]; then exec codex -c "developer_instructions='$agent_directive'" "$2"; else exec codex -c "developer_instructions='$agent_directive'"; fi ;;
|
||
*) exec "$1" ;;
|
||
esac
|
||
) <"$real_tty" >"$real_tty" 2>&1 || la_status=$?
|
||
resume_screen
|
||
# Closed straight away: most often its first question (trust this
|
||
# folder?) was answered No, which also exits 1.
|
||
if [ $(($(now) - la_started)) -lt 8 ]; then
|
||
message="${warn}${la_title:-$1} closed right away.${r0} ${dim}If it asked whether to trust this folder, open it again and choose Yes.${r0}"
|
||
return 1
|
||
fi
|
||
if [ "$la_status" != 0 ]; then message="${warn}${la_title:-$1} exited $la_status. It must already be installed and signed in.${r0}"; return 1; fi
|
||
scan
|
||
}
|
||
open_shell() {
|
||
real_tty
|
||
pause_screen
|
||
printf '\nProject: %s\nThis folder'"'"'s Rust is on PATH. Type exit to return to Makepad.\n\n' "$home"
|
||
(
|
||
if agent_release && rust_ready "$rel_rust"; then
|
||
cargo_env "$rel_label" "$rel_id" >/dev/null
|
||
write_agent_files
|
||
export MAKEPAD_AGENT_CONTEXT="$root/agent-context.txt"
|
||
fi
|
||
cd "$home"
|
||
exec "${SHELL:-/bin/sh}"
|
||
) <"$real_tty" >"$real_tty" 2>&1 || :
|
||
resume_screen
|
||
scan
|
||
}
|
||
|
||
# ---------------------------------------------------------------- update ---
|
||
# Licenses first (new purchases appear, expired ones go), then every
|
||
# installed app is compared with its newest release in the background, on
|
||
# the Update row: unchanged sources are replaced by a clean download;
|
||
# edited ones are first saved as a diff.
|
||
check_updates() {
|
||
[ -z "$u_pid" ] || return 0
|
||
consent Update 'Updates download the newest sources of your apps.' '' 'check for updates' makepad || { message="${dim}Nothing was downloaded.${r0}"; return 0; }
|
||
licenses_now
|
||
[ -n "$b_pid" ] || rm -rf "$root/tmp/checkout.lock"
|
||
rm -f "$steps_dir/_update" "$scratch/update-result"
|
||
( update_job ) </dev/null >/dev/null 2>>"$log_file" &
|
||
u_pid=$!
|
||
}
|
||
update_job() {
|
||
if ! fetch_public; then put "$scratch/update-result" "failed|$public_error"; exit 1; fi
|
||
load_registry
|
||
for cu_file in "$root"/available/*.json "$root"/installed/*.json; do
|
||
[ -f "$cu_file" ] || continue
|
||
cu_id=${cu_file##*/}; cu_id=${cu_id%.json}
|
||
[ "$cu_id" != makepad ] || continue
|
||
is_free "$cu_id" || continue
|
||
project_public "$cu_id" "$root/available/$cu_id.json" || :
|
||
done
|
||
cu_updated= cu_merges= cu_started=
|
||
for cu_file in "$root"/installed/*.json; do
|
||
[ -f "$cu_file" ] || continue
|
||
cu_id=${cu_file##*/}; cu_id=${cu_id%.json}
|
||
rel_load "$cu_file" 2>/dev/null || continue
|
||
cu_have=$rel_release
|
||
rel_load "$root/available/$cu_id.json" 2>/dev/null || continue
|
||
[ "$rel_release" != "$cu_have" ] && rel_supported || continue
|
||
if [ "$rel_public" != true ]; then case " $licensed_ids " in *" $cu_id "*) ;; *) continue ;; esac; fi
|
||
keep_edits "$cu_id" "$root/available/$cu_id.json"
|
||
[ ! -f "$root/changes/$cu_id.merge" ] || cu_merges=1
|
||
rel_load "$root/available/$cu_id.json"
|
||
release_file="$root/available/$cu_id.json"
|
||
[ "$rel_public" = true ] || email_headers
|
||
rel_dir
|
||
cu_key="$rel_directory|$rel_repos"
|
||
case "$cu_started" in *"<$cu_key>"*) cu_updated="$cu_updated${cu_updated:+, }$rel_title"; continue ;; esac
|
||
cu_started="$cu_started<$cu_key>"
|
||
if ! checkout_wait _update; then
|
||
IFS='|' read -r cu_state cu_reason 2>/dev/null < "$steps_dir/_update" || :
|
||
put "$scratch/update-result" "failed|${cu_reason:-The update stopped.}"
|
||
exit 1
|
||
fi
|
||
cu_updated="$cu_updated${cu_updated:+, }$rel_title"
|
||
done
|
||
put "$scratch/update-result" "ok|$cu_updated|$cu_merges"
|
||
}
|
||
finish_update() {
|
||
wait "$u_pid" 2>/dev/null || :
|
||
u_pid=
|
||
fu_state= fu_what= fu_merges=
|
||
IFS='|' read -r fu_state fu_what fu_merges 2>/dev/null < "$scratch/update-result" || :
|
||
measure_disk
|
||
if [ "$fu_state" != ok ]; then message="${warn}${fu_what:-The update stopped (see builder.log).}${r0}"
|
||
elif [ -z "$fu_what" ]; then checked="0 $(date +%H:%M)"; message="${ok}✓${r0} Licenses checked; everything is up to date."
|
||
elif [ -n "$fu_merges" ]; then checked="1 $(date +%H:%M)"; message="${ok}✓${r0} Updated $fu_what. Your edits are saved in changes/; select to merge."
|
||
else checked="1 $(date +%H:%M)"; message="${ok}✓${r0} Updated $fu_what. Each compiles on its next run."; fi
|
||
load_registry
|
||
scan
|
||
}
|
||
clear_build() {
|
||
[ -d "$root/target" ] || { message="${dim}There is no build data to delete.${r0}"; return 0; }
|
||
choose 'Delete the build data in target/?' 'your apps keep working; the next compile starts from scratch' delete keep delete || return 0
|
||
[ "$chosen" = delete ] || return 0
|
||
busy 'Deleting build data'
|
||
if remove_inside "$root/target"; then message="${ok}✓${r0} Build data cleaned. Built apps are unchanged."
|
||
else message="${warn}Could not delete all build data; the activity log says why.${r0}"; fi
|
||
measure_disk
|
||
scan
|
||
}
|
||
|
||
# ---------------------------------------------------------------- start ---
|
||
agents=
|
||
for ag in 'claude|Claude Code' 'codex|Codex' 'grok|Grok'; do
|
||
if command -v "${ag%%|*}" >/dev/null 2>&1; then agents="$agents${agents:+
|
||
}$ag"; fi
|
||
done
|
||
lic_state=none lic_error= licensed_ids= checked= tools_problem= m_rust_ok=0 m_license_rows= m_free_rows= m_free_total=0 m_free_ready=0
|
||
m_free_todo= m_public_ready=0 m_public_mb=0
|
||
screen=main sel=0 top=0
|
||
|
||
# Run again through curl | sh (not from the folder's own `makepad`): an
|
||
# installation already in the default folder is opened as it is, with its
|
||
# saved email, and its `makepad` command becomes this version. Only a folder
|
||
# without one is a first run.
|
||
if [ -z "$root" ]; then
|
||
rr_home=${MAKEPAD_LOADER_ROOT:-$HOME/makepad-commercial}
|
||
if [ -f "$rr_home/builder/makepad-builder.json" ] || [ -f "$rr_home/makepad-builder.json" ] || [ -f "$rr_home/makepad-loader.json" ]; then
|
||
home=$(cd -P -- "$rr_home" && pwd -P) root=$home/builder
|
||
migrate_layout
|
||
install_self
|
||
fi
|
||
fi
|
||
if [ -z "$root" ]; then
|
||
# First run: the email, then the folder, then Rust; each once.
|
||
scratch=$(mktemp -d "${TMPDIR:-/tmp}/makepad-builder.XXXXXX")
|
||
steps_dir="$scratch/steps" slots="$scratch/slots" log_file="$scratch/builder.log"
|
||
mkdir -p "$steps_dir" "$slots"
|
||
saved_email=
|
||
for fr_root in "${MAKEPAD_LOADER_ROOT:-$HOME/makepad-commercial}/builder" "${MAKEPAD_LOADER_ROOT:-$HOME/makepad-commercial}"; do
|
||
[ -f "$fr_root/makepad-builder.json" ] || continue
|
||
root=$fr_root; read_email; saved_email=$email; root=
|
||
break
|
||
done
|
||
email=
|
||
signin_screen
|
||
first_email=$email first_state=$lic_state
|
||
folder_screen
|
||
screen=main
|
||
install_self
|
||
[ -n "$first_email" ] || { read_email; first_email=$email; first_state=; }
|
||
session
|
||
email=$first_email
|
||
save_email
|
||
: > "$root/.login-asked"
|
||
log "Makepad Builder installed in $home"
|
||
else
|
||
session
|
||
if [ -z "$email" ] && [ ! -f "$root/.login-asked" ]; then
|
||
signin_screen
|
||
save_email
|
||
: > "$root/.login-asked"
|
||
fi
|
||
fi
|
||
if [ "$plat" = linux ] && [ ! -f "$root/graphics-notice-read" ]; then
|
||
gpu_screen || { quiet_exit='Setup cancelled. Nothing was downloaded or installed.'; exit 0; }
|
||
fi
|
||
|
||
# Every start refreshes the licenses, then stops at the menu with the first
|
||
# app selected.
|
||
[ -f "$root/available/makepad.json" ] || { busy 'Checking the Makepad release'; fetch_public || log "$public_error"; }
|
||
load_registry
|
||
tools_check || :
|
||
scan
|
||
draw
|
||
if [ -n "$email" ]; then
|
||
licenses_now
|
||
if [ "$lic_state" = rejected ]; then
|
||
sw_before=$email
|
||
switch_typed=$email
|
||
while [ "$lic_state" = rejected ]; do
|
||
edit 'Email for your licenses:' "$switch_typed" "${warn}Email not recognised: $switch_typed · fix any typo, then ⏎${r0}" '⏎ check again esc keep' || break
|
||
switch_typed=$edited
|
||
check_email "$edited" || continue
|
||
email=$email_ok
|
||
licenses_now
|
||
done
|
||
[ "$lic_state" != ok ] || save_email
|
||
fi
|
||
fi
|
||
measure_disk
|
||
scan
|
||
draw
|
||
# Build tools are set up when an app is first built, not at the start.
|
||
# Start on the first licensed app, or on logging in.
|
||
sel=0
|
||
first_license_row
|
||
|
||
while :; do
|
||
# Finished work reports on the status line, and the next queued app
|
||
# starts; the screen redraws a few times a second while work runs.
|
||
service_background
|
||
polling=idle
|
||
if [ ! -f "$scratch/disk" ] || [ -n "$b_pid" ] || [ -n "$u_pid" ]; then polling=1; fi
|
||
if [ "$polling" = idle ] && [ "${key:-}" = none ]; then
|
||
ml_size=$(stty size <&3 2>/dev/null || :)
|
||
[ "$ml_size" = "${ml_drawn:-}" ] || { ml_drawn=$ml_size; draw; }
|
||
else
|
||
ml_drawn=$(stty size <&3 2>/dev/null || :)
|
||
draw
|
||
fi
|
||
key
|
||
[ "$key" = none ] && continue
|
||
message=
|
||
case "$key" in
|
||
up) [ "$items" = 0 ] || sel=$(( (sel + items - 1) % items )) ;;
|
||
down) [ "$items" = 0 ] || sel=$(( (sel + 1) % items )) ;;
|
||
q) exit 0 ;;
|
||
esc) exit 0 ;;
|
||
c | s) select_row; background_key "$key" "$id" ;;
|
||
enter)
|
||
select_row
|
||
case "$screen/$id" in
|
||
main/account | main/login) switch_account ;;
|
||
main/gpu) gpu_screen || : ;;
|
||
main/terms) terms_screen ;;
|
||
main/rust) if [ "$m_rust_ok" = 1 ]; then change_rust; else install_build_tools || :; fi ;;
|
||
main/system)
|
||
busy 'Checking the system developer tools'
|
||
if tools_check; then message="${ok}✓${r0} Compiler, SDK, linker and git are ready."; else install_build_tools || :; fi
|
||
scan ;;
|
||
main/disk) clear_build ;;
|
||
main/updates) check_updates ;;
|
||
main/free) free_open=$((1 - free_open)) ;;
|
||
main/all) compile_all ;;
|
||
main/agent-shell) open_shell ;;
|
||
main/agent-*) launch_agent "${id#agent-}" || : ;;
|
||
main/app:*) open_app "${id#app:}" ;;
|
||
esac ;;
|
||
esac
|
||
done
|
||
MAKEPAD_BUILDER
|
||
chmod 700 "$makepad_dir/makepad"
|
||
if [ "$#" = 0 ]; then
|
||
# The Builder in this terminal (its keys come from the terminal, not
|
||
# from the pipe this script arrived through); it copies itself into
|
||
# the folder you choose.
|
||
exec sh "$makepad_dir/makepad" </dev/null
|
||
fi
|
||
# A command (the Rust Builder's read-only probes): run it and tidy up.
|
||
makepad_status=0
|
||
sh "$makepad_dir/makepad" "$@" || makepad_status=$?
|
||
rm -f "$makepad_dir/makepad"
|
||
rmdir "$makepad_dir" 2>/dev/null || :
|
||
return "$makepad_status"
|
||
}
|
||
|
||
makepad_builder "$@"
|