#!/usr/bin/env bash # # Build turbo-rust and install it where your shell can find it. # # scripts/install.sh # install into GOBIN, or GOPATH/bin # scripts/install.sh --prefix ~/bin # install somewhere else # scripts/install.sh --with-analyzer # install the language server too # scripts/install.sh --uninstall # remove it again # # The build goes to a temporary file first, so a failed build never replaces a # working installation, and the install itself is a rename rather than a write # over the binary that is already there. The version is stamped in by the # linker, so `turbo-rust -version` names the commit it was built from. set -euo pipefail readonly BINARY=turbo-rust # --- output ----------------------------------------------------------------- if [ -t 1 ]; then readonly BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m' else readonly BOLD='' DIM='' RED='' GREEN='' YELLOW='' RESET='' fi info() { printf '%s\n' "$*"; } step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; } ok() { printf '%s ✓%s %s\n' "$GREEN" "$RESET" "$*"; } warn() { printf '%s !%s %s\n' "$YELLOW" "$RESET" "$*"; } die() { printf '%s ✗%s %s\n' "$RED" "$RESET" "$*" >&2 exit 1 } usage() { cat </dev/null 2>&1 || die "go is not installed: https://go.dev/dl/" # default_prefix returns where "go install" would put a binary: GOBIN when it # is set, GOPATH/bin otherwise. That is the directory a Go developer is most # likely to already have on PATH. default_prefix() { local gobin gobin="$(go env GOBIN)" if [ -n "$gobin" ]; then printf '%s\n' "$gobin" else printf '%s/bin\n' "$(go env GOPATH)" fi } [ -n "$prefix" ] || prefix="$(default_prefix)" [ -n "$prefix" ] || die "cannot work out where to install; use --prefix DIR" readonly TARGET="$prefix/$BINARY" # --- uninstall -------------------------------------------------------------- if $uninstall; then step "Removing $TARGET" if [ -e "$TARGET" ]; then rm -f "$TARGET" ok "removed" else warn "nothing installed at $TARGET" fi exit 0 fi # --- toolchain -------------------------------------------------------------- step "Checking the Go toolchain" # The requirement lives in go.mod, so this check cannot drift from the build. required="$(awk '/^go /{print $2; exit}' "$REPO/go.mod")" installed="$(go env GOVERSION)" installed="${installed#go}" if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -1)" != "$required" ]; then die "Go $required or later is needed, but $installed is installed" fi ok "go $installed (go.mod asks for $required or later)" # --- build ------------------------------------------------------------------ step "Building $BINARY" readonly STAGING="$(mktemp -d)" trap 'rm -rf "$STAGING"' EXIT # The version the binary reports is stamped in by the linker, so that an # installed editor names the commit it was actually built from rather than a # constant somebody forgot to bump. Outside a git checkout — installed from a # tarball, say — there is nothing to describe and the binary works the version # out from its own build information instead. version_pkg="rickub.com/turbo-editors/turbo-core/version" if describe="$(git -C "$REPO" describe --tags --dirty 2>/dev/null)"; then commit="$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || true)" built="$(date -u +%Y-%m-%dT%H:%M:%SZ)" ldflags="-X '$version_pkg.stamp=$describe' -X '$version_pkg.commit=$commit' -X '$version_pkg.built=$built'" else ldflags="" fi if ! go build -ldflags "$ldflags" -o "$STAGING/$BINARY" "$REPO" 2>"$STAGING/build.log"; then cat "$STAGING/build.log" >&2 info "" warn "A failure in the repository root is often a stray .go file that has" warn "landed in package main. 'go vet .' names it." die "build failed; nothing was installed" fi # Running the staged binary is the only proof that the flags above reached the # linker rather than merely looking right. It happens before the install, so a # binary that cannot name its own version never replaces a working one. if ! stamped="$("$REPO/scripts/check-version.sh" "$STAGING/$BINARY" "${describe:-}" "${commit:-}" 2>&1)"; then info "$stamped" die "the build did not carry its version; nothing was installed" fi ok "built — $stamped" # --- install ---------------------------------------------------------------- step "Installing into $prefix" mkdir -p "$prefix" || die "cannot create $prefix" # Install by renaming a complete file over the target, never by writing into # the one that is there. # # macOS caches a binary's code signature against its inode. cp writes new bytes # into the existing inode, so the cached signature ends up describing something # else and the kernel refuses to execute the result — a reinstall that builds, # installs, and then will not run. A rename gives the name a fresh inode, so # there is nothing stale to cache. It is atomic besides: no moment at which a # half-written turbo-rust is on the PATH. # # The temporary has to sit in $prefix, because a rename only works within one # filesystem and $STAGING is somewhere else entirely. readonly INCOMING="$prefix/.$BINARY.incoming.$$" trap 'rm -rf "$STAGING"; rm -f "$INCOMING"' EXIT cp "$STAGING/$BINARY" "$INCOMING" || die "cannot write into $prefix" chmod 0755 "$INCOMING" mv -f "$INCOMING" "$TARGET" || die "cannot replace $TARGET" # Whatever the system said is the useful part: "does not run" on its own tells # nobody anything they can act on. if ! verify="$("$TARGET" -version 2>&1)"; then info "$verify" die "the installed binary does not run" fi version="$verify" ok "$version → $TARGET" # --- PATH ------------------------------------------------------------------- # on_path reports whether a directory is one the shell searches. on_path() { case ":${PATH:-}:" in *":$1:"*) return 0 ;; *) return 1 ;; esac } # shell_profile guesses the file that sets PATH for the user's shell. shell_profile() { case "${SHELL:-}" in */zsh) printf '~/.zshrc\n' ;; */fish) printf '~/.config/fish/config.fish\n' ;; *) printf '~/.bashrc\n' ;; esac } step "Checking your PATH" if on_path "$prefix"; then ok "$prefix is on your PATH" else warn "$prefix is not on your PATH. Add it:" info "" info " echo 'export PATH=\"\$PATH:$prefix\"' >> $(shell_profile)" info " exec \$SHELL" fi # --- the language server ---------------------------------------------------- # find_analyzer looks where the editor itself looks: PATH, then CARGO_HOME/bin, # then RUSTUP_HOME/bin. # # Finding it is not the same as its working. rustup installs a *shim* called # rust-analyzer whether or not the component is there, and the shim fails only # when it is run — so this asks it for its version rather than trusting the # file's existence, which is the difference between "you have completion" and # "you will find out you have not when you press Ctrl-Space". find_analyzer() { local candidate for candidate in \ "$(command -v rust-analyzer 2>/dev/null || true)" \ "${CARGO_HOME:-$HOME/.cargo}/bin/rust-analyzer" \ "${RUSTUP_HOME:-$HOME/.rustup}/bin/rust-analyzer"; do [ -n "$candidate" ] && [ -x "$candidate" ] || continue "$candidate" --version >/dev/null 2>&1 || continue printf '%s\n' "$candidate" return 0 done return 1 } step "Checking the language server" if $with_analyzer && ! find_analyzer >/dev/null; then info " installing rust-analyzer, which takes a minute…" rustup component add rust-analyzer || die "could not install rust-analyzer" fi if analyzer_path="$(find_analyzer)"; then ok "rust-analyzer at $analyzer_path" else warn "rust-analyzer is not installed, so there will be no completion." warn "Editing, colouring and themes all work without it." info "" info " rustup component add rust-analyzer" info " ${DIM}or re-run this script with --with-analyzer${RESET}" fi # --- what to do next -------------------------------------------------------- info "" step "Ready" info "" info " Open a file ${BOLD}inside a Go module${RESET} — completion needs one:" info "" info " cd /path/to/your/project" info " $BINARY main.go" info "" info " ${DIM}F10 menu · F2 save · Alt-X exit · type a '.' for completion${RESET}" info " ${DIM}themes: $BINARY -list-themes${RESET}" info ""