| 📦 Turbo Go 3d7798b k33g 9h ago | 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # Build turbo-go 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-gopls # 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-go -version` names the commit it was built from. |
| 14 | |
| 15 | set -euo pipefail |
| 16 | |
| 17 | readonly BINARY=turbo-go |
| 18 | |
| 19 | # --- output ----------------------------------------------------------------- |
| 20 | |
| 21 | if [ -t 1 ]; then |
| 22 | readonly BOLD=$'\033[1m' DIM=$'\033[2m' RED=$'\033[31m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RESET=$'\033[0m' |
| 23 | else |
| 24 | readonly BOLD='' DIM='' RED='' GREEN='' YELLOW='' RESET='' |
| 25 | fi |
| 26 | |
| 27 | info() { printf '%s\n' "$*"; } |
| 28 | step() { printf '%s==>%s %s\n' "$BOLD" "$RESET" "$*"; } |
| 29 | ok() { printf '%s ✓%s %s\n' "$GREEN" "$RESET" "$*"; } |
| 30 | warn() { printf '%s !%s %s\n' "$YELLOW" "$RESET" "$*"; } |
| 31 | die() { |
| 32 | printf '%s ✗%s %s\n' "$RED" "$RESET" "$*" >&2 |
| 33 | exit 1 |
| 34 | } |
| 35 | |
| 36 | usage() { |
| 37 | cat <<EOF |
| 38 | ${BOLD}$BINARY installer${RESET} |
| 39 | |
| 40 | scripts/install.sh [options] |
| 41 | |
| 42 | Options: |
| 43 | -p, --prefix DIR install into DIR (default: GOBIN, or GOPATH/bin) |
| 44 | --with-gopls also install gopls, which completion needs |
| 45 | --uninstall remove an installed $BINARY and stop |
| 46 | -h, --help show this and stop |
| 47 | EOF |
| 48 | } |
| 49 | |
| 50 | # --- arguments -------------------------------------------------------------- |
| 51 | |
| 52 | prefix="" |
| 53 | with_gopls=false |
| 54 | uninstall=false |
| 55 | |
| 56 | while [ $# -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-gopls) |
| 64 | with_gopls=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 |
| 77 | done |
| 78 | |
| 79 | # --- where things are ------------------------------------------------------- |
| 80 | |
| 81 | cd "$(dirname "${BASH_SOURCE[0]}")/.." |
| 82 | readonly REPO="$PWD" |
| 83 | |
| 84 | command -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. |
| 89 | default_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" |
| 101 | readonly TARGET="$prefix/$BINARY" |
| 102 | |
| 103 | # --- uninstall -------------------------------------------------------------- |
| 104 | |
| 105 | if $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 |
| 114 | fi |
| 115 | |
| 116 | # --- toolchain -------------------------------------------------------------- |
| 117 | |
| 118 | step "Checking the Go toolchain" |
| 119 | |
| 120 | # The requirement lives in go.mod, so this check cannot drift from the build. |
| 121 | required="$(awk '/^go /{print $2; exit}' "$REPO/go.mod")" |
| 122 | installed="$(go env GOVERSION)" |
| 123 | installed="${installed#go}" |
| 124 | |
| 125 | if [ "$(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" |
| 127 | fi |
| 128 | ok "go $installed (go.mod asks for $required or later)" |
| 129 | |
| 130 | # --- build ------------------------------------------------------------------ |
| 131 | |
| 132 | step "Building $BINARY" |
| 133 | |
| 134 | readonly STAGING="$(mktemp -d)" |
| 135 | trap '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. |
| 142 | version_pkg="rickub.com/turbo-editors/turbo-core/version" |
| 143 | if 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'" |
| 147 | else |
| 148 | ldflags="" |
| 149 | fi |
| 150 | |
| 151 | if ! 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" |
| 157 | fi |
| 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. |
| 162 | if ! 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" |
| 165 | fi |
| 166 | ok "built — $stamped" |
| 167 | |
| 168 | # --- install ---------------------------------------------------------------- |
| 169 | |
| 170 | step "Installing into $prefix" |
| 171 | |
| 172 | mkdir -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-go 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. |
| 186 | readonly INCOMING="$prefix/.$BINARY.incoming.$$" |
| 187 | trap 'rm -rf "$STAGING"; rm -f "$INCOMING"' EXIT |
| 188 | |
| 189 | cp "$STAGING/$BINARY" "$INCOMING" || die "cannot write into $prefix" |
| 190 | chmod 0755 "$INCOMING" |
| 191 | mv -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. |
| 195 | if ! verify="$("$TARGET" -version 2>&1)"; then |
| 196 | info "$verify" |
| 197 | die "the installed binary does not run" |
| 198 | fi |
| 199 | version="$verify" |
| 200 | ok "$version → $TARGET" |
| 201 | |
| 202 | # --- PATH ------------------------------------------------------------------- |
| 203 | |
| 204 | # on_path reports whether a directory is one the shell searches. |
| 205 | on_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. |
| 213 | shell_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 | |
| 221 | step "Checking your PATH" |
| 222 | if on_path "$prefix"; then |
| 223 | ok "$prefix is on your PATH" |
| 224 | else |
| 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" |
| 229 | fi |
| 230 | |
| 231 | # --- the language server ---------------------------------------------------- |
| 232 | |
| 233 | # find_gopls looks where the editor itself looks: PATH, then GOBIN, then |
| 234 | # GOPATH/bin. |
| 235 | find_gopls() { |
| 236 | command -v gopls 2>/dev/null && return 0 |
| 237 | local candidate |
| 238 | for candidate in "$(go env GOBIN)/gopls" "$(go env GOPATH)/bin/gopls"; do |
| 239 | [ -x "$candidate" ] && { |
| 240 | printf '%s\n' "$candidate" |
| 241 | return 0 |
| 242 | } |
| 243 | done |
| 244 | return 1 |
| 245 | } |
| 246 | |
| 247 | step "Checking the language server" |
| 248 | |
| 249 | if $with_gopls && ! find_gopls >/dev/null; then |
| 250 | info " installing gopls, which takes a minute…" |
| 251 | go install golang.org/x/tools/gopls@latest || die "could not install gopls" |
| 252 | fi |
| 253 | |
| 254 | if gopls_path="$(find_gopls)"; then |
| 255 | ok "gopls at $gopls_path" |
| 256 | else |
| 257 | warn "gopls is not installed, so there will be no completion." |
| 258 | warn "Editing, colouring and themes all work without it." |
| 259 | info "" |
| 260 | info " go install golang.org/x/tools/gopls@latest" |
| 261 | info " ${DIM}or re-run this script with --with-gopls${RESET}" |
| 262 | fi |
| 263 | |
| 264 | # --- what to do next -------------------------------------------------------- |
| 265 | |
| 266 | info "" |
| 267 | step "Ready" |
| 268 | info "" |
| 269 | info " Open a file ${BOLD}inside a Go module${RESET} — completion needs one:" |
| 270 | info "" |
| 271 | info " cd /path/to/your/project" |
| 272 | info " $BINARY main.go" |
| 273 | info "" |
| 274 | info " ${DIM}F10 menu · F2 save · Alt-X exit · type a '.' for completion${RESET}" |
| 275 | info " ${DIM}themes: $BINARY -list-themes${RESET}" |
| 276 | info "" |