turbo-editors/turbo-rustpublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-rust.git
git clone ssh://git@rickub.com/turbo-editors/turbo-rust.git

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

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