turbo-editors/turbo-pythonpublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-python.git
git clone ssh://git@rickub.com/turbo-editors/turbo-python.git

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

install.sh · 327 lines · 10.7 KBBash Blame HistoryRaw
📦 Turbo Python 6fc62ea k33g 10h ago1#!/usr/bin/env bash
2#
3# Build turbo-python 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-python -version` names the commit it was built from.
14
15set -euo pipefail
16
17readonly BINARY=turbo-python
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-server also install pylsp, 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_server=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-server)
64 with_server=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-python 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_server looks where the editor itself looks: PATH, then the active
234# virtual environment, the user's tool directory, pyenv's shims, and — on macOS
235# — the per-version script directory that is on nobody's PATH.
236#
237# Finding it is not the same as its working: a shim left behind by a tool
238# manager whose environment has since been removed sits on PATH and fails only
239# when started. So this asks it for its version rather than trusting the file's
240# existence, which is the difference between "you have completion" and "you
241# will find out you have not when you press Ctrl-Space".
242find_server() {
243 local candidate
244 for candidate in \
245 "$(command -v pylsp 2>/dev/null || true)" \
246 "${VIRTUAL_ENV:-/nonexistent}/bin/pylsp" \
247 "$HOME/.local/bin/pylsp" \
248 "${PYENV_ROOT:-$HOME/.pyenv}/shims/pylsp" \
249 "$HOME"/Library/Python/*/bin/pylsp; do
250 [ -n "$candidate" ] && [ -x "$candidate" ] || continue
251 "$candidate" --version >/dev/null 2>&1 || continue
252 printf '%s\n' "$candidate"
253 return 0
254 done
255 return 1
256}
257
258# server_has_linters reports whether the server can produce diagnostics at all.
259#
260# This is the trap that costs an afternoon. Installed without its extras, pylsp
261# starts, completes and jumps to definitions — and publishes an *empty* list of
262# problems for a file that does not even parse, because the linters that find
263# them are optional dependencies. A blank gutter because the server has no
264# linter and a blank gutter because the code is fine look identical.
265#
266# pylsp is a script whose first line names the interpreter it runs under, and
267# the linters live in that interpreter's environment. A server this cannot read
268# a shebang from returns 2, and nothing is claimed either way.
269server_has_linters() {
270 local interpreter
271 interpreter="$(sed -n '1s/^#!\([^ ]*\).*/\1/p' "$1" 2>/dev/null || true)"
272 [ -n "$interpreter" ] && [ -x "$interpreter" ] || return 2
273 "$interpreter" -c 'import pyflakes' >/dev/null 2>&1
274}
275
276# install_server runs the one command the editor's install hint names.
277install_server() {
278 if command -v pipx >/dev/null 2>&1; then
279 pipx install "python-lsp-server[all]" || die "pipx could not install python-lsp-server"
280 elif command -v uv >/dev/null 2>&1; then
281 uv tool install "python-lsp-server[all]" || die "uv could not install python-lsp-server"
282 else
283 die "neither pipx nor uv is installed; see https://pipx.pypa.io"
284 fi
285}
286
287step "Checking the language server"
288
289if $with_server && ! find_server >/dev/null; then
290 info " installing python-lsp-server…"
291 install_server
292fi
293
294if server_path="$(find_server)"; then
295 ok "pylsp at $server_path"
296
297 server_has_linters "$server_path"
298 case $? in
299 0) ;;
300 1)
301 warn "…but it has no linters, so there will be no error marks in the gutter."
302 warn "Completion and Go to definition work; problems will never appear."
303 info ""
304 info " pipx install --force \"python-lsp-server[all]\""
305 ;;
306 esac
307else
308 warn "pylsp is not installed, so there will be no completion."
309 warn "Editing, colouring and themes all work without it."
310 info ""
311 info " pipx install \"python-lsp-server[all]\""
312 info " ${DIM}or re-run this script with --with-server${RESET}"
313fi
314
315# --- what to do next --------------------------------------------------------
316
317info ""
318step "Ready"
319info ""
320info " Open a file ${BOLD}inside a Go module${RESET} — completion needs one:"
321info ""
322info " cd /path/to/your/project"
323info " $BINARY main.go"
324info ""
325info " ${DIM}F10 menu · F2 save · Alt-X exit · type a '.' for completion${RESET}"
326info " ${DIM}themes: $BINARY -list-themes${RESET}"
327info ""