turbo-editors/turbo-pythonpublic Fork 0
v1.0.2
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.

📦 Turbo Python 6fc62ea · on v1.0.2 · k33g · 7h ago
install.sh · 327 lines · 10.7 KBBash Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#!/usr/bin/env bash
#
# Build turbo-python 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-python -version` names the commit it was built from.

set -euo pipefail

readonly BINARY=turbo-python

# --- 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 <<EOF
${BOLD}$BINARY installer${RESET}

  scripts/install.sh [options]

Options:
  -p, --prefix DIR   install into DIR (default: GOBIN, or GOPATH/bin)
      --with-server  also install pylsp, which completion needs
      --uninstall    remove an installed $BINARY and stop
  -h, --help         show this and stop
EOF
}

# --- arguments --------------------------------------------------------------

prefix=""
with_server=false
uninstall=false

while [ $# -gt 0 ]; do
	case "$1" in
	-p | --prefix)
		[ $# -ge 2 ] || die "--prefix needs a directory"
		prefix="$2"
		shift 2
		;;
	--with-server)
		with_server=true
		shift
		;;
	--uninstall)
		uninstall=true
		shift
		;;
	-h | --help)
		usage
		exit 0
		;;
	*) die "unknown option: $1 (try --help)" ;;
	esac
done

# --- where things are -------------------------------------------------------

cd "$(dirname "${BASH_SOURCE[0]}")/.."
readonly REPO="$PWD"

command -v go >/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-python 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_server looks where the editor itself looks: PATH, then the active
# virtual environment, the user's tool directory, pyenv's shims, and — on macOS
# — the per-version script directory that is on nobody's PATH.
#
# Finding it is not the same as its working: a shim left behind by a tool
# manager whose environment has since been removed sits on PATH and fails only
# when started. 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_server() {
	local candidate
	for candidate in \
		"$(command -v pylsp 2>/dev/null || true)" \
		"${VIRTUAL_ENV:-/nonexistent}/bin/pylsp" \
		"$HOME/.local/bin/pylsp" \
		"${PYENV_ROOT:-$HOME/.pyenv}/shims/pylsp" \
		"$HOME"/Library/Python/*/bin/pylsp; do
		[ -n "$candidate" ] && [ -x "$candidate" ] || continue
		"$candidate" --version >/dev/null 2>&1 || continue
		printf '%s\n' "$candidate"
		return 0
	done
	return 1
}

# server_has_linters reports whether the server can produce diagnostics at all.
#
# This is the trap that costs an afternoon. Installed without its extras, pylsp
# starts, completes and jumps to definitions — and publishes an *empty* list of
# problems for a file that does not even parse, because the linters that find
# them are optional dependencies. A blank gutter because the server has no
# linter and a blank gutter because the code is fine look identical.
#
# pylsp is a script whose first line names the interpreter it runs under, and
# the linters live in that interpreter's environment. A server this cannot read
# a shebang from returns 2, and nothing is claimed either way.
server_has_linters() {
	local interpreter
	interpreter="$(sed -n '1s/^#!\([^ ]*\).*/\1/p' "$1" 2>/dev/null || true)"
	[ -n "$interpreter" ] && [ -x "$interpreter" ] || return 2
	"$interpreter" -c 'import pyflakes' >/dev/null 2>&1
}

# install_server runs the one command the editor's install hint names.
install_server() {
	if command -v pipx >/dev/null 2>&1; then
		pipx install "python-lsp-server[all]" || die "pipx could not install python-lsp-server"
	elif command -v uv >/dev/null 2>&1; then
		uv tool install "python-lsp-server[all]" || die "uv could not install python-lsp-server"
	else
		die "neither pipx nor uv is installed; see https://pipx.pypa.io"
	fi
}

step "Checking the language server"

if $with_server && ! find_server >/dev/null; then
	info "  installing python-lsp-server…"
	install_server
fi

if server_path="$(find_server)"; then
	ok "pylsp at $server_path"

	server_has_linters "$server_path"
	case $? in
	0) ;;
	1)
		warn "…but it has no linters, so there will be no error marks in the gutter."
		warn "Completion and Go to definition work; problems will never appear."
		info ""
		info "    pipx install --force \"python-lsp-server[all]\""
		;;
	esac
else
	warn "pylsp is not installed, so there will be no completion."
	warn "Editing, colouring and themes all work without it."
	info ""
	info "    pipx install \"python-lsp-server[all]\""
	info "    ${DIM}or re-run this script with --with-server${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 ""