Skip to content

Commit 5ba0a96

Browse files
authored
feat: uvx parity + --env-prefix shorthand (#4)
* feat: expose uvx with full parity to uv Extract uvx binary from the uv release archive alongside uv, create bin/uvx wrappers/symlinks, export $REDMATTER_UVX via env.sh/env.ps1/env.bat, and record uvx_env in the installed distro.toml. Update README and TESTING docs. * feat: generate env.sh on Windows for Git Bash compatibility * feat: add --env-prefix option to derive env var names from a common prefix Callers can now pass --env-prefix REDMATTER instead of spelling out --uv-env REDMATTER_UV --uvx-env REDMATTER_UVX --python-env REDMATTER_PYTHON. The two styles are mutually exclusive; mixing them or omitting both produces a clear error. Existing invocations using the individual flags are unchanged. * docs/fix: address PR review — uvx null guard, install.sh comment, README env-prefix examples --------- Co-authored-by: Dino Korah <691011+codemedic@users.noreply.github.com>
1 parent f83b415 commit 5ba0a96

5 files changed

Lines changed: 151 additions & 51 deletions

File tree

README.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ Expand-Archive managed-python.zip
4141
./install.sh \
4242
--prefix ~/.local/redmatter/python \
4343
--python 3.10 \
44-
--uv-env REDMATTER_UV \
45-
--python-env REDMATTER_PYTHON
44+
--env-prefix REDMATTER
4645

4746
source ~/.local/redmatter/python/env.sh
4847
```
@@ -53,8 +52,7 @@ source ~/.local/redmatter/python/env.sh
5352
.\install.ps1 `
5453
-Prefix "$env:USERPROFILE\.local\redmatter\python" `
5554
-Python "3.10" `
56-
-UvEnv "REDMATTER_UV" `
57-
-PythonEnv "REDMATTER_PYTHON"
55+
-EnvPrefix "REDMATTER"
5856
5957
. "$env:USERPROFILE\.local\redmatter\python\env.ps1"
6058
```
@@ -66,14 +64,14 @@ source ~/.local/redmatter/python/env.sh
6664
> **PowerShell** — evaluate `env.ps1` as a string (bypasses script execution policy):
6765
>
6866
> ```powershell
69-
> install.bat -Prefix "$env:USERPROFILE\.local\redmatter\python" -Python "3.10" -UvEnv "REDMATTER_UV" -PythonEnv "REDMATTER_PYTHON"
67+
> install.bat -Prefix "$env:USERPROFILE\.local\redmatter\python" -Python "3.10" -EnvPrefix "REDMATTER"
7068
> Invoke-Expression (Get-Content "$env:USERPROFILE\.local\redmatter\python\env.ps1" -Raw)
7169
> ```
7270
>
7371
> **CMD** — use `call` to load `env.bat` into the current session:
7472
>
7573
> ```bat
76-
> install.bat -Prefix "%USERPROFILE%\.local\redmatter\python" -Python "3.10" -UvEnv "REDMATTER_UV" -PythonEnv "REDMATTER_PYTHON"
74+
> install.bat -Prefix "%USERPROFILE%\.local\redmatter\python" -Python "3.10" -EnvPrefix "REDMATTER"
7775
> call "%USERPROFILE%\.local\redmatter\python\env.bat"
7876
> ```
7977
>
@@ -86,8 +84,7 @@ source ~/.local/redmatter/python/env.sh
8684
./install.sh \
8785
--prefix ~/.local/redmatter/python \
8886
--python 3.10 \
89-
--uv-env REDMATTER_UV \
90-
--python-env REDMATTER_PYTHON \
87+
--env-prefix REDMATTER \
9188
--quiet
9289
9390
source ~/.local/redmatter/python/env.sh
@@ -100,12 +97,16 @@ source ~/.local/redmatter/python/env.sh
10097
|------|----------|---------|
10198
| `--prefix PATH` | yes | Install location |
10299
| `--python X.Y` | yes | Python version for venv. Default mode: prefer matching system Python, fall back to uv-managed. Isolated mode: always uv-managed. |
103-
| `--uv-env NAME` | yes | Env var name for the uv binary path |
104-
| `--python-env NAME` | yes | Env var name for the python binary path |
100+
| `--env-prefix NAME` | yes* | Derives `NAME_UV`, `NAME_UVX`, and `NAME_PYTHON` — preferred over the three flags below |
101+
| `--uv-env NAME` | yes* | Env var name for the uv binary path |
102+
| `--uvx-env NAME` | yes* | Env var name for the uvx binary path |
103+
| `--python-env NAME` | yes* | Env var name for the python binary path |
105104
| `--isolated` | no | Force uv-managed Python (ignores system Python); always adds `bin/` to PATH |
106105
| `--shell-profile` | no | Append `source <prefix>/env.sh` to shell rc |
107106
| `--quiet` / `-q` | no | Suppress all output except warnings |
108107

108+
\* Use either `--env-prefix` **or** all three of `--uv-env` / `--uvx-env` / `--python-env` — they are mutually exclusive.
109+
109110
> [!NOTE]
110111
> **Choosing a mode:** Use the default on developer machines where a system Python already exists. Use `--isolated` in CI, containers, or shared servers where you need a fully reproducible environment independent of whatever Python is (or isn't) installed on the host.
111112
@@ -118,6 +119,9 @@ source ~/.local/redmatter/python/env.sh
118119
# Run a script with dependencies (pyproject.toml in app dir)
119120
"$REDMATTER_UV" run --project /path/to/app my-script.py
120121

122+
# Run a tool without installing it
123+
"$REDMATTER_UVX" ruff --version
124+
121125
# Install a package into the shared venv (use sparingly)
122126
"$REDMATTER_UV" pip install --python "$REDMATTER_PYTHON" some-package
123127

@@ -131,11 +135,14 @@ exec "$PYTHON" script.py
131135
```text
132136
<prefix>/
133137
uv # uv binary
138+
uvx # uvx binary
134139
bin/
135140
python -> ../venv/bin/python # symlink (Linux/macOS)
136141
uv -> ../uv # symlink (Linux/macOS)
142+
uvx -> ../uvx # symlink (Linux/macOS)
137143
python.cmd # wrapper (Windows)
138144
uv.cmd # wrapper (Windows)
145+
uvx.cmd # wrapper (Windows)
139146
venv/
140147
bin/python # Linux/macOS
141148
Scripts/python.exe # Windows
@@ -158,6 +165,7 @@ uv_version = "0.10.6"
158165
prefix = "/home/user/.local/redmatter/python"
159166
python = "3.10"
160167
uv_env = "REDMATTER_UV"
168+
uvx_env = "REDMATTER_UVX"
161169
python_env = "REDMATTER_PYTHON"
162170
shell_profile = false
163171
isolated = false

TESTING.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ rm -rf /tmp/mp-test
1616

1717
```bash
1818
./install.sh --prefix /tmp/mp-test --python 3.10 \
19-
--uv-env TEST_UV --python-env TEST_PYTHON
19+
--uv-env TEST_UV --uvx-env TEST_UVX --python-env TEST_PYTHON
2020
```
2121

2222
**Expect:**
2323

2424
- uv downloaded (or skipped if already current)
2525
- venv created using system Python if a matching version exists, otherwise uv-managed
26-
- `distro.toml` contains `isolated = false`
27-
- `env.sh` contains `export PATH=...` (system python found → shadow warning; no system python → silently added)
26+
- `distro.toml` contains `isolated = false` and `uvx_env = "TEST_UVX"`
27+
- `env.sh` contains `export TEST_UVX=...` and `export PATH=...` (system python found → shadow warning; no system python → silently added)
2828

2929
```bash
30-
grep -A7 '^\[install\]' /tmp/mp-test/distro.toml
30+
grep -A8 '^\[install\]' /tmp/mp-test/distro.toml
3131
cat /tmp/mp-test/env.sh
3232
```
3333

@@ -36,7 +36,7 @@ cat /tmp/mp-test/env.sh
3636
```bash
3737
rm -rf /tmp/mp-test
3838
./install.sh --prefix /tmp/mp-test --python 3.10 \
39-
--uv-env TEST_UV --python-env TEST_PYTHON --isolated
39+
--uv-env TEST_UV --uvx-env TEST_UVX --python-env TEST_PYTHON --isolated
4040
```
4141

4242
**Expect:**
@@ -47,7 +47,7 @@ rm -rf /tmp/mp-test
4747
- `venv/bin/python` is a uv-managed build (not `/usr/bin/python*`)
4848

4949
```bash
50-
grep -A7 '^\[install\]' /tmp/mp-test/distro.toml
50+
grep -A8 '^\[install\]' /tmp/mp-test/distro.toml
5151
cat /tmp/mp-test/env.sh
5252
/tmp/mp-test/venv/bin/python --version
5353
```
@@ -56,7 +56,7 @@ cat /tmp/mp-test/env.sh
5656

5757
```bash
5858
./install.sh --prefix /tmp/mp-test --python 3.10 \
59-
--uv-env TEST_UV --python-env TEST_PYTHON --isolated
59+
--uv-env TEST_UV --uvx-env TEST_UVX --python-env TEST_PYTHON --isolated
6060
```
6161

6262
**Expect:**
@@ -69,7 +69,7 @@ cat /tmp/mp-test/env.sh
6969

7070
```bash
7171
./install.sh --prefix /tmp/mp-test --min-python 3.10 \
72-
--uv-env TEST_UV --python-env TEST_PYTHON
72+
--uv-env TEST_UV --uvx-env TEST_UVX --python-env TEST_PYTHON
7373
```
7474

7575
**Expect:** exits with `ERROR: --python is required`
@@ -90,19 +90,19 @@ Remove-Item -Recurse -Force C:\Users\Quickemu\temp\mp-test -ErrorAction Silently
9090

9191
```powershell
9292
.\install.ps1 -Prefix "C:\Users\Quickemu\temp\mp-test" -Python "3.10" `
93-
-UvEnv "TEST_UV" -PythonEnv "TEST_PYTHON"
93+
-UvEnv "TEST_UV" -UvxEnv "TEST_UVX" -PythonEnv "TEST_PYTHON"
9494
Get-Content C:\Users\Quickemu\temp\mp-test\distro.toml
9595
Get-Content C:\Users\Quickemu\temp\mp-test\env.ps1
9696
```
9797

98-
**Expect:** `isolated = false` in distro.toml; PATH added only if no system python/uv found.
98+
**Expect:** `isolated = false` and `uvx_env = "TEST_UVX"` in distro.toml; PATH added only if no system python/uv found.
9999

100100
### Test 2 — Isolated
101101

102102
```powershell
103103
Remove-Item -Recurse -Force C:\Users\Quickemu\temp\mp-test
104104
.\install.ps1 -Prefix "C:\Users\Quickemu\temp\mp-test" -Python "3.10" `
105-
-UvEnv "TEST_UV" -PythonEnv "TEST_PYTHON" -Isolated
105+
-UvEnv "TEST_UV" -UvxEnv "TEST_UVX" -PythonEnv "TEST_PYTHON" -Isolated
106106
Get-Content C:\Users\Quickemu\temp\mp-test\distro.toml
107107
Get-Content C:\Users\Quickemu\temp\mp-test\env.ps1
108108
& "C:\Users\Quickemu\temp\mp-test\venv\Scripts\python.exe" --version
@@ -114,7 +114,7 @@ Get-Content C:\Users\Quickemu\temp\mp-test\env.ps1
114114

115115
```powershell
116116
.\install.ps1 -Prefix "C:\Users\Quickemu\temp\mp-test" -Python "3.10" `
117-
-UvEnv "TEST_UV" -PythonEnv "TEST_PYTHON" -Isolated
117+
-UvEnv "TEST_UV" -UvxEnv "TEST_UVX" -PythonEnv "TEST_PYTHON" -Isolated
118118
```
119119

120120
**Expect:** uv and venv skipped; env files regenerated.
@@ -123,7 +123,7 @@ Get-Content C:\Users\Quickemu\temp\mp-test\env.ps1
123123

124124
```powershell
125125
.\install.ps1 -Prefix "C:\Users\Quickemu\temp\mp-test" -MinPython "3.10" `
126-
-UvEnv "TEST_UV" -PythonEnv "TEST_PYTHON"
126+
-UvEnv "TEST_UV" -UvxEnv "TEST_UVX" -PythonEnv "TEST_PYTHON"
127127
```
128128

129129
**Expect:** PowerShell parameter binding error — `-MinPython` is not a recognised parameter.

install.ps1

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,41 @@
88
All configuration (env.ps1, env.sh, bin\ wrappers, shell profile) is
99
handled by setup.py.
1010
11+
.EXAMPLE
12+
.\install.ps1 -Prefix "$env:USERPROFILE\.local\redmatter\python" -Python "3.10" -EnvPrefix "REDMATTER"
13+
1114
.EXAMPLE
1215
.\install.ps1 -Prefix "$env:USERPROFILE\.local\redmatter\python" `
13-
-Python "3.10" -UvEnv "REDMATTER_UV" -PythonEnv "REDMATTER_PYTHON"
16+
-Python "3.10" -UvEnv "REDMATTER_UV" -UvxEnv "REDMATTER_UVX" -PythonEnv "REDMATTER_PYTHON"
1417
#>
1518
param(
1619
[Parameter(Mandatory=$true)] [string]$Prefix,
1720
[Parameter(Mandatory=$true)] [string]$Python,
18-
[Parameter(Mandatory=$true)] [string]$UvEnv,
19-
[Parameter(Mandatory=$true)] [string]$PythonEnv,
21+
[Parameter(Mandatory=$false)] [string]$EnvPrefix,
22+
[Parameter(Mandatory=$false)] [string]$UvEnv,
23+
[Parameter(Mandatory=$false)] [string]$UvxEnv,
24+
[Parameter(Mandatory=$false)] [string]$PythonEnv,
2025
[switch]$ShellProfile,
2126
[switch]$Quiet,
2227
[switch]$Isolated
2328
)
2429

30+
if ($EnvPrefix) {
31+
if ($UvEnv -or $UvxEnv -or $PythonEnv) {
32+
Write-Error "-EnvPrefix cannot be combined with -UvEnv, -UvxEnv, or -PythonEnv"
33+
exit 1
34+
}
35+
} else {
36+
$missing = @()
37+
if (-not $UvEnv) { $missing += "-UvEnv" }
38+
if (-not $UvxEnv) { $missing += "-UvxEnv" }
39+
if (-not $PythonEnv) { $missing += "-PythonEnv" }
40+
if ($missing.Count -gt 0) {
41+
Write-Error "The following parameters are required: $($missing -join ', ') (or use -EnvPrefix)"
42+
exit 1
43+
}
44+
}
45+
2546
function Write-Msg($msg) { if (-not $Quiet) { Write-Host $msg } }
2647

2748
Set-StrictMode -Version Latest
@@ -72,6 +93,9 @@ if ($currentVer -eq $UvVersion) {
7293
Expand-Archive $tmp $tmpDir -Force
7394
$uvSrc = Get-ChildItem $tmpDir -Filter "uv.exe" -Recurse | Select-Object -First 1
7495
Copy-Item $uvSrc.FullName $UvExe -Force
96+
$uvxSrc = Get-ChildItem $tmpDir -Filter "uvx.exe" -Recurse | Select-Object -First 1
97+
if (-not $uvxSrc) { Write-Error "Failed to locate uvx.exe in archive"; exit 1 }
98+
Copy-Item $uvxSrc.FullName (Join-Path $Prefix "uvx.exe") -Force
7599
} catch {
76100
Write-Error "Failed to download uv $UvVersion from $url`: $_"
77101
exit 1
@@ -108,7 +132,11 @@ if (Test-Path $VenvPy) {
108132
Write-Msg ""
109133

110134
# Hand off to setup.py
111-
$setupArgs = @("--prefix", $Prefix, "--python", $Python, "--uv-env", $UvEnv, "--python-env", $PythonEnv)
135+
if ($EnvPrefix) {
136+
$setupArgs = @("--prefix", $Prefix, "--python", $Python, "--env-prefix", $EnvPrefix)
137+
} else {
138+
$setupArgs = @("--prefix", $Prefix, "--python", $Python, "--uv-env", $UvEnv, "--uvx-env", $UvxEnv, "--python-env", $PythonEnv)
139+
}
112140
if ($ShellProfile) { $setupArgs += "--shell-profile" }
113141
if ($Isolated) { $setupArgs += "--isolated" }
114142
if ($Quiet) { $setupArgs += "--quiet" }

install.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ _bootstrap_uv() {
8888
fi
8989
cp "$uv_src" "$uv_bin"
9090
chmod +x "$uv_bin"
91+
local uvx_src
92+
uvx_src="$(find "$tmp" -name "uvx" -type f | head -1 || true)"
93+
if [[ -z "$uvx_src" || ! -f "$uvx_src" ]]; then
94+
printf "ERROR: failed to locate uvx binary in downloaded archive\n" >&2; exit 1
95+
fi
96+
cp "$uvx_src" "${prefix}/uvx"
97+
chmod +x "${prefix}/uvx"
9198
_msg " ✓ uv $uv_version installed"
9299
}
93100

@@ -146,6 +153,8 @@ main() {
146153
_bootstrap_venv "$prefix" "$min_python" "$isolated"
147154

148155
_msg ""
156+
# All flags (including --env-prefix / --uv-env / --uvx-env / --python-env) are forwarded
157+
# verbatim to setup.py, which owns env var name resolution and validation.
149158
exec "${prefix}/venv/bin/python" "${script_dir}/setup.py" "$@"
150159
}
151160

0 commit comments

Comments
 (0)