-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup_python.sh
More file actions
executable file
·272 lines (237 loc) · 10 KB
/
Copy pathsetup_python.sh
File metadata and controls
executable file
·272 lines (237 loc) · 10 KB
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
#!/usr/bin/env bash
# Installs the python.org CPython builds this setup expects, then points uv at them.
#
# Run after install.sh on a new Mac. Safe to re-run; installed versions are skipped
# unless --force is passed.
#
# ./setup_python.sh install missing versions
# ./setup_python.sh --force reinstall everything
# ./setup_python.sh --dry-run print what would happen
#
# Rebuilding project virtualenvs is deliberately out of scope. Each repo's own
# `just install` does that, and direnv recreates .venv on the next cd.
set -euo pipefail
# Newest macOS installer per line. 3.9 through 3.12 are security-only or EOL
# upstream, so their last binary release is older than the current source
# release. 3.9 and 3.10 stopped shipping installers after 3.9.13 and 3.10.11.
VERSIONS=(
"3.9:3.9.13"
"3.10:3.10.11"
"3.11:3.11.9"
"3.12:3.12.10"
"3.13:3.13.15"
"3.14:3.14.7"
"3.15:3.15.0rc1"
)
# Lines that also get the optional free-threaded build as python3.Xt.
FREETHREADED=(3.14 3.15)
# Backs bare `python3` and `pip3`. The PATH entry that selects it lives in bashrc.
DEFAULT_VERSION=3.11
# Backs bare `python3t`. Must be a line listed in FREETHREADED. Kept off 3.15
# so a release candidate is never the default free-threaded interpreter.
DEFAULT_FREETHREADED=3.14
# PyPy has no python.org installer or PSF signature; installed from a tarball
# pinned to its published sha256 instead. Update both when bumping a version:
# https://www.pypy.org/checksums.html
PYPY_VERSIONS=(
"pypy3.9:7.3.16:88f824e7a2d676440d09bc90fc959ae0fd3557d7e2f14bfbbe53d41d159a47fe"
"pypy3.11:7.3.23:4747b3aceba4c1c6104cddc0fe5ea302101d32955f0957347b9ecc4fbd7aed05"
)
PYPY_PREFIX=/usr/local/lib
# python.org installers have been signed by either the PSF org account or,
# for older 3.9/3.10 releases, release manager Ned Deily individually.
PSF_TEAM_IDS=("BMM5U3QVKW" "DJ3H93M7VJ")
FORCE=0
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--force) FORCE=1 ;;
--dry-run) DRY_RUN=1 ;;
-h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $arg" >&2; exit 2 ;;
esac
done
[ "$(uname -s)" = "Darwin" ] || { echo "error: macOS only" >&2; exit 1; }
run() { if [ "$DRY_RUN" = 1 ]; then echo " would run: $*"; else "$@"; fi; }
is_freethreaded() {
local v="$1" f
for f in "${FREETHREADED[@]}"; do [ "$f" = "$v" ] && return 0; done
return 1
}
# True if $1 >= $2 as a version string. Never downgrade an interpreter someone
# installed by hand ahead of what this script currently pins.
version_ge() {
[ "$1" = "$2" ] && return 0
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ]
}
WORKDIR=$(mktemp -d -t setup-python)
trap 'rm -rf "$WORKDIR"' EXIT
echo "==> Installing python.org CPython"
INSTALLED_ANY=0
for entry in "${VERSIONS[@]}"; do
series="${entry%%:*}"; full="${entry##*:}"
if [ "$FORCE" = 0 ] && [ -x "/Library/Frameworks/Python.framework/Versions/$series/bin/python3" ]; then
have=$("/Library/Frameworks/Python.framework/Versions/$series/bin/python3" -c 'import platform; print(platform.python_version())' 2>/dev/null || echo "?")
if [ "$have" != "?" ] && version_ge "$have" "$full"; then
echo " $series: $have already installed (>= $full)"
continue
fi
echo " $series: found $have, want $full"
fi
# Release-candidate installers live under the final version's directory.
dir=$(echo "$full" | sed -E 's/(a|b|rc)[0-9]+$//')
pkg="python-$full-macos11.pkg"
url="https://www.python.org/ftp/python/$dir/$pkg"
echo " $series: downloading $full"
run curl -fsSL -o "$WORKDIR/$pkg" "$url"
if [ "$DRY_RUN" = 0 ]; then
sig=$(pkgutil --check-signature "$WORKDIR/$pkg")
ok=0
for team in "${PSF_TEAM_IDS[@]}"; do
echo "$sig" | grep -q "($team)" && { ok=1; break; }
done
[ "$ok" = 1 ] || { echo "error: $pkg is not signed by a trusted release manager" >&2; exit 1; }
fi
# Skip IDLE and the docs. Skip the shell profile updater too: every installer
# prepends its own framework bin to ~/.zprofile, so whichever ran last would
# silently own bare `python3`. Versioned /usr/local/bin links do the job instead.
ft=0; is_freethreaded "$series" && ft=1
{
echo '<?xml version="1.0" encoding="UTF-8"?>'
echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
echo '<plist version="1.0"><array>'
for c in "PythonFramework:1" "PythonUnixTools:1" "PythonInstallPip:1" \
"PythonApplications:0" "PythonDocumentation:0" "PythonProfileChanges:0" \
"PythonTFramework:$ft"; do
printf '<dict><key>choiceIdentifier</key><string>org.python.Python.%s-%s</string><key>choiceAttribute</key><string>selected</string><key>attributeSetting</key><integer>%s</integer></dict>\n' \
"${c%%:*}" "$series" "${c##*:}"
done
echo '</array></plist>'
} > "$WORKDIR/choices-$series.xml"
echo " $series: installing (needs sudo)"
run sudo installer -pkg "$WORKDIR/$pkg" -applyChoiceChangesXML "$WORKDIR/choices-$series.xml" -target /
INSTALLED_ANY=1
certs="/Applications/Python $series/Install Certificates.command"
[ -f "$certs" ] && run bash "$certs" >/dev/null 2>&1 || true
done
echo "==> Installing PyPy"
arch=$(uname -m)
for entry in "${PYPY_VERSIONS[@]}"; do
IFS=: read -r name version sha256 <<< "$entry"
dest="$PYPY_PREFIX/$name-v$version-macos_$arch"
if [ "$FORCE" = 0 ]; then
have=$(ls -d "$PYPY_PREFIX/$name-v"*"-macos_$arch" 2>/dev/null \
| sed -E "s#.*/$name-v(.*)-macos_$arch#\1#" | sort -V | tail -1)
if [ -n "$have" ] && [ -x "$PYPY_PREFIX/$name-v$have-macos_$arch/bin/$name" ] && version_ge "$have" "$version"; then
echo " $name: $have already installed (>= $version)"
continue
fi
fi
archive="$name-v$version-macos_$arch.tar.bz2"
url="https://downloads.python.org/pypy/$archive"
echo " $name: downloading $version"
run curl -fsSL -o "$WORKDIR/$archive" "$url"
if [ "$DRY_RUN" = 0 ]; then
got=$(shasum -a 256 "$WORKDIR/$archive" | awk '{print $1}')
[ "$got" = "$sha256" ] || { echo "error: $archive checksum mismatch (got $got, want $sha256)" >&2; exit 1; }
fi
echo " $name: installing to $dest (needs sudo)"
run sudo mkdir -p "$PYPY_PREFIX"
run sudo rm -rf "$dest"
run sudo tar -xjf "$WORKDIR/$archive" -C "$PYPY_PREFIX"
run sudo ln -sfn "$dest/bin/$name" "/usr/local/bin/$name"
INSTALLED_ANY=1
done
# The UNIX tools component links python3.Xt whether or not the free-threaded
# framework was installed, leaving broken symlinks on the lines that skip it.
echo "==> Cleaning broken symlinks"
for f in /usr/local/bin/python3*; do
if [ -L "$f" ] && [ ! -e "$f" ]; then
echo " removing $f"
run sudo rm -f "$f"
fi
done
# Each installer claims the unversioned links, so the last one to run owns them.
# Repoint at the chosen defaults instead.
link_default() {
local framework="$1" version="$2" name
shift 2
echo "==> Pointing /usr/local/bin/$1 at $version"
for name in "$@"; do
local src="/Library/Frameworks/$framework.framework/Versions/$version/bin/$name"
if [ -e "$src" ]; then
run sudo ln -sfn "../../../Library/Frameworks/$framework.framework/Versions/$version/bin/$name" "/usr/local/bin/$name"
else
echo " skipping $name, $src not found"
fi
done
}
link_default Python "$DEFAULT_VERSION" python3 python3-config
link_default PythonT "$DEFAULT_FREETHREADED" python3t python3t-config
echo "==> Configuring uv"
if command -v uv >/dev/null 2>&1; then
if [ "$DRY_RUN" = 0 ]; then
mkdir -p ~/.config/uv
cat > ~/.config/uv/uv.toml <<'UVTOML'
# Use only interpreters already installed on this machine, and never download a
# managed one. Without this, `uv run` and `uv venv` silently pull their own
# CPython and projects end up pinned to interpreters nothing else can see.
python-preference = "only-system"
python-downloads = "never"
UVTOML
echo " wrote ~/.config/uv/uv.toml"
else
echo " would write ~/.config/uv/uv.toml"
fi
# Match on the full cpython- key. A bare `uv python uninstall 3.13` also
# matches pyodide-3.13.x and removes it.
managed=$(ls -1 "$(uv python dir 2>/dev/null)" 2>/dev/null | grep '^cpython-' || true)
if [ -n "$managed" ]; then
echo " removing uv-managed CPython:"
echo "$managed" | sed 's/^/ /'
while read -r key; do
[ -n "$key" ] && run uv python uninstall "$key"
done <<< "$managed"
else
echo " no uv-managed CPython to remove"
fi
# Tools built against a removed interpreter keep a dangling venv.
for tool in $(ls -1 "$(uv tool dir 2>/dev/null)" 2>/dev/null || true); do
cfg="$(uv tool dir)/$tool/pyvenv.cfg"
[ -f "$cfg" ] || continue
home=$(awk -F' = ' '/^home/{print $2}' "$cfg")
if [ ! -x "$home/python3" ] && [ ! -x "$home/python" ]; then
echo " rebuilding tool: $tool"
run uv tool install --reinstall --force "$tool"
fi
done
else
echo " uv not installed, skipping"
fi
echo
echo "==> Installed"
for entry in "${VERSIONS[@]}"; do
series="${entry%%:*}"
bin="/usr/local/bin/python3.$(echo "$series" | cut -d. -f2)"
[ -x "$bin" ] && printf ' %-28s %s\n' "$bin" "$("$bin" -V 2>&1)"
t="${bin}t"
[ -x "$t" ] && printf ' %-28s %s (GIL: %s)\n' "$t" "$("$t" -V 2>&1)" "$("$t" -c 'import sys; print(sys._is_gil_enabled())' 2>&1)"
done
for entry in "${PYPY_VERSIONS[@]}"; do
name="${entry%%:*}"
bin="/usr/local/bin/$name"
[ -x "$bin" ] && printf ' %-28s %s\n' "$bin" "$("$bin" -V 2>&1)"
done
# bashrc puts the default framework's bin ahead of Homebrew, and only an
# interactive shell sources it, so resolve the way a login shell would.
resolved=$(zsh -ic 'command -v python3' 2>/dev/null || command -v python3)
echo
printf ' python3 resolves to %s (%s)\n' "$resolved" "$("$resolved" -V 2>&1)"
case "$resolved" in
"/Library/Frameworks/Python.framework/Versions/$DEFAULT_VERSION/"*) ;;
*) echo " warning: expected the $DEFAULT_VERSION framework. Check that bashrc prepends"
echo " /Library/Frameworks/Python.framework/Versions/$DEFAULT_VERSION/bin to PATH." ;;
esac
if [ "$INSTALLED_ANY" = 1 ]; then
echo " Open a new shell to pick up PATH changes."
fi