Files
Medios-Macina/scripts/bootstrap.ps1

395 lines
16 KiB
PowerShell
Raw Normal View History

2025-12-23 16:36:39 -08:00
<#
.SYNOPSIS
Bootstrap a Python virtualenv and install the project on Windows (PowerShell).
.DESCRIPTION
Creates a Python virtual environment (default: .venv), upgrades pip, installs the project
(either editable or normal), and optionally creates Desktop and Start Menu shortcuts.
.EXAMPLE
# Create .venv and install in editable mode, create Desktop shortcut
.\scripts\bootstrap.ps1 -Editable -CreateDesktopShortcut
.EXAMPLE
# Use a specific python executable and force overwrite existing venv
.\scripts\bootstrap.ps1 -Python "C:\\Python39\\python.exe" -Force
# Note: you may need to run PowerShell with ExecutionPolicy Bypass:
# powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1 -Editable
#>
param(
[switch]$Editable,
[switch]$CreateDesktopShortcut,
[switch]$CreateStartMenuShortcut,
[string]$VenvPath = ".venv",
[string]$Python = "",
[switch]$Force,
[switch]$NoInstall,
2025-12-24 02:13:21 -08:00
[switch]$NoPlaywright,
[string]$PlaywrightBrowsers = "chromium",
[switch]$FixUrllib3,
2025-12-23 16:36:39 -08:00
[switch]$Quiet
)
function Write-Log {
param([string]$msg,[string]$lvl="INFO")
if (-not $Quiet) {
if ($lvl -eq "ERROR") { Write-Host "[$lvl] $msg" -ForegroundColor Red } else { Write-Host "[$lvl] $msg" }
}
}
function Find-Python {
param([string]$preferred)
$candidates = @()
if ($preferred -and $preferred.Trim()) { $candidates += $preferred }
$candidates += @("python","python3","py")
foreach ($c in $candidates) {
try {
if ($c -eq "py") {
$out = & py -3 -c "import sys, json; print(sys.executable)" 2>$null
if ($out) { return $out.Trim() }
} else {
$out = & $c -c "import sys, json; print(sys.executable)" 2>$null
if ($out) { return $out.Trim() }
}
} catch {}
}
return $null
}
# Resolve OS detection in a broad-compatible way
try { $IsWindowsPlatform = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows) } catch { $IsWindowsPlatform = $env:OS -match 'Windows' }
# operate from repo root (parent of scripts dir)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path
Set-Location $repoRoot
$pythonExe = Find-Python -preferred $Python
if (-not $pythonExe) { Write-Log "No Python interpreter found. Specify -Python <path> or install Python." "ERROR"; exit 2 }
Write-Log "Using Python: $pythonExe"
# Full venv path
try { $venvFull = (Resolve-Path -LiteralPath $VenvPath -ErrorAction SilentlyContinue).Path } catch { $venvFull = $null }
if (-not $venvFull) { $venvFull = (Join-Path $repoRoot $VenvPath) }
# Handle existing venv
$venvExists = Test-Path $venvFull
if ($venvExists) {
if ($Force) {
Write-Log "Removing existing venv at $venvFull"
Remove-Item -Recurse -Force $venvFull
$venvExists = $false
} else {
# Quick health check: does the existing venv have a python executable?
$venvPy1 = Join-Path $venvFull "Scripts\python.exe"
$venvPy2 = Join-Path $venvFull "bin/python"
$venvHasPython = $false
try {
if (Test-Path $venvPy1 -PathType Leaf -ErrorAction SilentlyContinue) { $venvHasPython = $true }
elseif (Test-Path $venvPy2 -PathType Leaf -ErrorAction SilentlyContinue) { $venvHasPython = $true }
} catch {}
if (-not $venvHasPython) {
if ($Quiet) {
Write-Log "Existing venv appears incomplete or broken and quiet mode prevents prompting. Use -Force to recreate." "ERROR"
exit 4
}
$ans = Read-Host "$venvFull exists but appears invalid (no python executable). Overwrite to recreate? (y/N)"
if ($ans -eq 'y' -or $ans -eq 'Y') {
Write-Log "Removing broken venv at $venvFull"
Remove-Item -Recurse -Force $venvFull
$venvExists = $false
} else {
Write-Log "Aborted due to broken venv." "ERROR"; exit 4
}
} else {
if ($Quiet) {
Write-Log "Using existing venv at $venvFull (quiet mode)" "INFO"
} else {
$ans = Read-Host "$venvFull already exists. Overwrite? (y/N) (default: use existing venv)"
if ($ans -eq 'y' -or $ans -eq 'Y') {
Write-Log "Removing existing venv at $venvFull"
Remove-Item -Recurse -Force $venvFull
$venvExists = $false
} else {
Write-Log "Continuing using existing venv at $venvFull" "INFO"
}
}
}
}
}
if (-not (Test-Path $venvFull)) {
Write-Log "Creating venv at $venvFull"
try {
& $pythonExe -m venv $venvFull
} catch {
Write-Log "Failed to create venv: $_" "ERROR"; exit 3
}
} else {
Write-Log "Using existing venv at $venvFull" "INFO"
}
# Determine venv python executable
$venvPython = Join-Path $venvFull "Scripts\python.exe"
if (-not (Test-Path $venvPython)) { $venvPython = Join-Path $venvFull "bin/python" }
if (-not (Test-Path $venvPython)) { Write-Log "Created venv but could not find python inside it." "ERROR"; exit 4 }
Write-Log "Using venv python: $venvPython"
if (-not $NoInstall) {
Write-Log "Upgrading pip, setuptools, wheel"
try { & $venvPython -m pip install -U pip setuptools wheel } catch { Write-Log "pip upgrade failed: $_" "ERROR"; exit 5 }
if ($Editable) { $editable_label = "(editable)" } else { $editable_label = "" }
Write-Log ("Installing project {0}" -f $editable_label)
try {
if ($Editable) { & $venvPython -m pip install -e . } else { & $venvPython -m pip install . }
} catch {
Write-Log "pip install failed: $_" "ERROR"; exit 6
}
2025-12-24 02:13:21 -08:00
# Install Playwright browsers (default: chromium) unless explicitly disabled
if (-not $NoPlaywright) {
Write-Log "Ensuring Playwright browsers are installed (browsers=$PlaywrightBrowsers)..."
try {
& $venvPython -c "import importlib; importlib.import_module('playwright')" 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Log "'playwright' package not found in venv; installing via pip..."
& $venvPython -m pip install playwright
}
} catch {
Write-Log "Failed to check/install 'playwright' package: $_" "ERROR"
}
try {
if ($PlaywrightBrowsers -eq 'all') {
Write-Log "Installing all Playwright browsers..."
& $venvPython -m playwright install
} else {
$list = $PlaywrightBrowsers -split ','
foreach ($b in $list) {
$btrim = $b.Trim()
if ($btrim) {
Write-Log "Installing Playwright browser: $btrim"
& $venvPython -m playwright install $btrim
}
}
}
} catch {
Write-Log "Playwright browser install failed: $_" "ERROR"
}
}
# Verify environment for known package conflicts (urllib3 compatibility)
Write-Log "Verifying environment for known package conflicts (urllib3 compatibility)..."
try {
& $venvPython -c "import sys; from SYS.env_check import check_urllib3_compat; ok, msg = check_urllib3_compat(); print(msg); sys.exit(0 if ok else 2)"
if ($LASTEXITCODE -ne 0) {
Write-Log "Bootstrap detected a potentially broken 'urllib3' installation. See message above." "ERROR"
Write-Log "Suggested fixes (activate the venv first):" "INFO"
Write-Log " $ $venvPython -m pip uninstall urllib3-future -y" "INFO"
Write-Log " $ $venvPython -m pip install --upgrade --force-reinstall urllib3" "INFO"
Write-Log " $ $venvPython -m pip install niquests -U" "INFO"
if ($FixUrllib3) {
Write-Log "Attempting automatic fix (--FixUrllib3)..." "INFO"
try { & $venvPython -m pip uninstall urllib3-future -y } catch {}
try { & $venvPython -m pip install --upgrade --force-reinstall urllib3 } catch { Write-Log "pip install failed: $_" "ERROR"; exit 7 }
try { & $venvPython -m pip install niquests -U } catch { Write-Log "pip install niquests failed: $_" "ERROR" }
& $venvPython -c "import sys; from SYS.env_check import check_urllib3_compat; ok, msg = check_urllib3_compat(); print(msg); sys.exit(0 if ok else 2)"
if ($LASTEXITCODE -ne 0) {
Write-Log "Automatic fix failed; aborting." "ERROR"
exit 7
} else {
Write-Log "Success: urllib3 problems appear resolved; continuing." "INFO"
}
} else {
if ($Quiet) {
Write-Log "Bootstrap detected a potentially broken 'urllib3' installation. Use -FixUrllib3 to attempt an automatic fix." "ERROR"
exit 7
}
$ans = Read-Host "Attempt automatic fix now? (y/N)"
if ($ans -eq 'y' -or $ans -eq 'Y') {
try { & $venvPython -m pip uninstall urllib3-future -y } catch {}
try { & $venvPython -m pip install --upgrade --force-reinstall urllib3 } catch { Write-Log "pip install failed: $_" "ERROR"; exit 7 }
try { & $venvPython -m pip install niquests -U } catch { Write-Log "pip install niquests failed: $_" "ERROR" }
& $venvPython -c "import sys; from SYS.env_check import check_urllib3_compat; ok, msg = check_urllib3_compat(); print(msg); sys.exit(0 if ok else 2)"
if ($LASTEXITCODE -ne 0) {
Write-Log "Automatic fix failed; aborting." "ERROR"
exit 7
} else {
Write-Log "Success: urllib3 problems appear resolved; continuing." "INFO"
}
} else {
Write-Log "Aborting bootstrap to avoid leaving a broken environment." "ERROR"
exit 7
}
}
2025-12-24 02:13:21 -08:00
}
} catch {
Write-Log "Failed to run environment verification: $_" "ERROR"
}
2025-12-23 16:36:39 -08:00
Write-Log "Deno is already installed: $($denoCmd.Path)"
} else {
Write-Log "Installing Deno via official installer (https://deno.land)"
try {
try {
irm https://deno.land/install.ps1 | iex
} catch {
iwr https://deno.land/install.ps1 -UseBasicParsing | iex
}
# Ensure common install locations are on PATH for this session
$denoCandidatePaths = @(
Join-Path $env:USERPROFILE ".deno\bin",
Join-Path $env:LOCALAPPDATA "deno\bin"
)
foreach ($p in $denoCandidatePaths) {
if (Test-Path $p) {
if ($env:PATH -notmatch [regex]::Escape($p)) {
$env:PATH = $env:PATH + ";" + $p
}
}
}
$v = & deno --version 2>$null
if ($v) {
Write-Log "Deno installed: $v"
} else {
Write-Log "Deno installer completed but 'deno' not found on PATH; you may need to restart your shell or add the Deno bin folder to PATH." "ERROR"
}
} catch {
Write-Log "Deno install failed: $_" "ERROR"
}
}
# Shortcuts (Windows only)
if ($IsWindowsPlatform) {
if ($CreateDesktopShortcut -or $CreateStartMenuShortcut) {
$wsh = New-Object -ComObject WScript.Shell
$mmExe = Join-Path $venvFull "Scripts\mm.exe"
$target = $null
$args = ""
if (Test-Path $mmExe) {
$target = $mmExe
} else {
$target = $venvPython
$args = "-m medeia_macina.cli_entry"
}
if ($CreateDesktopShortcut) {
$desk = [Environment]::GetFolderPath('Desktop')
$link = Join-Path $desk "Medeia-Macina.lnk"
Write-Log "Creating Desktop shortcut: $link"
$sc = $wsh.CreateShortcut($link)
$sc.TargetPath = $target
$sc.Arguments = $args
$sc.WorkingDirectory = $repoRoot
$sc.IconLocation = "$target,0"
$sc.Save()
}
if ($CreateStartMenuShortcut) {
$start = Join-Path ([Environment]::GetFolderPath('ApplicationData')) 'Microsoft\Windows\Start Menu\Programs'
$dir = Join-Path $start "Medeia-Macina"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
$link2 = Join-Path $dir "Medeia-Macina.lnk"
Write-Log "Creating Start Menu shortcut: $link2"
$sc2 = $wsh.CreateShortcut($link2)
$sc2.TargetPath = $target
$sc2.Arguments = $args
$sc2.WorkingDirectory = $repoRoot
$sc2.IconLocation = "$target,0"
$sc2.Save()
}
}
}
# Install global 'mm' launcher into the user's bin directory so it can be invoked from any shell.
try {
$globalBin = Join-Path $env:USERPROFILE 'bin'
New-Item -ItemType Directory -Path $globalBin -Force | Out-Null
$mmCmd = Join-Path $globalBin 'mm.cmd'
$mmPs1 = Join-Path $globalBin 'mm.ps1'
$repo = $repoRoot
$cmdText = @"
@echo off
set "REPO=__REPO__"
2025-12-24 02:13:21 -08:00
if exist "%REPO%\.venv\Scripts\python.exe" (
"%REPO%\.venv\Scripts\python.exe" "%REPO%\CLI.py" %*
2025-12-23 16:36:39 -08:00
exit /b %ERRORLEVEL%
)
2025-12-24 02:13:21 -08:00
if exist "%REPO%\CLI.py" (
python "%REPO%\CLI.py" %*
2025-12-23 16:36:39 -08:00
exit /b %ERRORLEVEL%
)
python -m medeia_macina.cli_entry %*
"@
# Inject actual repo path safely (escape double-quotes if any)
$cmdText = $cmdText.Replace('__REPO__', $repo.Replace('"', '""'))
if (Test-Path $mmCmd) {
$bak = "$mmCmd.bak$(Get-Date -UFormat %s)"
Move-Item -Path $mmCmd -Destination $bak -Force
}
Set-Content -LiteralPath $mmCmd -Value $cmdText -Encoding UTF8
# PowerShell shim: use single-quoted here-string so literal PowerShell variables
# (like $args) are not expanded by this script when writing the file.
$ps1Text = @'
Param([Parameter(ValueFromRemainingArguments=$true)] $args)
$repo = "__REPO__"
$venv = Join-Path $repo '.venv'
$py = Join-Path $venv 'Scripts\python.exe'
2025-12-24 02:13:21 -08:00
$cli = Join-Path $repo 'CLI.py'
if (Test-Path $py) { & $py $cli @args; exit $LASTEXITCODE }
if (Test-Path $cli) { & $py $cli @args; exit $LASTEXITCODE }
2025-12-23 16:36:39 -08:00
# fallback
2025-12-24 02:13:21 -08:00
python $cli @args
2025-12-23 16:36:39 -08:00
'@
# Inject the actual repo path safely (escape embedded double-quotes if any)
$ps1Text = $ps1Text.Replace('__REPO__', $repo.Replace('"', '""'))
# Ensure the PowerShell shim falls back to the correct module when the venv isn't present
$ps1Text = $ps1Text.Replace(' -m medeia_entry ', ' -m medeia_macina.cli_entry ')
$ps1Text = $ps1Text.Replace('python -m medeia_entry', 'python -m medeia_macina.cli_entry')
if (Test-Path $mmPs1) {
$bak = "$mmPs1.bak$(Get-Date -UFormat %s)"
Move-Item -Path $mmPs1 -Destination $bak -Force
}
Set-Content -LiteralPath $mmPs1 -Value $ps1Text -Encoding UTF8
# Ensure user's bin is on PATH (User env var)
try {
$cur = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($cur -notlike "*$globalBin*") {
if ($cur) { $new = ($globalBin + ';' + $cur) } else { $new = $globalBin }
[Environment]::SetEnvironmentVariable('PATH', $new, 'User')
# Update current session PATH for immediate use
$env:PATH = $globalBin + ';' + $env:PATH
Write-Log "Added $globalBin to User PATH. Restart your shell to pick this up." "INFO"
} else {
Write-Log "$globalBin is already on the User PATH" "INFO"
}
} catch {
Write-Log "Failed to update user PATH: $_" "ERROR"
}
} catch {
Write-Log "Failed to install global launcher: $_" "ERROR"
}
Write-Log "Bootstrap complete." "INFO"
Write-Host ""
Write-Host "To activate the venv:"
if ($IsWindowsPlatform) {
Write-Host " PS> .\$VenvPath\Scripts\Activate.ps1"
Write-Host " CMD> .\$VenvPath\Scripts\activate.bat"
} else {
Write-Host " $ source ./$VenvPath/bin/activate"
}
Write-Host ""
Write-Host "To run the app:"
Write-Host " $ .\$VenvPath\Scripts\mm.exe (Windows) or"
Write-Host " $ ./$VenvPath/bin/mm (Linux) or"
Write-Host " $ $venvPython -m medeia_macina.cli_entry"