<# .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, [switch]$NoPlaywright, [string]$PlaywrightBrowsers = "chromium", [switch]$FixUrllib3, [switch]$RemovePth, [switch]$Quiet ) # Track whether the user chose an interactive auto-fix (so we can auto-remove .pth files) $AutoFixInteractive = $false ) 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 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) { # Suggest editable install for development checkouts (interactive git clones) if (-not $Editable) { $isGit = $false try { if (Test-Path (Join-Path $repoRoot '.git')) { $isGit = $true } elseif ((Get-Command git -ErrorAction SilentlyContinue) -ne $null) { try { $gitOut = & git -C $repoRoot rev-parse --is-inside-work-tree 2>$null; if ($gitOut -eq 'true') { $isGit = $true } } catch {} } } catch {} if ($isGit) { $Editable = $true Write-Log "Detected development checkout; installing in editable mode for development." "INFO" } } 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 } # 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" function Get-SitePackages { param($python) try { $json = & $python -c "import site, sysconfig, json; p=[]; try: p.extend(site.getsitepackages()) except Exception: pass try: pp = sysconfig.get_paths().get('purelib') if pp: p.append(pp) except Exception: pass seen = [] out = [] for s in p: if s and s not in seen: seen.append(s) out.append(s) print(json.dumps(out))" return $json | ConvertFrom-Json } catch { return @() } } function Find-InterferingPth { param($python) $pths = @() $sps = Get-SitePackages -python $python foreach ($sp in $sps) { if (Test-Path $sp) { Get-ChildItem -Path $sp -Filter *.pth -File -ErrorAction SilentlyContinue | ForEach-Object { $c = Get-Content -Path $_.FullName -ErrorAction SilentlyContinue | Out-String if ($c -match 'urllib3_future' -or $c -match 'urllib3-future') { $pths += $_.FullName } } } } return $pths } # Helper to try removal and re-verify function RemovePthAndVerify { param($python, $paths) foreach ($p in $paths) { Remove-Item -Force $p -ErrorAction SilentlyContinue } try { & $python -m pip install --upgrade --force-reinstall urllib3 } catch { Write-Log "pip install failed: $_" "ERROR"; return $false } & $python -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)" return ($LASTEXITCODE -eq 0) } 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 -eq 0) { Write-Log "Success: urllib3 problems appear resolved; continuing." "INFO" } else { Write-Log "Initial automatic fix did not resolve the issue; searching for interfering .pth files..." "INFO" $pths = Find-InterferingPth -python $venvPython if ($pths.Count -eq 0) { Write-Log "No interfering .pth files found; aborting." "ERROR" exit 7 } Write-Log ("Found interfering .pth files:`n" + ($pths -join "`n")) "ERROR" if ($RemovePth) { Write-Log "Removing .pth files as requested..." "INFO" if (RemovePthAndVerify -python $venvPython -paths $pths) { Write-Log "Success: urllib3 problems resolved after .pth removal; continuing." "INFO" } else { Write-Log "Automatic fix failed even after .pth removal; aborting." "ERROR" exit 7 } } else { if ($Quiet) { Write-Log "Detected interfering .pth files but cannot prompt in quiet mode. Use -RemovePth to remove them automatically." "ERROR"; exit 7 } $ans = Read-Host "Remove these files now? (y/N)" if ($ans -eq 'y' -or $ans -eq 'Y') { if (RemovePthAndVerify -python $venvPython -paths $pths) { Write-Log "Success: urllib3 problems resolved after .pth removal; continuing." "INFO" } else { Write-Log "Automatic fix failed even after .pth removal; aborting." "ERROR" exit 7 } } else { Write-Log "User declined to remove .pth files. Aborting." "ERROR" exit 7 } } } } 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') { $AutoFixInteractive = $true 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 -eq 0) { Write-Log "Success: urllib3 problems appear resolved; continuing." "INFO" } else { Write-Log "Initial automatic fix did not resolve the issue; searching for interfering .pth files..." "INFO" $pths = Find-InterferingPth -python $venvPython if ($pths.Count -eq 0) { Write-Log "No interfering .pth files found; aborting." "ERROR" exit 7 } Write-Log ("Found interfering .pth files:`n" + ($pths -join "`n")) "ERROR" if ($RemovePth -or $AutoFixInteractive -or $FixUrllib3) { Write-Log "Removing .pth files automatically..." "INFO" if (RemovePthAndVerify -python $venvPython -paths $pths) { Write-Log "Success: urllib3 problems resolved after .pth removal; continuing." "INFO" } else { Write-Log "Automatic fix failed even after .pth removal; aborting." "ERROR" exit 7 } } else { $ans2 = Read-Host "Remove these files now? (y/N)" if ($ans2 -eq 'y' -or $ans2 -eq 'Y') { if (RemovePthAndVerify -python $venvPython -paths $pths) { Write-Log "Success: urllib3 problems resolved after .pth removal; continuing." "INFO" } else { Write-Log "Automatic fix failed even after .pth removal; aborting." "ERROR" exit 7 } } else { Write-Log "User declined to remove .pth files. Aborting." "ERROR" exit 7 } } } } else { Write-Log "Aborting bootstrap to avoid leaving a broken environment." "ERROR" exit 7 } } } } catch { Write-Log "Failed to run environment verification: $_" "ERROR" } 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__" if exist "%REPO%\.venv\Scripts\mm.exe" "%REPO%\.venv\Scripts\mm.exe" %* if defined MM_DEBUG ( echo MM_DEBUG: REPO=%REPO% if exist "%REPO%\.venv\Scripts\python.exe" ( "%REPO%\.venv\Scripts\python.exe" -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);" ) else ( python -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);" ) ) if exist "%REPO%\.venv\Scripts\python.exe" ( "%REPO%\.venv\Scripts\python.exe" -m medeia_macina.cli_entry %* exit /b %ERRORLEVEL% ) if exist "%REPO%\CLI.py" ( python "%REPO%\CLI.py" %* 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' $exe = Join-Path $venv 'Scripts\mm.exe' if (Test-Path $exe) { & $exe @args; exit $LASTEXITCODE } $py = Join-Path $venv 'Scripts\python.exe' if ($env:MM_DEBUG) { Write-Host "MM_DEBUG: diagnostics" -ForegroundColor Yellow if (Test-Path $py) { & $py -c "import sys,importlib,importlib.util,traceback; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);" } else { python -c "import sys,importlib,importlib.util,traceback; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);" } } if (Test-Path $py) { & $py -m medeia_macina.cli_entry @args; exit $LASTEXITCODE } if (Test-Path (Join-Path $repo 'CLI.py')) { & python (Join-Path $repo 'CLI.py') @args; exit $LASTEXITCODE } # fallback python -m medeia_macina.cli_entry @args '@ # 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" Write-Host "" Write-Host "If the global 'mm' launcher fails, collect runtime diagnostics by setting MM_DEBUG and re-running the command:"" if ($IsWindowsPlatform) { Write-Host " PowerShell: $env:MM_DEBUG = '1'; mm" } else { Write-Host " POSIX: MM_DEBUG=1 mm" }