11#! /usr/bin/env bash
22
33# -----------------------------------------------------------------------------
4- # install.bash -- v1.0 .0
4+ # install.bash -- v1.1 .0
55# https://github.com/newbery/uv-python-shims
66#
77# Copies the project-root ./python shim to:
88# ~/.local/uv-python-shims/python
99#
10- # Then creates symlinks :
10+ # Then creates shims for "major.minor" versions :
1111# python3, python3.9, python3.10, ... python3.14
1212#
13- # Controls:
14- # DEST_DIR=... change install directory
15- # PAUSE=0.2 slow down / speed up (seconds). Use PAUSE=0 to disable.
13+ # Safety:
14+ # A manifest file is written on each successful run:
15+ # ${DEST_DIR}/.uv-python-shim-manifest
16+ #
17+ # On the next run, if DEST_DIR contains any items NOT listed in the manifest,
18+ # the install will warn and exit (to avoid deleting user files).
19+ #
20+ # Control via environmment vars:
21+ # PAUSE=0.2 : slow down / speed up (seconds). Use PAUSE=0 to disable.
22+ # DEST_DIR=... : change install directory
23+ # SHIM_MODE=copy : set major.minor shim mode (copy|symlink|wrapper)
24+ # MINOR_VERSIONS="13 14" : set major.minor versions to be created
25+ # UV_PYTHON_SHIMS_FORCE=1 : bypass safety checks and wipe DEST_DIR contents
26+ #
27+ # Default values:
28+ # PAUSE=0.08
29+ # DEST_DIR=$HOME/.local/uv-python-shims
30+ # SHIM_MODE=symlink
31+ # MINOR_VERSIONS="9 10 11 12 13 14"
1632# -----------------------------------------------------------------------------
1733
1834set -euo pipefail
1935
20- DEST_DIR=" ${DEST_DIR:- $HOME / .local/ uv-python-shims} "
2136PAUSE=" ${PAUSE:- 0.08} "
37+ SHIM_MODE=" ${SHIM_MODE:- symlink} "
2238MINOR_VERSIONS=" ${MINOR_VERSIONS:- 9 10 11 12 13 14} "
39+ UV_PYTHON_SHIMS_FORCE=" ${UV_PYTHON_SHIMS_FORCE:- } "
40+
41+ SRC_DIR=" $( cd -- " $( dirname -- " ${BASH_SOURCE[0]} " ) " && pwd) "
42+ DEST_DIR=" ${DEST_DIR:- $HOME / .local/ uv-python-shims} "
2343
24- SCRIPT_DIR=" $( cd -- " $( dirname -- " ${BASH_SOURCE[0]} " ) " && pwd) "
25- SRC_SHIM=" ${SCRIPT_DIR} /python"
44+ SRC_SHIM=" ${SRC_DIR} /python"
2645DEST_SHIM=" ${DEST_DIR} /python"
46+ MANIFEST=" ${DEST_DIR} /.uv-python-shim-manifest"
2747
2848pause () {
2949 [[ " ${PAUSE} " == " 0" || " ${PAUSE} " == " 0.0" ]] && return 0
@@ -38,6 +58,41 @@ run_step() {
3858 pause
3959}
4060
61+ create_copy () {
62+ local name=" $1 "
63+ local path=" ${DEST_DIR} /${name} "
64+ command cp -f -- " ${SRC_SHIM} " " ${path} "
65+ chmod 0755 " ${path} "
66+ }
67+
68+ create_symlink () {
69+ local name=" $1 "
70+ command ln -sfn " python" " ${DEST_DIR} /${name} "
71+ }
72+
73+ create_wrapper () {
74+ local name=" $1 "
75+ local path=" ${DEST_DIR} /${name} "
76+ command cat > " ${path} " << 'EOF '
77+ #!/usr/bin/env bash
78+ # generated by uv-python-shim installer
79+ UV_PYTHON_SHIM_INVOKED_AS="$(basename "$0")"
80+ export UV_PYTHON_SHIM_INVOKED_AS
81+ exec "$(dirname "$0")/python" "$@"
82+ EOF
83+ chmod 0755 " ${path} "
84+ }
85+
86+ create_shim () {
87+ local name=" $1 "
88+ case " ${SHIM_MODE} " in
89+ copy) create_copy " ${name} " ;;
90+ symlink) create_symlink " ${name} " ;;
91+ wrapper) create_wrapper " ${name} " ;;
92+ esac
93+ }
94+
95+
4196# ----------------------------
4297# Some simple color setup
4398# ----------------------------
@@ -53,39 +108,157 @@ else
53108 C_RESET=" " ; C_BOLD=" " ; C_GREEN=" " ; C_YELLOW=" " ; C_RED=" " ; C_CYAN=" "
54109fi
55110
56- info () { printf " %s==>%s %s\n" " ${C_CYAN}${C_BOLD} " " ${C_RESET} " " $* " ; }
57- ok () { printf " %s✔%s %s\n" " ${C_GREEN} " " ${C_RESET} " " $* " ; }
58- warn () { printf " %s!%s %s\n" " ${C_YELLOW} " " ${C_RESET} " " $* " ; }
59- err () { printf " %sERROR:%s %s\n" " ${C_RED}${C_BOLD} " " ${C_RESET} " " $* " >&2 ; }
111+ info () { printf ' %s==>%s %s\n' " ${C_CYAN}${C_BOLD} " " ${C_RESET} " " $* " ; }
112+ ok () { printf ' %s✔%s %s\n' " ${C_GREEN} " " ${C_RESET} " " $* " ; }
113+ warn () { printf ' %s!%s %s\n' " ${C_YELLOW} " " ${C_RESET} " " $* " ; }
114+ err () { printf ' %sERROR:%s %s\n' " ${C_RED}${C_BOLD} " " ${C_RESET} " " $* " >&2 ; }
115+
116+
117+ # ----------------------------
118+ # Manifest safety helpers
119+ # ----------------------------
120+
121+ is_truthy () {
122+ case " ${1:- } " in
123+ 1|true|TRUE|yes|YES|y|Y|on|ON) return 0 ;;
124+ * ) return 1 ;;
125+ esac
126+ }
127+
128+ check_denylist () {
129+ # Refuse to wipe obviously dangerous paths (even with force).
130+ case " ${DEST_DIR} " in
131+ " " |" /" |" $HOME " |" $HOME /" |" $HOME /.local" |" $HOME /.local/" |" $HOME /.local/bin" |" $HOME /bin" )
132+ err " Refusing to wipe dangerous DEST_DIR: ${DEST_DIR} "
133+ err " Choose a dedicated directory (default is fine): \$ HOME/.local/uv-python-shims"
134+ exit 1
135+ ;;
136+ esac
137+ }
138+
139+ list_items () {
140+ local p
141+ shopt -s dotglob nullglob
142+ for p in " ${DEST_DIR} " /* ; do
143+ [[ -e " ${p} " ]] || continue
144+ printf ' %s\n' " ${p# " ${DEST_DIR} " / } "
145+ done
146+ }
147+
148+ write_manifest () {
149+ local tmp
150+ tmp=" $( mktemp " ${DEST_DIR} /.uv-python-shim-manifest.tmp.XXXXXX" ) "
151+ installed=( " python" " python3" )
152+ for minor in ${MINOR_VERSIONS} ; do
153+ installed+=( " python3.${minor} " )
154+ done
155+ installed+=( " .uv-python-shim-manifest" )
156+ printf ' %s\n' " ${installed[@]} " > " $tmp "
157+ command mv -f -- " ${tmp} " " ${MANIFEST} "
158+ }
159+
160+ load_manifest () {
161+ if [[ ! -f " ${MANIFEST} " ]]; then
162+ warn " Refusing to modify non-empty directory without manifest: ${DEST_DIR} "
163+ warn " To proceed (and wipe it), run with: UV_PYTHON_SHIMS_FORCE=1 bash ./install.bash"
164+ exit 1
165+ fi
166+ mapfile -t manifest_items < " ${MANIFEST} "
167+ }
168+
169+ validate_manifest () {
170+ declare -A allowed=()
171+ local item
172+
173+ for item in " ${manifest_items[@]} " ; do
174+ [[ -n " ${item} " ]] || continue
175+ allowed[" ${item} " ]=1
176+ done
177+
178+ while IFS= read -r item; do
179+ if [[ -z " ${allowed[${item}]:- } " ]]; then
180+ warn " Refusing to modify ${DEST_DIR} : unexpected item not in manifest: ${item} "
181+ warn " If this directory is dedicated to uv-python-shims:"
182+ warn " - remove the unexpected files, OR"
183+ warn " - set UV_PYTHON_SHIMS_FORCE=1 to wipe the directory"
184+ exit 1
185+ fi
186+ done < <( list_items)
187+ }
188+
189+ wipe_previous_install () {
190+ if [[ ! -d " ${DEST_DIR} " ]]; then
191+ return 0
192+ fi
193+
194+ # If user opts into force, just wipe DEST_DIR and skip the rest
195+ if is_truthy " ${UV_PYTHON_SHIMS_FORCE} " ; then
196+ check_denylist
197+ warn " UV_PYTHON_SHIMS_FORCE=1 set; wiping ${DEST_DIR} "
198+ command rm -rf -- " ${DEST_DIR} "
199+ printf ' \n'
200+ return 0
201+ fi
202+
203+ # If DEST_DIR is already empty, just skip the rest
204+ local items=()
205+ shopt -s dotglob nullglob
206+ items=( " ${DEST_DIR} " /* )
207+ if (( ${# items[@]} == 0 )) ; then
208+ return 0
209+ fi
210+
211+ # Now load and validate manifest
212+ load_manifest
213+ validate_manifest
214+
215+ # If we reach here, it's safe to remove all items listed in the manifest
216+ local rel
217+ for rel in " ${manifest_items[@]} " ; do
218+ [[ -n " ${rel} " ]] || continue
219+ command rm -f -- " ${DEST_DIR} /${rel} "
220+ done
221+ }
60222
61223
62224# ----------------------------
63225# Do the work...
64226# ----------------------------
65227
66228info " Install uv python shims"
67- printf " Source: %s\n" " ${SRC_SHIM} "
68- printf " Destination: %s\n\n" " ${DEST_SHIM} "
229+ printf ' Source: %s\n' " ${SRC_SHIM} "
230+ printf ' Destination: %s\n\n' " ${DEST_SHIM} "
69231
70232if [[ ! -f " ${SRC_SHIM} " ]]; then
71- err " Shim not found at: ${SRC_SHIM} "
233+ err " Source shim not found at: ${SRC_SHIM} "
72234 err " Expected a file named 'python' in the project root next to install.bash"
73235 exit 1
74236fi
75237
238+ case " ${SHIM_MODE} " in symlink|wrapper|copy) ;;
239+ * )
240+ err " Invalid shim-mode: ${SHIM_MODE} "
241+ err " Allowed values: symlink|wrapper|copy"
242+ exit 1
243+ ;;
244+ esac
245+
246+ wipe_previous_install
247+
76248info " Create main shim"
77249run_step " Create directory ${DEST_DIR} " mkdir -p " ${DEST_DIR} "
78- run_step " Copy shim to ${DEST_SHIM} " cp " ${SRC_SHIM} " " ${DEST_SHIM} "
79- run_step " Set executable bit on ${DEST_SHIM} " chmod 0755 " ${DEST_SHIM} "
80- printf " \n"
250+ run_step " Copy shim to ${DEST_SHIM} " create_copy " python"
251+ printf ' \n'
81252
82- info " Create symlinks "
83- run_step " Link ${DEST_DIR} /python3 -> python " ln -sfn " python " " ${DEST_DIR} / python3"
253+ info " Create major.minor shims (SHIM_MODE= ${SHIM_MODE} ) "
254+ run_step " Create ${DEST_DIR} /python3" create_shim " python3"
84255for minor in ${MINOR_VERSIONS} ; do
85- run_step " Link ${DEST_DIR} /python3.${minor} -> python " ln -sfn " python " " ${DEST_DIR} / python3.${minor} "
256+ run_step " Create ${DEST_DIR} /python3.${minor} " create_shim " python3.${minor} "
86257done
87258
88- printf " \n"
259+ write_manifest
260+
261+ printf ' \n'
89262info " Done"
90263
91264cat << EOF
@@ -110,8 +283,11 @@ ${C_BOLD}Verify result${C_RESET}
110283 python3.11 --version
111284
112285${C_BOLD} Install can be customized${C_RESET}
113- PAUSE=0 bash ./install.bash
114- PAUSE=0.25 bash ./install.bash
115- DEST_DIR=... bash ./install.bash
116-
286+ PAUSE=0 bash ./install.bash
287+ PAUSE=0.25 bash ./install.bash
288+ DEST_DIR=... bash ./install.bash
289+ SHIM_MODE=copy bash ./install.bash
290+ SHIM_MODE=wrapper bash ./install.bash
291+ MINOR_VERSIONS="13 14" bash ./install.bash
292+ UV_PYTHON_SHIMS_FORCE=1 bash ./install.bash
117293EOF
0 commit comments