Repository navigation
Expand file tree
/
Copy pathsetup.sh
More file actions
executable file
·2612 lines (2477 loc) · 122 KB
/
Copy pathsetup.sh
File metadata and controls
executable file
·2612 lines (2477 loc) · 122 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# setup.sh: build and install everything weft-related on the local
# machine.
#
# By default, runs the install pipeline for the everyday loop:
# - CLI (cargo build, copied into the install's bin
# folder; on main, also linked into ~/.local/bin)
# - Daemon (bring the local install up, or refresh it:
# Postgres, the object store, and weft's
# runtime restarted on the new binary)
# - VS Code extension (compile, package .vsix, install into VS Code)
#
# Which install: the branch this checkout is on decides. On `main` it is
# the default install, the one `weft` on your PATH drives. On any other
# branch it is that branch's own install, beside the default one with its
# own database, ports and binaries (in ~/.local/share/weft/installs/<name>),
# so you can build and test a branch without touching main's. A branch
# always builds from source (the published build is main's), and skips
# the VS Code and browser extensions: to try the editor on a branch, run
# scripts/dev-editor.sh. Inside the checkout, `weft` hands over to the
# branch's own binary by itself. The install this run set up is recorded
# in .weft-install, which the other scripts read.
#
# The browser extension is OPT-IN via `--browser` since rebuilding
# it signs Firefox and builds every target, which is heavier than
# most rebuild loops need.
#
# Prebuilt artifacts: on a CLEAN checkout of main at a commit CI has
# already built (the rolling `latest` release), the CLI and runtime binaries
# and the .vsix are downloaded instead of compiled and the shared
# images are pulled from the registry, so a fresh install needs no Rust or Node
# toolchain and takes minutes, not tens of minutes. Any local change
# at all, tracked or not, takes the build path for everything (the
# published artifacts are keyed to one exact commit).
#
# Disk hygiene (every install that touches docker, not just --purge):
# - orphaned anonymous docker volumes are reaped (postgres:18
# declares a VOLUME, so a throwaway test database whose container
# was removed without -v strands one forever; named volumes are
# never touched),
# - a --cli install (the default includes it) bounds the BuildKit
# cache to 20GB (LRU), and one that builds weft from source removes
# from the workspace target/ every compiled piece a newer build of
# the same piece replaced (`weft trim-cargo-target`),
# - the daemon start removes the worker containers and images a weft
# from before workers ran as processes left for this install (a
# local install's workers are programs compiled on this machine),
# - after the daemon refresh, `weft clean --images --build-plane`
# drops the worker programs no install on this machine still uses
# (the standard worker of the weft this update replaced among them),
# the shared build crates no build staged for 30 days, and the
# builder bases and node-test images an older weft left that no
# install records using; and `weft clean --images --all` reclaims
# everything of weft's nothing runs any more (unreferenced workers,
# stale weft-infra-* tags, old runtimes, and what an older weft left:
# standard worker images and BuildKit worker compile caches). KEEP
# set = the dispatcher's referenced set (running projects' worker
# hashes, live tasks', every project's infra image tags, every live
# infra unit's recorded refs), so a running project's current worker
# and a live project's infra images survive the update untouched;
# the user resyncs a project when they want it on the new engine,
# and that project's old worker goes on a LATER install's reclaim
# once nothing points at it.
#
# Component flags pick a subset (multiple combine):
# --cli build CLI only
# --daemon refresh daemon only
# --vscode build/install VS Code extension only
# --browser build browser extension only (signs, zips)
# (e.g. --cli --daemon does both, skips the VS Code extension)
#
# Extension release gesture:
# --bump bump the version of the extensions this run builds
# (the VS Code extension on the default install and
# --vscode; the browser extension with --browser). The
# version is what makes CI publish the pushed build to
# the stores (VS Code Marketplace, Open VSX, Chrome,
# Firefox, Edge), so this is the deliberate "release
# the extension" switch; a plain rebuild never bumps.
#
# Default-on knobs you opt OUT of:
# --no-sign skip the Firefox AMO signing step
# --no-daemon skip the daemon refresh on a default install (it does
# not combine with the component flags, which already
# pick what runs)
#
# Questions:
# --yes answer yes to what an install asks before it wipes
# something (an older weft on this machine, or this
# install's database from before runs were renamed), for
# scripts and machines with no terminal.
#
# Escape hatch:
# --from-source compile the CLI and the VS Code extension locally
# even when a published build matches this checkout
# (for when a published artifact turns out broken).
# The daemon images still pull from the registry when
# their content matches; add --rebuild to rebuild them
# locally too.
#
# Diagnostics:
# --verbose show each step's output as it runs. Without it, a long
# step (a build, an image pull) shows a spinner with the
# last line it printed, and a failed step shows the end
# of its output and the file holding all of it.
# An install writes a START line and a DONE/FAIL line (with the
# section it died in) to ~/.local/share/weft/setup-runs.log. A run
# refused on its arguments records only the FAIL; a --purge deletes
# the journal along with the rest of the directory, so the next
# install starts a fresh one.
#
# Public trigger surface (event triggers delivered BY providers):
# --public-url open a public address for the doors outside callers
# use (/events/..., /signal/..., the file links, live
# routes, the OAuth callback) through an outbound
# tunnel, so provider event pushes reach this local
# install. The management API is never on it.
# Persisted across runs, per install: a branch's
# install opens its own (its own named tunnel when the
# checkout's .env.local names one, else a quick one).
# --no-public-url close it again.
#
# Browser-target flags (default: every browser):
# --chrome Chrome / Brave / Vivaldi / Arc / Edge / Opera unpacked
# --firefox
# --edge
# --opera
# --safari
# (multiple combine)
#
# CLI knobs:
# --rebuild rebuild the runtime image and recompile the standard
# worker program even when nothing they are built from
# has changed. For when either is corrupt or
# hand-modified; a plain run already rebuilds whatever
# actually moved.
# --debug build CLI with the debug profile
# --prefix PATH install CLI binary into PATH/bin (default ~/.local)
#
# Changing a table:
# --migration NAME Write the migration for whatever you changed in a
# CREATE TABLE, then install as usual, which applies it.
# Compares the schema your database has against the one
# your code now declares and writes the difference. By
# itself it writes a DRAFT: gitignored, yours alone, and
# applied to your database like any other migration, so
# nothing you have stored is lost while you are still
# deciding the shape. Ask as often as you change the
# table.
# --release With --migration: write the one released migration,
# the one that goes in the PR, and bring your database
# (this checkout's install's) along: if it ran
# drafts, it is told the release is
# already in it; if it never saw one, the release runs
# on it now. One command either way, nothing is lost.
# --rename SPEC With --migration: a rename, which no diff can tell
# from a drop and an add (the drop throws the rows
# away). SPEC is table.old_column=new_column or
# old_table=new_table; repeat it, in the order the
# renames happen. A migration where something goes
# while something arrives beside it is refused until
# every such pair is named, or --unrelated says they
# are separate things.
#
# Removal:
# --uninstall Remove the default install's user-facing pieces but
# preserve work (a branch's install goes with
# scripts/remove-worktree.sh instead). Stops every
# install's runtime and takes it off the service manager,
# uninstalls the VS Code extension, drops the CLI and
# the runtime binary. PRESERVES: the Postgres container
# and its files, the object-store container + its data
# volume, infra units, docker images, BuildKit cache,
# cargo target/, browser extensions. Reinstall via
# ./setup.sh and your projects + history come back
# instantly.
# --purge TRUE clean slate. Deletes every container weft
# started (infra units, Postgres, the tunnel, and an
# older weft's workers) and their volumes, every
# weft-built docker image (weft-runtime, every weft
# infra image, and an older weft's weft-worker images), the object-store container + data volume +
# seaweedfs image, the BuildKit cache, the workspace
# target/ cargo cache, ~/.local/share/weft (THE
# DATABASE'S FILES under postgres-data/, the install's
# config and keys, prebuilt binaries, and every branch's
# install under installs/), this checkout's
# .weft-install, and the
# untracked extension build artifacts. Also deletes the
# Kubernetes cluster an older weft ran, if this machine
# still has it. The next install pays a full
# cold-rebuild cost. Can combine with --uninstall.
#
# SHARED base images (commonly reused by other
# docker projects on the host) are kept by default.
# Add the matching flag to remove them too:
# --postgres remove postgres:18-alpine (and the
# postgres:18 image --migration pulls)
# --kind remove kindest/node images (an older weft's)
# --debian remove debian:trixie-slim (and the
# debian:bookworm-slim an older weft used)
#
# Examples:
# ./setup.sh # full install
# ./setup.sh --browser # rebuild browser ext only
# ./setup.sh --browser --no-sign # browser ext, skip AMO signing
# ./setup.sh --browser --chrome # browser ext, Chrome only
# ./setup.sh --cli --daemon # CLI + daemon, no extensions
# ./setup.sh --uninstall # remove installed pieces
# ./setup.sh --uninstall --purge # remove + nuke local state
# ./setup.sh --uninstall --purge --postgres --kind --debian
# # purge including shared base images
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# The invocation's args, captured before the parse loop consumes them,
# for the run journal.
orig_args="$*"
# The VS Code extension id (publisher.name from extension-vscode/package.json).
# Single source so install, version-probe, and uninstall can never drift (a
# stale `weavemindai.` typo made --uninstall silently match nothing).
ext_id="weavemind.weft-vscode"
# ---- visual library --------------------------------------------------
#
# Colors + symbols. Disabled when stdout isn't a TTY (CI pipelines,
# `setup.sh > log.txt`) or when NO_COLOR is set, so the output stays
# clean and grep-able.
if [[ -t 1 && "${NO_COLOR:-}" == "" ]]; then
C_RESET=$'\033[0m'
C_DIM=$'\033[2m'
C_BOLD=$'\033[1m'
C_RED=$'\033[31m'
C_GREEN=$'\033[32m'
C_YELLOW=$'\033[33m'
C_BLUE=$'\033[34m'
C_CYAN=$'\033[36m'
HAS_TTY=1
else
C_RESET=""
C_DIM=""
C_BOLD=""
C_RED=""
C_GREEN=""
C_YELLOW=""
C_BLUE=""
C_CYAN=""
HAS_TTY=0
fi
# Symbols: braille spinner frames + status glyphs. Plain ASCII
# fallbacks aren't worth the branching; every modern terminal
# (kitty, alacritty, iTerm2, Windows Terminal, vscode terminal,
# gnome-terminal) renders these correctly.
SP_FRAMES=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
SYM_OK="✓"
SYM_FAIL="✗"
SYM_STEP="▸"
SYM_INFO="ℹ"
SYM_WARN="⚠"
SYM_ARROW="→"
section() {
# Recorded so a failed run's journal line can name where it died.
current_section="$*"
printf '\n%s%s%s %s%s%s\n' "${C_BOLD}" "${C_BLUE}" "▶" "${C_BOLD}" "$*" "${C_RESET}"
}
ok() { printf ' %s%s%s %s\n' "${C_GREEN}" "${SYM_OK}" "${C_RESET}" "$*"; }
fail() { printf ' %s%s%s %s\n' "${C_RED}" "${SYM_FAIL}" "${C_RESET}" "$*" >&2; }
step() { printf ' %s%s%s %s\n' "${C_CYAN}" "${SYM_STEP}" "${C_RESET}" "$*"; }
hint() { printf ' %s%s %s%s\n' "${C_DIM}" "${SYM_INFO}" "$*" "${C_RESET}"; }
warn() { printf ' %s%s%s %s\n' "${C_YELLOW}" "${SYM_WARN}" "${C_RESET}" "$*"; }
# spin_status <logfile> <width>: the last non-empty line the command
# wrote, colours stripped and cut to <width>, for the spinner line.
# Progress bars rewrite one line with \r, so those count as line ends.
spin_status() {
tail -c 4096 "$1" 2>/dev/null \
| tr '\r' '\n' \
| sed -e $'s/\033\\[[0-9;?]*[A-Za-z]//g' -e 's/[[:space:]]\{1,\}/ /g' -e 's/^ //' \
| awk 'NF { last = $0 } END { print last }' \
| cut -c "1-$2"
}
# spin "label" cmd args... -- run cmd with its output written to a log
# file, showing a spinner, the time so far and the last line the command
# wrote, so a long build shows it is moving without flooding the
# terminal. On success the line becomes `✓ label (time)` and the log is
# deleted. On failure it becomes `✗ label`, the log's last 60 lines are
# shown, and the log is kept and named so the whole output can be read.
# --verbose streams the command's output instead. With no terminal, one
# line before and one after.
spin() {
local label="$1"; shift
if [[ $verbose -eq 1 ]]; then
step "${label}"
if "$@"; then
ok "${label}"
return 0
else
local rc=$?
fail "${label}"
return $rc
fi
fi
# One mktemp log per call: a fixed /tmp name would let two concurrent
# runs interleave and delete each other's output (and hand any local
# user a symlink-overwrite seat).
local logfile started rc=0
logfile="$(mktemp -t weft-setup.XXXXXX.log)"
started=$SECONDS
if [[ $HAS_TTY -eq 0 ]]; then
printf ' %s %s\n' "${SYM_STEP}" "${label}"
"$@" >"${logfile}" 2>&1 || rc=$?
else
"$@" >"${logfile}" 2>&1 &
local pid=$! i=0 status="" cols width
cols="$(tput cols 2>/dev/null || echo 80)"
while kill -0 "${pid}" 2>/dev/null; do
local frame="${SP_FRAMES[$((i % ${#SP_FRAMES[@]}))]}"
# The status line is re-read every half second, not every frame:
# tail and sed per frame would cost more than the spinner shows.
if (( i % 6 == 0 )); then
width=$(( cols - ${#label} - 16 ))
status=""
(( width > 10 )) && status="$(spin_status "${logfile}" "${width}")"
fi
printf '\r\033[K %s%s%s %s %s(%ss)%s %s%s%s' \
"${C_CYAN}" "${frame}" "${C_RESET}" "${label}" \
"${C_DIM}" "$((SECONDS - started))" "${C_RESET}" \
"${C_DIM}" "${status}" "${C_RESET}"
sleep 0.08
i=$((i + 1))
done
wait "${pid}" || rc=$?
printf '\r\033[K'
fi
if [[ $rc -eq 0 ]]; then
ok "${label} ${C_DIM}($((SECONDS - started))s)${C_RESET}"
rm -f "${logfile}"
return 0
fi
fail "${label}"
tail -n 60 "${logfile}" >&2 || true
hint "the whole output is in ${logfile}"
return $rc
}
# Portable sha256 (GNU sha256sum on Linux, shasum on macOS), same
# output format either way. `sha256_bin` is the underlying COMMAND for
# the call sites that go through xargs, which cannot invoke a shell
# function; expand it unquoted there so the shasum form keeps its args.
if command -v sha256sum >/dev/null 2>&1; then
sha256_bin="sha256sum"
else
sha256_bin="shasum -a 256"
fi
sha256() {
${sha256_bin} "$@"
}
# weft_image_ids, remove_docker_object, remove_docker_images_by_id and
# what --uninstall / --purge remove, shared with scripts/scrub-old-install.sh.
# shellcheck source=scripts/lib/weft-cleanup.sh
. "${here}/scripts/lib/weft-cleanup.sh"
# with_build_slot (one heavy cargo build at a time on this machine), and
# machine_lock_release_mine for the exit.
# shellcheck source=scripts/lib/machine-lock.sh
. "${here}/scripts/lib/machine-lock.sh"
# weft_which (which install a weft binary acts on from this checkout)
# and weft_record_checkout_install (.weft-install).
# shellcheck source=scripts/lib/checkout-install.sh
. "${here}/scripts/lib/checkout-install.sh"
# weft_vscode_link_borrowers and weft_vscode_compile, shared with
# scripts/dev-editor.sh.
# shellcheck source=scripts/lib/vscode-build.sh
. "${here}/scripts/lib/vscode-build.sh"
# One docker-daemon reachability probe, the same shape everywhere it
# gates a best-effort action. (The --purge refusal keeps its own
# PATH-then-daemon form: it must TELL docker-missing and daemon-down
# apart in its error, so it does not share this shape.)
docker_reachable() {
docker version --format '{{.Server.Version}}' >/dev/null 2>&1
}
# The engine's major version, empty when the daemon is unreachable.
# Gates the one step whose SAFETY depends on the engine generation
# (the anonymous-only scope of `docker volume prune`, Docker >= 23).
docker_server_major() {
# `|| true`: under pipefail an unreachable daemon would otherwise
# fail the pipeline and, through the caller's assignment, the whole
# script under set -e; empty output IS the unreachable answer.
{ docker version --format '{{.Server.Version}}' 2>/dev/null || true; } | cut -d. -f1
}
# Remove a directory tree and report: ok exactly when it is gone,
# warn when something survived. ONE shape for every best-effort tree
# removal, so partial removals never print as success.
remove_dir_reporting() { # path label [ok_note] [fail_note]
local path="$1" label="$2" ok_note="${3:-}" fail_note="${4:-}"
if [[ ! -e "${path}" ]]; then
hint "no ${label} to remove"
return 0
fi
rm -rf "${path}" 2>/dev/null || true
if [[ -e "${path}" ]]; then
warn "could not remove ${label} (permissions?)${fail_note}"
else
ok "removed ${label}${ok_note}"
fi
}
# One `code` call through a live IPC socket, with a deadline.
#
# Every `code` call here is answered by the extension host of the window
# that owns the socket, and that host is somebody else's process: busy
# with its own extensions, or wedged behind a socket a closed terminal
# left connectable. A plain `--list-extensions` on a loaded remote
# window has been measured at over a minute, printing nothing the whole
# time, which reads as a hung install. So no call here is unbounded, and
# the caller decides what a timeout means.
#
# Exit status is `code`'s own, or 124 when the deadline passed.
code_ipc() {
local sock="$1" deadline="$2"
shift 2
VSCODE_IPC_HOOK_CLI="${sock}" with_deadline "${deadline}" code "$@"
}
# Run a command with a deadline in seconds (fractions allowed), in
# plain bash: macOS ships no `timeout`, and asking every Mac to install
# coreutils for four call sites is the wrong trade. Exit status is the
# command's own, or 124 when the deadline passed, the same contract as
# `timeout`, so the callers read it the same way.
with_deadline() {
local secs="$1"
shift
"$@" &
local pid=$!
( sleep "${secs}"; kill "${pid}" 2>/dev/null ) &
local killer=$!
local rc=0
wait "${pid}" || rc=$?
# The killer is still sleeping when the command finished on its own;
# a killer that already fired is gone and the kill is a harmless miss.
kill "${killer}" 2>/dev/null
wait "${killer}" 2>/dev/null || true
# 143 is "killed by SIGTERM", which is what the killer sends.
if [[ $rc -eq 143 ]]; then
return 124
fi
return "${rc}"
}
# Nothing asks the window what it has installed any more: the answer
# costs a round trip to its extension host (over a minute, measured, on
# a loaded remote window) and both callers can get it cheaper. The
# install side reads the stamp it wrote last time; the uninstall side
# just tries the uninstall, which answers the same question by working
# or not.
# Print the path of a LIVE VS Code IPC socket (closed terminals leave dead
# sockets behind, so probe for one that answers). VS Code writes them
# under /run/user/$UID on Linux and under $TMPDIR on macOS, so both are
# searched, newest first. Used by both the install side and the
# uninstall/reinstall side; defined once here so the two can't drift.
# Returns non-zero if no live socket is found.
pick_live_vscode_socket() {
local sock
for sock in $(ls -1t "/run/user/${UID}"/vscode-ipc-*.sock "${TMPDIR:-/tmp}"/vscode-ipc-*.sock 2>/dev/null); do
if command -v socat >/dev/null 2>&1; then
if with_deadline 0.3 socat -u /dev/null UNIX-CONNECT:"${sock}" >/dev/null 2>&1; then
printf '%s' "${sock}"; return 0
fi
elif command -v nc >/dev/null 2>&1; then
if with_deadline 0.3 nc -U -z "${sock}" >/dev/null 2>&1; then
printf '%s' "${sock}"; return 0
fi
else
printf '%s' "${sock}"; return 0
fi
done
return 1
}
# ---- defaults --------------------------------------------------------
profile="release"
prefix="${HOME}/.local"
do_uninstall=0
do_purge=0
write_migration=""
migration_renames=()
do_release=0
rebuild_flag=""
purge_debian=0
purge_kind=0
purge_postgres=0
# Components: 0 = excluded, 1 = included. The default install set
# is CLI + daemon + VS Code extension. The browser extension is
# OPT-IN via --browser since it's heavier (extension store sign,
# full per-target build) and most rebuild loops don't need it.
#
# When the user passes any --<component> we flip every component
# to 0 first so the listed flags act as opt-ins.
build_cli=1
refresh_daemon=1
public_url_flag=""
build_vscode=1
build_browser=0
component_flag_seen=0
no_daemon_seen=0
# Browser-target subset: same logic as components.
target_chrome=1
target_firefox=1
target_edge=1
target_opera=1
target_safari=1
target_flag_seen=0
# The extension versions in package.json are what gate the STORE
# publishes in CI (a pushed commit whose version moved gets submitted
# to the VS Code Marketplace / Open VSX / Chrome / Firefox / Edge), so
# bumping is the deliberate release gesture (--bump), never a side
# effect of a rebuild.
do_bump=0
do_sign=1
# --from-source: refuse the prebuilt CLI/.vsix fast path and compile
# locally (the escape hatch when a published binary is broken).
from_source=0
# --yes: answer yes to every question asked before a wipe.
assume_yes=0
# --verbose: stream every step's output instead of the spinner.
verbose=0
# Called the first time a --<component> flag is seen. Zeroes every
# component so subsequent flags act as opt-ins. No-op after the
# first call.
components_flip() {
if [[ $component_flag_seen -eq 0 ]]; then
build_cli=0
refresh_daemon=0
build_vscode=0
build_browser=0
component_flag_seen=1
fi
}
targets_flip() {
if [[ $target_flag_seen -eq 0 ]]; then
target_chrome=0
target_firefox=0
target_edge=0
target_opera=0
target_safari=0
target_flag_seen=1
fi
}
# ---- argv ------------------------------------------------------------
# Run journal: one line per run at start, one at exit, in
# ~/.local/share/weft/setup-runs.log. A run that dies mid-pipeline
# (set -e) scrolls its failure past and leaves the machine half
# installed with no trace; the journal makes "what did my last install
# actually do, and did it finish" a lookup instead of a debate.
# Installed BEFORE argument parsing so even a bad-flag exit is
# recorded; the EXIT trap logs the exit code and the LAST section
# entered, so a partial run names where it stopped.
run_log_dir="${weft_state_dir}"
run_log="${run_log_dir}/setup-runs.log"
# Best-effort at both ends: the journal is a diagnostic aid, and a
# root-owned ~/.local/share/weft (docker can auto-create such entries)
# must not hard-kill the very install that would explain it.
if ! mkdir -p "${run_log_dir}" 2>/dev/null || ! { : >> "${run_log}"; } 2>/dev/null; then
run_log=/dev/null
warn "cannot write the run journal under ${run_log_dir}; continuing without it"
fi
# The journal keeps the last 400 lines (a couple of hundred runs), so it
# never grows without end.
if [[ "${run_log}" != /dev/null ]] && [[ "$(wc -l < "${run_log}")" -gt 400 ]]; then
tail -n 400 "${run_log}" > "${run_log}.tmp" 2>/dev/null && mv "${run_log}.tmp" "${run_log}" 2>/dev/null || rm -f "${run_log}.tmp"
fi
current_section="(argument parsing)"
# Whether this run has started changing the install (its binaries, its
# daemon, its extensions): until then a failure leaves the install as it
# was, and the exit line says so.
install_touched=0
# The exit code arrives as $1: a composite trap (`cleanup; log_run_exit`)
# would otherwise have $? read the CLEANUP's status and journal a died
# run as `DONE ok`.
# One entry per tracked package.json --bump has already moved this
# run (--vscode --browser --bump moves two): a failure AFTER a bump
# must name every version that moved, or a re-run with the same flags
# bumps a second time (and the version is what CI turns into a store
# release).
bump_pending_notes=()
log_run_exit() {
local code="${1:-$?}"
# A build slot this run still holds (it died mid-build) goes back
# first, so a waiting run elsewhere does not sit on it.
machine_lock_release_mine
if [[ ${code} -eq 0 ]]; then
printf '%s DONE ok\n' "$(date '+%Y-%m-%d %H:%M:%S')" >> "${run_log}"
else
printf '%s FAIL exit=%s in section %s\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" "${code}" "${current_section}" >> "${run_log}"
local journal_note=""
[[ "${run_log}" != /dev/null ]] && journal_note=" (journal: ${run_log})"
local state_note="the install is incomplete"
[[ ${install_touched} -eq 0 ]] && state_note="nothing was installed"
printf ' %s%s%s this run FAILED in %s%s%s; %s%s\n' \
"${C_RED}" "${SYM_FAIL}" "${C_RESET}" "${C_BOLD}" "${current_section}" "${C_RESET}" \
"${state_note}" "${journal_note}" >&2
if [[ ${#bump_pending_notes[@]} -gt 0 ]]; then
local n
for n in "${bump_pending_notes[@]}"; do
printf ' %s%s%s %s\n' "${C_YELLOW}" "${SYM_WARN}" "${C_RESET}" "${n}" >&2
done
fi
fi
}
trap 'log_run_exit $?' EXIT
while [[ $# -gt 0 ]]; do
case "$1" in
--uninstall) do_uninstall=1 ;;
--purge) do_purge=1 ;;
--debian) purge_debian=1 ;;
--kind) purge_kind=1 ;;
--postgres) purge_postgres=1 ;;
--cli) components_flip; build_cli=1 ;;
--daemon) components_flip; refresh_daemon=1 ;;
--vscode) components_flip; build_vscode=1 ;;
--browser) components_flip; build_browser=1 ;;
--bump) do_bump=1 ;;
--no-sign) do_sign=0 ;;
--no-daemon) refresh_daemon=0; no_daemon_seen=1 ;;
--public-url) public_url_flag="--public-url" ;;
--no-public-url) public_url_flag="--no-public-url" ;;
--chrome) targets_flip; target_chrome=1 ;;
--firefox) targets_flip; target_firefox=1 ;;
--edge) targets_flip; target_edge=1 ;;
--opera) targets_flip; target_opera=1 ;;
--safari) targets_flip; target_safari=1 ;;
--rebuild) rebuild_flag="--rebuild" ;;
--debug) profile="dev" ;;
--prefix)
shift
[[ $# -gt 0 ]] || { fail "--prefix needs a path"; exit 1; }
prefix="$1" ;;
--migration)
shift
[[ $# -gt 0 ]] || { fail "--migration needs a name: ./setup.sh --migration add_owner"; exit 1; }
write_migration="$1" ;;
--release) do_release=1 ;;
--rename)
shift
[[ $# -gt 0 ]] || { fail "--rename needs what: --rename table.old_column=new_column, or --rename old_table=new_table"; exit 1; }
migration_renames+=(--rename "$1") ;;
--unrelated) migration_renames+=(--unrelated) ;;
--from-source) from_source=1 ;;
--yes) assume_yes=1 ;;
--verbose) verbose=1 ;;
-h|--help)
# The whole leading comment block, however long it grows: from
# line 2, print `#` lines with the marker stripped, stop at the
# first non-comment line. Never journaled: a help read is not a
# run, and a DONE line for it would dilute the journal's answer
# to "did my last install finish".
trap - EXIT
awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"
exit 0
;;
*)
fail "unknown flag: $1 (run --help for the list)"
exit 1
;;
esac
shift
done
# A flag that would silently apply to nothing is a mistyped invocation,
# not a preference: fail naming the shape that works. An uninstall or
# purge builds nothing, so every build-shaping flag is inapplicable
# there; the purge-image pickers only mean something WITH a purge; and
# --public-url / --migration both ride the daemon refresh, so a run
# that skips it must not accept them (--public-url opens an
# internet-facing surface: a silent no-op there is the worst outcome,
# and --migration's draft is only APPLIED by the refresh).
if [[ $do_uninstall -eq 1 || $do_purge -eq 1 ]]; then
if [[ $do_bump -eq 1 || $do_sign -eq 0 || $target_flag_seen -eq 1 || -n "${write_migration}" \
|| -n "${public_url_flag}" || -n "${rebuild_flag}" \
|| $from_source -eq 1 || "${profile}" != "release" || $assume_yes -eq 1 \
|| $component_flag_seen -eq 1 || $no_daemon_seen -eq 1 ]]; then
fail "an --uninstall / --purge run builds and refreshes nothing, so --bump, --no-sign, --migration, --public-url/--no-public-url, --rebuild, --debug, --from-source, --yes, the component flags (--cli/--daemon/--vscode/--browser), --no-daemon and the browser-target flags do not apply here"
exit 1
fi
else
if [[ $purge_postgres -eq 1 || $purge_kind -eq 1 || $purge_debian -eq 1 ]]; then
fail "--postgres / --kind / --debian only pick which SHARED base images a purge also removes; pass them with --purge"
exit 1
fi
if [[ $build_cli -eq 0 && $refresh_daemon -eq 0 && $build_vscode -eq 0 && $build_browser -eq 0 ]]; then
# Reachable via e.g. `--daemon --no-daemon`: a run that does
# nothing must say so, never journal a DONE over it.
fail "this combination selects nothing to build or refresh"
exit 1
fi
if [[ -n "${public_url_flag}" && $refresh_daemon -eq 0 ]]; then
fail "${public_url_flag} is applied by the daemon refresh, which this run skips; drop --no-daemon or the component flags that exclude --daemon"
exit 1
fi
if [[ -n "${write_migration}" && $refresh_daemon -eq 0 ]]; then
fail "--migration writes the migration and the daemon refresh applies it, and this run skips the daemon; include --daemon (or drop --no-daemon)"
exit 1
fi
if [[ $no_daemon_seen -eq 1 && $component_flag_seen -eq 1 ]]; then
fail "--no-daemon opts out of the daemon refresh, which this component selection does not include anyway; drop it"
exit 1
fi
if [[ $do_bump -eq 1 && $build_vscode -eq 0 && $build_browser -eq 0 ]]; then
fail "--bump releases the extensions this run builds, and this run builds neither; pass it with --vscode and/or --browser (the default install covers --vscode)"
exit 1
fi
if [[ $build_browser -eq 0 ]]; then
if [[ $do_sign -eq 0 ]]; then
fail "--no-sign shapes the browser-extension build, which this run does not do; pass it with --browser"
exit 1
fi
if [[ $target_flag_seen -eq 1 ]]; then
fail "the browser-target flags (--chrome/--firefox/--edge/--opera/--safari) shape the browser-extension build, which this run does not do; pass them with --browser"
exit 1
fi
fi
fi
# The full install (CLI + daemon + VS Code, no browser), the one shape
# that gets the post-install summary. Computed ONCE, here,
# where every component flag is final; an uninstall/purge run is never
# an install, whatever the component defaults say.
is_full_install=0
if [[ $do_uninstall -eq 0 && $do_purge -eq 0 \
&& $build_cli -eq 1 && $refresh_daemon -eq 1 && $build_vscode -eq 1 && $build_browser -eq 0 ]]; then
is_full_install=1
fi
# The journal's START line, after parsing so a --help read never
# journals; a bad flag above still lands its FAIL line via the trap.
printf '%s START tree=%s args=[%s]\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" "${here}" "${orig_args}" >> "${run_log}"
# Worktree banner: this script builds/installs from its OWN directory (`here`).
# With multiple git worktrees of this repo checked out, it is easy to run the
# wrong one and rebuild stale source; print the tree up front so a wrong-tree
# run is obvious before anything is built.
hint "installing from ${C_BOLD}${here}${C_RESET}"
# ---- prebuilt artifacts ----------------------------------------------
#
# CI publishes a rolling release on every push to the release branch:
# CLI binaries, the .vsix, and a manifest.json naming the exact commit
# and source tree they were built from plus a sha256 per asset. When
# this checkout holds that source with no local change, downloading is
# equivalent to building,
# so the install skips the compilers entirely (no Rust or Node
# toolchain needed). Any local change, a different source tree, or no
# network takes the build path instead, and every branch says so.
#
# SYNC: release tag + asset names + manifest.json keys (commit, tree,
# vscode_version, sha256_<asset>) <->
# .github/workflows/release.yml (the linux and macos matrix `asset`
# values, the vsix `mv` target, the release job's manifest
# generation, release-macos), scripts/release-cli.sh
release_assets_url="https://github.com/WeaveMindAI/weft/releases/download/latest"
# Where downloads land before they are installed: shared by every
# checkout, and only ever main's release, which only the default install
# runs.
prebuilt_dir="${weft_state_dir}/prebuilt"
prebuilt_commit=""
prebuilt_vscode_version=""
use_prebuilt_cli=0
use_prebuilt_vsix=0
cli_sha256=""
runtime_sha256=""
vsix_sha256=""
cli_asset=""
case "$(uname -s)-$(uname -m)" in
Linux-x86_64) cli_asset="weft-x86_64-linux" ;;
Linux-aarch64) cli_asset="weft-aarch64-linux" ;;
Darwin-arm64) cli_asset="weft-aarch64-macos" ;;
Darwin-x86_64) cli_asset="weft-x86_64-macos" ;;
esac
# The runtime published beside each CLI.
# SYNC: asset names <-> .github/workflows/release.yml (the runtime binaries)
runtime_asset="${cli_asset:+${cli_asset/weft-/weft-runtime-}}"
# The standard worker published beside each CLI: the program a local
# install's projects run when their nodes are all the standard library's.
# SYNC: the worker asset name and layout <-> .github/actions/build-binaries, crates/weft-cli/src/host_programs.rs (pack_standard_program)
worker_asset="${cli_asset:+${cli_asset/weft-/weft-worker-}}"
# One string value out of the fetched manifest, empty when the key is
# absent (an asset key missing means "not published; build locally
# there"). THE one JSON-string extractor, `[^"]*` so no value alphabet
# assumption can silently zero a field (a prerelease version once
# parsed empty and cost every user a full compile).
manifest_value() {
sed -n 's/.*"'"$1"'": *"\([^"]*\)".*/\1/p' <<<"${release_manifest}"
}
# Decide, per component this run installs, whether the published build
# can stand in for a local one. Sets use_prebuilt_cli / use_prebuilt_vsix
# plus the shas + version the install sections consume; every path that
# declines says why.
decide_prebuilt_use() {
if [[ -n "${write_migration}" ]]; then
# The migration lands in the source this run, and the runtime
# carries every migration in its binary: the published one cannot.
hint "--migration adds a migration to the source; building locally"
return 0
fi
if ! git -C "${here}" rev-parse HEAD >/dev/null 2>&1; then
hint "not a git checkout, so no published build can be matched; building locally"
return 0
fi
if [[ -n "$(git -C "${here}" status --porcelain 2>/dev/null)" ]]; then
hint "local changes present; building locally"
return 0
fi
# Interrupted downloads leave per-PID temps; reap only STALE ones so
# a concurrent run's in-flight temp survives.
find "${prebuilt_dir}" -name '*.download' -mmin +60 -delete 2>/dev/null || true
release_manifest="$(curl -fsSL --max-time 10 "${release_assets_url}/manifest.json" 2>/dev/null || true)"
if [[ -z "${release_manifest}" ]]; then
hint "could not reach the published build manifest; building locally"
return 0
fi
prebuilt_commit="$(manifest_value "commit")"
prebuilt_vscode_version="$(manifest_value "vscode_version")"
# The source tree, not the commit: a merge that brings in exactly the
# published source is another commit with the same tree.
prebuilt_tree="$(manifest_value "tree")"
if [[ -z "${prebuilt_tree}" ]]; then
hint "the published manifest names no source tree; building locally"
return 0
fi
if [[ "${prebuilt_tree}" != "$(git -C "${here}" rev-parse 'HEAD^{tree}')" ]]; then
hint "no prebuilt artifacts for this source yet (latest published: commit ${prebuilt_commit:0:12}); building locally. If you just pulled, CI is likely still building; re-run in a while to download instead."
return 0
fi
if [[ $build_cli -eq 1 ]]; then
# A debug CLI (--debug) is always a local build; the published
# binary is a release build.
if [[ "${profile}" != "release" ]]; then
hint "a --debug CLI is always built locally"
elif [[ -z "${cli_asset}" ]]; then
hint "no published CLI for $(uname -s)-$(uname -m); building the CLI locally"
else
cli_sha256="$(manifest_value "sha256_${cli_asset}")"
runtime_sha256="$(manifest_value "sha256_${runtime_asset}")"
if [[ -n "${cli_sha256}" && -n "${runtime_sha256}" ]]; then
use_prebuilt_cli=1
else
hint "this commit's ${cli_asset} or ${runtime_asset} is not published (still building in CI, or its build failed); building the CLI and the runtime locally. Re-run in a while to download instead."
fi
fi
fi
if [[ $build_vscode -eq 1 ]]; then
if [[ $do_bump -eq 1 ]]; then
# The bump moves the version past the published package by
# definition; the release must ship the bumped build.
hint "--bump: building the VS Code extension locally at the new version"
else
vsix_sha256="$(manifest_value "sha256_weft-vscode.vsix")"
if [[ -n "${prebuilt_vscode_version}" && -n "${vsix_sha256}" ]]; then
use_prebuilt_vsix=1
else
hint "this commit's .vsix was not published (its build failed in CI); building the extension locally"
fi
fi
fi
}
# Probe only when this run can actually consume a prebuilt (installing
# the CLI or the extension): an uninstall/purge or a --browser-only run
# must neither wait on the network nor print build-path hints. --bump
# also forces the build path for the bumped extension: the bump makes
# the tree diverge from the published commit by definition.
if [[ $do_uninstall -eq 0 && $do_purge -eq 0 ]] \
&& [[ $build_cli -eq 1 || $build_vscode -eq 1 ]]; then
if [[ $from_source -eq 0 ]]; then
decide_prebuilt_use
else
hint "--from-source: compiling the CLI/.vsix locally, published artifacts ignored"
fi
fi
# Download one release asset to its final path, verified against the
# sha256 the manifest recorded for it: a truncated transfer, a caching
# proxy, or an asset mid-replacement (the release updates
# non-atomically) all fail the compare instead of installing wrong
# bytes. `mode` (optional) is applied to the temp BEFORE the atomic
# move, so a binary is never observable half-installed as
# non-executable. Per-PID temp name so two concurrent runs never race
# the mv. Returns non-zero (having removed the temp) on any miss; the
# caller owns the fallback.
download_verified() {
local asset="$1" want_sha="$2" dest="$3" mode="${4:-}"
local tmp="${dest}.$$.download"
mkdir -p "$(dirname "${dest}")"
# --max-time: a stalled connection must fail (and fall back to the
# local build) instead of hanging the install with no output.
if ! curl -fsSL --retry 2 --max-time 300 -o "${tmp}" "${release_assets_url}/${asset}"; then
rm -f "${tmp}"
return 1
fi
local got_sha
got_sha="$(sha256 "${tmp}" | cut -d' ' -f1)"
if [[ "${got_sha}" != "${want_sha}" ]]; then
rm -f "${tmp}"
warn "downloaded ${asset} does not match the manifest's checksum (got ${got_sha:0:12}, want ${want_sha:0:12})"
return 1
fi
if [[ -n "${mode}" ]]; then
chmod "${mode}" "${tmp}"
fi
# Every failure path drops the temp, this one included (an
# unmovable temp would otherwise linger as junk nobody names).
mv -f "${tmp}" "${dest}" || { rm -f "${tmp}"; return 1; }
}
# The published standard worker, unpacked where the install's workers
# start from: <data dir>/workers/<its hash>/ (the hash travels inside the
# asset, the binary hash the CLI computes for this same source). Without
# it the install's first start compiles the standard worker here, once.
install_prebuilt_standard_worker() {
local want_sha
want_sha="$(manifest_value "sha256_${worker_asset}")"
if [[ -z "${worker_asset}" || -z "${want_sha}" ]]; then
hint "no published standard worker for this commit and machine; the install's first start compiles it here, once"
return 0
fi
local packed="${prebuilt_dir}/${worker_asset}"
if ! acquire_prebuilt "${worker_asset}" "${want_sha}" "${packed}"; then
warn "the published standard worker could not be downloaded; the install's first start compiles it here, once"
return 0
fi
local hash
if ! hash="$(tar -xzOf "${packed}" hash)" || ! [[ "${hash}" =~ ^[0-9a-f]+$ ]]; then
warn "the published standard worker names no binary hash; the install's first start compiles it here, once"
return 0
fi
local dest="${weft_state_dir}/workers/${hash}"
if [[ -f "${dest}/worker" ]]; then
return 0
fi
local unpacking="${dest}.unpacking.$$"
rm -rf "${unpacking}" && mkdir -p "${unpacking}"
tar -xzf "${packed}" -C "${unpacking}" worker left-out.json
if [[ -e "${dest}" ]]; then
# A folder with no program yet (a compile stopped halfway, or one
# running now, which then puts the same program over this one): the
# record and then the program go into it by rename, the order a
# compile places them in. Its holds stay.
if ! mv -f "${unpacking}/left-out.json" "${dest}/left-out.json" || ! mv -f "${unpacking}/worker" "${dest}/worker"; then
rm -rf "${unpacking}"
warn "the published standard worker could not be put in ${dest}; the install's first start compiles it here, once"
return 0
fi
rm -rf "${unpacking}"
else
mv "${unpacking}" "${dest}"
fi
ok "standard worker ready ${C_DIM}(${hash:0:12}, from the published build)${C_RESET}"
}
# Make one prebuilt asset exist at `dest`: a byte-matching copy from an
# earlier run is reused, anything else is downloaded and verified. One
# ladder for the CLI and the .vsix, so "already downloaded" can never
# mean two things. Returns non-zero when neither worked; the caller
# owns the build-locally fallback and its toolchain messaging.
acquire_prebuilt() {
local asset="$1" want_sha="$2" dest="$3" mode="${4:-}"
if [[ -f "${dest}" && "$(sha256 "${dest}" | cut -d' ' -f1)" == "${want_sha}" ]]; then
ok "prebuilt $(basename "${dest}") already downloaded ${C_DIM}(commit ${prebuilt_commit:0:12})${C_RESET}"
return 0
fi
if download_verified "${asset}" "${want_sha}" "${dest}" ${mode:+"${mode}"}; then
ok "downloaded prebuilt $(basename "${dest}") ${C_DIM}(${asset}, commit ${prebuilt_commit:0:12})${C_RESET}"
return 0
fi