turbo-editors/turbo-jspublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-js.git
git clone ssh://git@rickub.com/turbo-editors/turbo-js.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

install.sh · 294 lines · 9.5 KBBash Blame HistoryRaw
📦 Turbo JS 91999d1 k33g 11h ago1#!/usr/bin/env bash
2#
3# Build turbo-js and install it where your shell can find it.
4#
5# scripts/install.sh # install into GOBIN, or GOPATH/bin
6# scripts/install.sh --prefix ~/bin # install somewhere else
7# scripts/install.sh --with-server # install the language server too
8# scripts/install.sh --uninstall # remove it again
9#
10# The build goes to a temporary file first, so a failed build never replaces a
11# working installation, and the install itself is a rename rather than a write
12# over the binary that is already there. The version is stamped in by the
13# linker, so `turbo-js -version` names the commit it was built from.
14
15set -euo pipefail
16
17readonly BINARY=turbo-js
18readonly SERVER=typescript-language-server
19
20# --- output -----------------------------------------------------------------
21
22if [ -t 1 ]; then
23 readonly BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m'
24else
25 readonly BOLD='' DIM='' RED='' GREEN='' YELLOW='' RESET=''
26fi
27
28info() { printf '%s\n' "$*"; }
29step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; }
30ok() { printf '%s ✓%s %s\n' "$GREEN" "$RESET" "$*"; }
31warn() { printf '%s !%s %s\n' "$YELLOW" "$RESET" "$*"; }
32die() {
33 printf '%s ✗%s %s\n' "$RED" "$RESET" "$*" >&2
34 exit 1
35}
36
37usage() {
38 cat <<EOF
39${BOLD}$BINARY installer${RESET}
40
41 scripts/install.sh [options]
42
43Options:
44 -p, --prefix DIR install into DIR (default: GOBIN, or GOPATH/bin)
45 --with-server also install $SERVER, which completion needs
46 --uninstall remove an installed $BINARY and stop
47 -h, --help show this and stop
48EOF
49}
50
51# --- arguments --------------------------------------------------------------
52
53prefix=""
54with_server=false
55uninstall=false
56
57while [ $# -gt 0 ]; do
58 case "$1" in
59 -p | --prefix)
60 [ $# -ge 2 ] || die "--prefix needs a directory"
61 prefix="$2"
62 shift 2
63 ;;
64 --with-server)
65 with_server=true
66 shift
67 ;;
68 --uninstall)
69 uninstall=true
70 shift
71 ;;
72 -h | --help)
73 usage
74 exit 0
75 ;;
76 *) die "unknown option: $1 (try --help)" ;;
77 esac
78done
79
80# --- where things are -------------------------------------------------------
81
82cd "$(dirname "${BASH_SOURCE[0]}")/.."
83readonly REPO="$PWD"
84
85command -v go >/dev/null 2>&1 || die "go is not installed: https://go.dev/dl/"
86
87# default_prefix returns where "go install" would put a binary: GOBIN when it
88# is set, GOPATH/bin otherwise. That is the directory a Go developer is most
89# likely to already have on PATH.
90default_prefix() {
91 local gobin
92 gobin="$(go env GOBIN)"
93 if [ -n "$gobin" ]; then
94 printf '%s\n' "$gobin"
95 else
96 printf '%s/bin\n' "$(go env GOPATH)"
97 fi
98}
99
100[ -n "$prefix" ] || prefix="$(default_prefix)"
101[ -n "$prefix" ] || die "cannot work out where to install; use --prefix DIR"
102readonly TARGET="$prefix/$BINARY"
103
104# --- uninstall --------------------------------------------------------------
105
106if $uninstall; then
107 step "Removing $TARGET"
108 if [ -e "$TARGET" ]; then
109 rm -f "$TARGET"
110 ok "removed"
111 else
112 warn "nothing installed at $TARGET"
113 fi
114 exit 0
115fi
116
117# --- toolchain --------------------------------------------------------------
118
119step "Checking the Go toolchain"
120
121# The requirement lives in go.mod, so this check cannot drift from the build.
122required="$(awk '/^go /{print $2; exit}' "$REPO/go.mod")"
123installed="$(go env GOVERSION)"
124installed="${installed#go}"
125
126if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then
127 die "Go $required or later is needed, but $installed is installed"
128fi
129ok "go $installed (go.mod asks for $required or later)"
130
131# --- build ------------------------------------------------------------------
132
133step "Building $BINARY"
134
135readonly STAGING="$(mktemp -d)"
136trap 'rm -rf "$STAGING"' EXIT
137
138# The version the binary reports is stamped in by the linker, so that an
139# installed editor names the commit it was actually built from rather than a
140# constant somebody forgot to bump. Outside a git checkout — installed from a
141# tarball, say — there is nothing to describe and the binary works the version
142# out from its own build information instead.
143version_pkg="rickub.com/turbo-editors/turbo-core/version"
144if describe="$(git -C "$REPO" describe --tags --dirty 2>/dev/null)"; then
145 commit="$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || true)"
146 built="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
147 ldflags="-X '$version_pkg.stamp=$describe' -X '$version_pkg.commit=$commit' -X '$version_pkg.built=$built'"
148else
149 ldflags=""
150fi
151
152if ! go build -ldflags "$ldflags" -o "$STAGING/$BINARY" "$REPO" 2>"$STAGING/build.log"; then
153 cat "$STAGING/build.log" >&2
154 info ""
155 warn "A failure in the repository root is often a stray .go file that has"
156 warn "landed in package main. 'go vet .' names it."
157 die "build failed; nothing was installed"
158fi
159
160# Running the staged binary is the only proof that the flags above reached the
161# linker rather than merely looking right. It happens before the install, so a
162# binary that cannot name its own version never replaces a working one.
163if ! stamped="$("$REPO/scripts/check-version.sh" "$STAGING/$BINARY" "${describe:-}" "${commit:-}" 2>&1)"; then
164 info "$stamped"
165 die "the build did not carry its version; nothing was installed"
166fi
167ok "built — $stamped"
168
169# --- install ----------------------------------------------------------------
170
171step "Installing into $prefix"
172
173mkdir -p "$prefix" || die "cannot create $prefix"
174
175# Install by renaming a complete file over the target, never by writing into
176# the one that is there.
177#
178# macOS caches a binary's code signature against its inode. cp writes new bytes
179# into the existing inode, so the cached signature ends up describing something
180# else and the kernel refuses to execute the result — a reinstall that builds,
181# installs, and then will not run. A rename gives the name a fresh inode, so
182# there is nothing stale to cache. It is atomic besides: no moment at which a
183# half-written turbo-js is on the PATH.
184#
185# The temporary has to sit in $prefix, because a rename only works within one
186# filesystem and $STAGING is somewhere else entirely.
187readonly INCOMING="$prefix/.$BINARY.incoming.$$"
188trap 'rm -rf "$STAGING"; rm -f "$INCOMING"' EXIT
189
190cp "$STAGING/$BINARY" "$INCOMING" || die "cannot write into $prefix"
191chmod 0755 "$INCOMING"
192mv -f "$INCOMING" "$TARGET" || die "cannot replace $TARGET"
193
194# Whatever the system said is the useful part: "does not run" on its own tells
195# nobody anything they can act on.
196if ! verify="$("$TARGET" -version 2>&1)"; then
197 info "$verify"
198 die "the installed binary does not run"
199fi
200version="$verify"
201ok "$version$TARGET"
202
203# --- PATH -------------------------------------------------------------------
204
205# on_path reports whether a directory is one the shell searches.
206on_path() {
207 case ":${PATH:-}:" in
208 *":$1:"*) return 0 ;;
209 *) return 1 ;;
210 esac
211}
212
213# shell_profile guesses the file that sets PATH for the user's shell.
214shell_profile() {
215 case "${SHELL:-}" in
216 */zsh) printf '~/.zshrc\n' ;;
217 */fish) printf '~/.config/fish/config.fish\n' ;;
218 *) printf '~/.bashrc\n' ;;
219 esac
220}
221
222step "Checking your PATH"
223if on_path "$prefix"; then
224 ok "$prefix is on your PATH"
225else
226 warn "$prefix is not on your PATH. Add it:"
227 info ""
228 info " echo 'export PATH=\"\$PATH:$prefix\"' >> $(shell_profile)"
229 info " exec \$SHELL"
230fi
231
232# --- the language server ----------------------------------------------------
233
234# find_server looks where the editor itself looks: PATH, then the directory
235# nvm's current Node writes global binaries into, then a per-user npm prefix,
236# then pnpm's and Volta's, then the two system prefixes.
237#
238# Finding it is not the same as its working. A version manager leaves a shim
239# on PATH for a Node that has since been removed, and the shim fails only when
240# it is run — so this asks the server for its version rather than trusting the
241# file's existence, which is the difference between "you have completion" and
242# "you will find out you have not when you press Ctrl-Space".
243find_server() {
244 local candidate
245 for candidate in \
246 "$(command -v "$SERVER" 2>/dev/null || true)" \
247 "${NVM_BIN:+$NVM_BIN/$SERVER}" \
248 "${NPM_CONFIG_PREFIX:-$HOME/.npm-global}/bin/$SERVER" \
249 "${PNPM_HOME:+$PNPM_HOME/$SERVER}" \
250 "${VOLTA_HOME:-$HOME/.volta}/bin/$SERVER" \
251 "/usr/local/bin/$SERVER" \
252 "/opt/homebrew/bin/$SERVER"; do
253 [ -n "$candidate" ] && [ -x "$candidate" ] || continue
254 "$candidate" --version >/dev/null 2>&1 || continue
255 printf '%s\n' "$candidate"
256 return 0
257 done
258 return 1
259}
260
261step "Checking the language server"
262
263if $with_server && ! find_server >/dev/null; then
264 command -v npm >/dev/null 2>&1 || die "npm is not installed, so $SERVER cannot be: https://nodejs.org"
265 info " installing $SERVER, which takes a minute…"
266 # typescript@6, not typescript: TypeScript 7 is the native port and ships no
267 # tsserver.js, and the server refuses to start beside it.
268 npm install -g typescript-language-server typescript@6 || die "could not install $SERVER"
269fi
270
271if server_path="$(find_server)"; then
272 ok "$SERVER at $server_path"
273else
274 warn "$SERVER is not installed, so there will be no completion."
275 warn "Editing, colouring and themes all work without it."
276 info ""
277 info " npm install -g typescript-language-server typescript@6"
278 info " ${DIM}or re-run this script with --with-server${RESET}"
279fi
280
281# --- what to do next --------------------------------------------------------
282
283info ""
284step "Ready"
285info ""
286info " Open a file ${BOLD}inside a Node project${RESET} — the server is started in the"
287info " directory holding package.json:"
288info ""
289info " cd /path/to/your/project"
290info " $BINARY main.js"
291info ""
292info " ${DIM}F10 menu · F2 save · Alt-X exit · type a '.' for completion${RESET}"
293info " ${DIM}themes: $BINARY -list-themes${RESET}"
294info ""