Skip to content

Commit 24d1761

Browse files
committed
v0.1.1: Add Windows installer and python -m entry point
Add install-windows.ps1 automatic installer that creates a private venv, installs the package, adds the Scripts directory to the user PATH, initializes config, and registers URI handlers. The installer handles Python discovery via py launcher and direct candidates, suggests winget when Python is missing, and pauses before closing when launched via double-click or -File. Add __main__.py so the CLI can be invoked as python -m slicer_uri_bridge when the Scripts directory is not on PATH. Bump version to 0.1.1. Update README with Windows (automatic) install section and python -m fallback in Troubleshooting. Add test for the module entry point
1 parent 0c7f49f commit 24d1761

6 files changed

Lines changed: 362 additions & 3 deletions

File tree

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ It registers URI handlers for other slicers (PrusaSlicer, OrcaSlicer, Cura, and
1212

1313
## Installation
1414

15+
### Windows (automatic)
16+
17+
Open PowerShell and run:
18+
19+
```powershell
20+
powershell -ExecutionPolicy Bypass -c "iwr -useb https://raw.githubusercontent.com/mbv06/slicer-uri-bridge/main/install-windows.ps1 | iex"
21+
```
22+
23+
The installer creates a private virtual environment in `%LOCALAPPDATA%\slicer-uri-bridge`, installs or upgrades the package there, adds the Scripts directory to the user `PATH`, initializes config if needed, and registers URI handlers.
24+
25+
After installation, open a new terminal window if the command is not found, then test the registered handler by opening a known Benchy model URI:
26+
27+
```powershell
28+
slicer-uri-bridge test
29+
```
30+
1531
### macOS (automatic)
1632

1733
Run the installer:
@@ -131,7 +147,7 @@ slicer-uri-bridge config-path
131147

132148
Log files in that directory record each handler invocation and can help diagnose download failures, URI parsing issues, or slicer launch problems.
133149

134-
If the `slicer-uri-bridge` command is not found after installation, make sure the Python scripts directory is on your `PATH`. On macOS with the automatic installer, open a new Terminal window. On Windows, ensure the `Add python.exe to PATH` option was enabled during Python installation.
150+
If the `slicer-uri-bridge` command is not found after installation, make sure the Python scripts directory is on your `PATH`. On macOS with the automatic installer, open a new Terminal window. On Windows, ensure the `Add python.exe to PATH` option was enabled during Python installation, or use the automatic installer above. As a fallback, you can always run `python -m slicer_uri_bridge` instead of `slicer-uri-bridge`.
135151

136152
If URI links do not open after registration, verify the current handler status:
137153

install-windows.ps1

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
#Requires -Version 5.1
2+
<#
3+
.SYNOPSIS
4+
Slicer URI Bridge Windows installer.
5+
6+
.DESCRIPTION
7+
1. Finds Python 3.11+
8+
2. Creates a private virtual environment
9+
3. Installs / upgrades Slicer URI Bridge into that environment
10+
4. Adds the environment Scripts directory to the user PATH
11+
5. Creates config if missing
12+
6. Registers URI handlers
13+
7. Shows how to test the registered handler
14+
15+
To update later, run this installer again.
16+
17+
.EXAMPLE
18+
powershell -ExecutionPolicy Bypass -File install-windows.ps1
19+
#>
20+
21+
Set-StrictMode -Version Latest
22+
$ErrorActionPreference = 'Stop'
23+
24+
$ProjectSpec = 'https://github.com/mbv06/slicer-uri-bridge/archive/refs/heads/main.zip'
25+
$AppHome = Join-Path $env:LOCALAPPDATA 'slicer-uri-bridge'
26+
$VenvDir = Join-Path $AppHome 'venv'
27+
$ScriptsDir = Join-Path $VenvDir 'Scripts'
28+
$BridgeExe = Join-Path $ScriptsDir 'slicer-uri-bridge.exe'
29+
$MinMajor = 3
30+
$MinMinor = 11
31+
32+
function Log($Message) {
33+
Write-Host "`n==> $Message" -ForegroundColor Cyan
34+
}
35+
36+
function Test-ShouldPauseOnExit {
37+
try {
38+
if (-not [Environment]::UserInteractive) { return $false }
39+
if ($Host.Name -ne 'ConsoleHost') { return $false }
40+
41+
$commandLine = Get-CimInstance Win32_Process -Filter "ProcessId = $PID" -ErrorAction Stop |
42+
Select-Object -ExpandProperty CommandLine
43+
if (-not $commandLine) { return $false }
44+
45+
return $commandLine -match '(?i)(^|\s)-(c|command|file|encodedcommand)\b'
46+
}
47+
catch {
48+
return $false
49+
}
50+
}
51+
52+
function Wait-BeforeExit {
53+
if (Test-ShouldPauseOnExit) {
54+
Write-Host ''
55+
Read-Host 'Press Enter to close this window' | Out-Null
56+
}
57+
}
58+
59+
function Write-SummaryLine {
60+
param(
61+
[Parameter(Mandatory = $true)]
62+
[string]$Label,
63+
64+
[Parameter(Mandatory = $true)]
65+
[string]$Value
66+
)
67+
68+
Write-Host (" {0,-12}" -f $Label) -ForegroundColor Cyan -NoNewline
69+
Write-Host " $Value"
70+
}
71+
72+
function Format-ArgumentForProcess {
73+
param(
74+
[Parameter(Mandatory = $true)]
75+
[string]$Value
76+
)
77+
78+
if ($Value -notmatch '[\s"]') {
79+
return $Value
80+
}
81+
82+
return '"' + ($Value -replace '"', '\"') + '"'
83+
}
84+
85+
function Format-CommandForDisplay {
86+
param(
87+
[Parameter(Mandatory = $true)]
88+
[string]$FilePath,
89+
90+
[Parameter()]
91+
[string[]]$Arguments = @()
92+
)
93+
94+
$parts = @($FilePath) + @($Arguments)
95+
$quotedParts = foreach ($part in $parts) {
96+
if ($part -match '[\s"]') {
97+
'"' + ($part -replace '"', '\"') + '"'
98+
}
99+
else {
100+
$part
101+
}
102+
}
103+
104+
return ($quotedParts -join ' ')
105+
}
106+
107+
function Invoke-NativeCommand {
108+
param(
109+
[Parameter(Mandatory = $true)]
110+
[string]$FilePath,
111+
112+
[Parameter()]
113+
[string[]]$Arguments = @(),
114+
115+
[Parameter()]
116+
[string]$FailureMessage = 'Command failed.'
117+
)
118+
119+
Write-Host ("Running: {0}" -f (Format-CommandForDisplay -FilePath $FilePath -Arguments $Arguments)) -ForegroundColor DarkGray
120+
121+
$stdoutPath = [System.IO.Path]::GetTempFileName()
122+
$stderrPath = [System.IO.Path]::GetTempFileName()
123+
$argumentLine = (@($Arguments) | ForEach-Object { Format-ArgumentForProcess $_ }) -join ' '
124+
125+
try {
126+
$process = Start-Process -FilePath $FilePath `
127+
-ArgumentList $argumentLine `
128+
-NoNewWindow `
129+
-Wait `
130+
-PassThru `
131+
-RedirectStandardOutput $stdoutPath `
132+
-RedirectStandardError $stderrPath
133+
134+
$stdout = if (Test-Path -LiteralPath $stdoutPath) {
135+
@(Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue)
136+
} else {
137+
@()
138+
}
139+
$stderr = if (Test-Path -LiteralPath $stderrPath) {
140+
@(Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue)
141+
} else {
142+
@()
143+
}
144+
145+
$output = @($stdout + $stderr)
146+
$exitCode = $process.ExitCode
147+
}
148+
finally {
149+
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
150+
}
151+
152+
if ($output) {
153+
foreach ($line in @($output)) {
154+
Write-Host $line
155+
}
156+
}
157+
158+
if ($exitCode -ne 0) {
159+
Die $FailureMessage
160+
}
161+
162+
return @($output)
163+
}
164+
165+
function Die($Message) {
166+
Write-Host "Error: $Message" -ForegroundColor Red
167+
Wait-BeforeExit
168+
exit 1
169+
}
170+
171+
function Test-PythonCompatible($PythonPath) {
172+
try {
173+
$version = & $PythonPath -c 'import sys; print(sys.version_info.major); print(sys.version_info.minor)' 2>$null
174+
if (-not $version) { return $false }
175+
$parts = @($version)
176+
$major = [int]$parts[0]
177+
$minor = [int]$parts[1]
178+
return ($major -gt $MinMajor) -or ($major -eq $MinMajor -and $minor -ge $MinMinor)
179+
}
180+
catch {
181+
return $false
182+
}
183+
}
184+
185+
function Find-Python {
186+
$candidates = @(
187+
'python3.14', 'python3.13', 'python3.12', 'python3.11',
188+
'python3', 'python', 'py'
189+
)
190+
191+
foreach ($candidate in $candidates) {
192+
$path = Get-Command $candidate -ErrorAction SilentlyContinue |
193+
Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue
194+
if ($path -and (Test-PythonCompatible $path)) {
195+
return $path
196+
}
197+
}
198+
199+
# Try the py launcher with version flags
200+
$pyLauncher = Get-Command 'py' -ErrorAction SilentlyContinue |
201+
Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue
202+
if ($pyLauncher) {
203+
foreach ($ver in @('-3.14', '-3.13', '-3.12', '-3.11')) {
204+
try {
205+
$check = & $pyLauncher $ver -c 'import sys; print(sys.executable)' 2>$null
206+
if ($check -and (Test-PythonCompatible $check)) {
207+
return $check
208+
}
209+
}
210+
catch {}
211+
}
212+
}
213+
214+
return $null
215+
}
216+
217+
function Add-ToUserPath($Directory) {
218+
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
219+
if (-not $userPath) { $userPath = '' }
220+
221+
$entries = $userPath.Split(';', [StringSplitOptions]::RemoveEmptyEntries)
222+
$normalized = $entries | ForEach-Object { $_.TrimEnd('\') }
223+
$target = $Directory.TrimEnd('\')
224+
225+
if ($normalized -contains $target) {
226+
return $false
227+
}
228+
229+
$newPath = if ($userPath) { "$userPath;$Directory" } else { $Directory }
230+
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
231+
232+
# Update the current session so the command is immediately available
233+
if ($env:Path -notlike "*$target*") {
234+
$env:Path = "$env:Path;$Directory"
235+
}
236+
237+
return $true
238+
}
239+
240+
function Main {
241+
Log 'Checking Python 3.11+'
242+
$python = Find-Python
243+
if (-not $python) {
244+
$winget = Get-Command 'winget' -ErrorAction SilentlyContinue |
245+
Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue
246+
if ($winget) {
247+
Die @"
248+
Python $MinMajor.$MinMinor+ was not found.
249+
250+
Install Python by running:
251+
252+
winget install Python.Python.3.12
253+
254+
Then open a new terminal window and run this installer again.
255+
"@
256+
}
257+
258+
Die @"
259+
Python $MinMajor.$MinMinor+ was not found.
260+
261+
Install Python from:
262+
https://www.python.org/downloads/windows/
263+
264+
Then open a new terminal window and run this installer again.
265+
"@
266+
}
267+
Write-Host "Using Python: $python"
268+
269+
Log 'Checking built-in venv support'
270+
Invoke-NativeCommand -FilePath $python -Arguments @('-c', 'import venv') -FailureMessage 'This Python does not support the built-in venv module. Install a full Python distribution and try again.' | Out-Null
271+
272+
Log 'Creating private Python environment'
273+
if (-not (Test-Path $AppHome)) {
274+
New-Item -ItemType Directory -Path $AppHome -Force | Out-Null
275+
}
276+
Invoke-NativeCommand -FilePath $python -Arguments @('-m', 'venv', $VenvDir) -FailureMessage 'Failed to create virtual environment.' | Out-Null
277+
278+
$venvPython = Join-Path $ScriptsDir 'python.exe'
279+
280+
Log 'Installing / upgrading Slicer URI Bridge'
281+
Invoke-NativeCommand -FilePath $venvPython -Arguments @('-m', 'pip', 'install', '--upgrade', $ProjectSpec) -FailureMessage 'pip install failed.' | Out-Null
282+
283+
Log 'Adding Scripts directory to user PATH'
284+
$added = Add-ToUserPath $ScriptsDir
285+
if ($added) {
286+
Write-Host "Added to PATH: $ScriptsDir"
287+
}
288+
else {
289+
Write-Host 'Already on PATH.'
290+
}
291+
292+
Log 'Creating config if needed'
293+
Invoke-NativeCommand -FilePath $BridgeExe -Arguments @('init-config') -FailureMessage 'Failed to create config.' | Out-Null
294+
295+
Log 'Registering URI handlers'
296+
Invoke-NativeCommand -FilePath $BridgeExe -Arguments @('register', '--auto') -FailureMessage 'Failed to register URI handlers.' | Out-Null
297+
298+
$configDir = Join-Path $env:APPDATA 'slicer-uri-bridge'
299+
300+
Write-Host ''
301+
Write-Host 'Done! Slicer URI Bridge is installed.' -ForegroundColor Green
302+
Write-Host ''
303+
Write-SummaryLine -Label 'Command:' -Value 'slicer-uri-bridge'
304+
Write-SummaryLine -Label 'Config:' -Value "$configDir\config.toml"
305+
Write-SummaryLine -Label 'Logs:' -Value "$configDir\bridge.log"
306+
Write-SummaryLine -Label 'Test:' -Value 'slicer-uri-bridge test'
307+
Write-SummaryLine -Label 'Environment:' -Value $VenvDir
308+
Write-Host ''
309+
Write-Host ' If "slicer-uri-bridge" is not found, open a new terminal window.' -ForegroundColor DarkGray
310+
Write-Host ' To update later, just run this installer again.' -ForegroundColor DarkGray
311+
Write-Host ''
312+
Wait-BeforeExit
313+
}
314+
315+
try {
316+
Main
317+
}
318+
catch {
319+
$message = $_.Exception.Message
320+
if (-not $message) {
321+
$message = ($_ | Out-String).Trim()
322+
}
323+
Die $message
324+
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "slicer-uri-bridge"
7-
version = "0.1.0"
7+
version = "0.1.1"
88
description = "Register slicer URI handlers and bridge slicer links to Bambu Studio."
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/slicer_uri_bridge/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from __future__ import annotations
44

5-
__version__ = "0.1.0"
5+
__version__ = "0.1.1"

src/slicer_uri_bridge/__main__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Allow running the CLI as ``python -m slicer_uri_bridge``."""
2+
3+
from __future__ import annotations
4+
5+
from .cli import main
6+
7+
raise SystemExit(main())

0 commit comments

Comments
 (0)