dsf
This commit is contained in:
319
scripts/bootstrap.ps1
Normal file
319
scripts/bootstrap.ps1
Normal file
@@ -0,0 +1,319 @@
|
||||
<#
|
||||
.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]$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
|
||||
}
|
||||
} else {
|
||||
Write-Log "Skipping install (--NoInstall set)"
|
||||
}
|
||||
|
||||
# Install Deno (official installer) - installed automatically
|
||||
try {
|
||||
$denoCmd = Get-Command 'deno' -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
$denoCmd = $null
|
||||
}
|
||||
if ($denoCmd) {
|
||||
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" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
if exist "%REPO%\.venv\Scripts\python.exe" (
|
||||
"%REPO%\.venv\Scripts\python.exe" -m medeia_macina.cli_entry %*
|
||||
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 (Test-Path $py) { & $py -m medeia_entry @args; exit $LASTEXITCODE }
|
||||
# fallback
|
||||
python -m medeia_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"
|
||||
220
scripts/bootstrap.sh
Normal file
220
scripts/bootstrap.sh
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bootstrap script for POSIX (Linux/macOS) to create a Python venv and install the project.
|
||||
# Usage: scripts/bootstrap.sh [--editable] [--venv <path>] [--python <python>] [--desktop] [--no-install]
|
||||
set -euo pipefail
|
||||
|
||||
VENV_PATH=".venv"
|
||||
EDITABLE=false
|
||||
DESKTOP=false
|
||||
PYTHON_CMD=""
|
||||
NOINSTALL=false
|
||||
FORCE=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
Options:
|
||||
-e, --editable Install project in editable mode (pip -e .)
|
||||
-p, --venv <path> Venv path (default: .venv)
|
||||
--python <python> Python executable to use (e.g. python3)
|
||||
-d, --desktop Create a desktop launcher (~/.local/share/applications and ~/Desktop)
|
||||
-n, --no-install Skip pip install
|
||||
-f, --force Overwrite existing venv without prompting
|
||||
-h, --help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-e|--editable) EDITABLE=true; shift;;
|
||||
-p|--venv) VENV_PATH="$2"; shift 2;;
|
||||
--python) PYTHON_CMD="$2"; shift 2;;
|
||||
-d|--desktop) DESKTOP=true; shift;;
|
||||
-n|--no-install) NOINSTALL=true; shift;;
|
||||
-f|--force) FORCE=true; shift;;
|
||||
-h|--help) usage; exit 0;;
|
||||
*) echo "Unknown option: $1"; usage; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$PYTHON_CMD" ]]; then
|
||||
PY="$PYTHON_CMD"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
PY=python3
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PY=python
|
||||
else
|
||||
echo "ERROR: No python executable found; install Python or pass --python <path>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "Using Python: $PY"
|
||||
|
||||
if [[ -d "$VENV_PATH" ]]; then
|
||||
if [[ "$FORCE" == "true" ]]; then
|
||||
echo "Removing existing venv $VENV_PATH"
|
||||
rm -rf "$VENV_PATH"
|
||||
else
|
||||
read -p "$VENV_PATH already exists. Overwrite? [y/N] " REPLY
|
||||
if [[ "$REPLY" != "y" && "$REPLY" != "Y" ]]; then
|
||||
echo "Aborted."; exit 0
|
||||
fi
|
||||
rm -rf "$VENV_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Creating venv at $VENV_PATH"
|
||||
$PY -m venv "$VENV_PATH"
|
||||
VENV_PY="$VENV_PATH/bin/python"
|
||||
|
||||
if [[ ! -x "$VENV_PY" ]]; then
|
||||
echo "ERROR: venv python not found at $VENV_PY" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [[ "$NOINSTALL" != "true" ]]; then
|
||||
echo "Upgrading pip, setuptools, wheel..."
|
||||
"$VENV_PY" -m pip install -U pip setuptools wheel
|
||||
|
||||
if [[ "$EDITABLE" == "true" ]]; then
|
||||
echo "Installing project in editable mode..."
|
||||
"$VENV_PY" -m pip install -e .
|
||||
else
|
||||
echo "Installing project..."
|
||||
"$VENV_PY" -m pip install .
|
||||
fi
|
||||
|
||||
# Verify the installed CLI module can be imported. This helps catch packaging
|
||||
# or installation problems early (e.g., missing modules or mispackaged project).
|
||||
echo "Verifying installed CLI import..."
|
||||
if "$VENV_PY" -c 'import importlib; importlib.import_module("medeia_macina.cli_entry")' >/dev/null 2>&1; then
|
||||
echo "OK: 'medeia_macina.cli_entry' is importable in the venv."
|
||||
else
|
||||
echo "WARNING: Could not import 'medeia_macina.cli_entry' from the venv." >&2
|
||||
# Check if legacy top-level module is present; if so, inform the user to prefer the packaged entrypoint
|
||||
if "$VENV_PY" -c 'import importlib; importlib.import_module("medeia_entry")' >/dev/null 2>&1; then
|
||||
echo "Note: 'medeia_entry' top-level module is present. It's recommended to install the project so 'medeia_macina.cli_entry' is available." >&2
|
||||
else
|
||||
echo "Action: Try running: $VENV_PY -m pip install -e . or inspect the venv site-packages to verify the installation." >&2
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Skipping install (--no-install)"
|
||||
fi
|
||||
|
||||
# Install Deno (official installer) - installed automatically
|
||||
if command -v deno >/dev/null 2>&1; then
|
||||
echo "Deno already installed: $(deno --version | head -n 1)"
|
||||
else
|
||||
echo "Installing Deno via official installer (https://deno.land)..."
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO- https://deno.land/install.sh | sh
|
||||
else
|
||||
echo "ERROR: curl or wget is required to install Deno automatically; please install Deno manually." >&2
|
||||
fi
|
||||
export DENO_INSTALL="${DENO_INSTALL:-$HOME/.deno}"
|
||||
export PATH="$DENO_INSTALL/bin:$PATH"
|
||||
if command -v deno >/dev/null 2>&1; then
|
||||
echo "Deno installed: $(deno --version | head -n 1)"
|
||||
else
|
||||
echo "Warning: Deno installer completed but 'deno' not found on PATH; add $HOME/.deno/bin to your PATH or restart your shell." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$DESKTOP" == "true" ]]; then
|
||||
echo "Creating desktop launcher..."
|
||||
EXEC_PATH="$VENV_PATH/bin/mm"
|
||||
if [[ ! -x "$EXEC_PATH" ]]; then
|
||||
# fallback to python -m
|
||||
EXEC_PATH="$VENV_PY -m medeia_macina.cli_entry"
|
||||
fi
|
||||
|
||||
APPDIR="$HOME/.local/share/applications"
|
||||
mkdir -p "$APPDIR"
|
||||
DESKTOP_FILE="$APPDIR/medeia-macina.desktop"
|
||||
cat > "$DESKTOP_FILE" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=Medeia-Macina
|
||||
Comment=Launch Medeia-Macina
|
||||
Exec=$EXEC_PATH
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
EOF
|
||||
chmod +x "$DESKTOP_FILE" || true
|
||||
if [[ -d "$HOME/Desktop" ]]; then
|
||||
cp "$DESKTOP_FILE" "$HOME/Desktop/"
|
||||
chmod +x "$HOME/Desktop/$(basename "$DESKTOP_FILE")" || true
|
||||
fi
|
||||
echo "Desktop launcher created: $DESKTOP_FILE"
|
||||
fi
|
||||
|
||||
# Install a global 'mm' launcher so it can be invoked from any shell.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
USER_BIN="${XDG_BIN_HOME:-$HOME/.local/bin}"
|
||||
mkdir -p "$USER_BIN"
|
||||
if [[ -f "$USER_BIN/mm" ]]; then
|
||||
echo "Backing up existing $USER_BIN/mm to $USER_BIN/mm.bak.$(date +%s)"
|
||||
mv "$USER_BIN/mm" "$USER_BIN/mm.bak.$(date +%s)"
|
||||
fi
|
||||
cat > "$USER_BIN/mm" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
REPO="$REPO"
|
||||
VENV="$REPO/.venv"
|
||||
if [ -x "$VENV/bin/mm" ]; then
|
||||
exec "$VENV/bin/mm" "$@"
|
||||
elif [ -x "$VENV/bin/python" ]; then
|
||||
exec "$VENV/bin/python" -m medeia_macina.cli_entry "$@"
|
||||
else
|
||||
exec python -m medeia_macina.cli_entry "$@"
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$USER_BIN/mm"
|
||||
|
||||
# Quick verification of the global launcher; helps catch packaging issues early.
|
||||
if "$USER_BIN/mm" --help >/dev/null 2>&1; then
|
||||
echo "Global 'mm' launcher verified: $USER_BIN/mm runs correctly."
|
||||
else
|
||||
echo "Warning: Global 'mm' launcher failed to run in this shell. Ensure $USER_BIN is on your PATH and the venv is installed; try: $VENV/bin/python -m medeia_macina.cli_entry --help" >&2
|
||||
fi
|
||||
|
||||
# Ensure the user's bin is on PATH for future sessions by adding to ~/.profile if needed
|
||||
if ! echo ":$PATH:" | grep -q ":$USER_BIN:"; then
|
||||
PROFILE="$HOME/.profile"
|
||||
if [ ! -f "$PROFILE" ]; then
|
||||
if [ -f "$HOME/.bash_profile" ]; then
|
||||
PROFILE="$HOME/.bash_profile"
|
||||
elif [ -f "$HOME/.bashrc" ]; then
|
||||
PROFILE="$HOME/.bashrc"
|
||||
elif [ -f "$HOME/.zshrc" ]; then
|
||||
PROFILE="$HOME/.zshrc"
|
||||
else
|
||||
PROFILE="$HOME/.profile"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! grep -q "ensure user local bin is on PATH" "$PROFILE" 2>/dev/null; then
|
||||
cat >> "$PROFILE" <<PROFILE_SNIPPET
|
||||
# Added by Medeia-Macina setup: ensure user local bin is on PATH
|
||||
if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||
PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
PROFILE_SNIPPET
|
||||
echo "Added $USER_BIN export to $PROFILE; restart your shell or source $PROFILE to use 'mm' from anywhere"
|
||||
fi
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Bootstrap complete.
|
||||
To activate the virtualenv:
|
||||
source $VENV_PATH/bin/activate
|
||||
To run the app:
|
||||
$VENV_PATH/bin/mm # if installed as a console script
|
||||
$VENV_PY -m medeia_macina.cli_entry # alternative
|
||||
|
||||
Global launcher installed: $USER_BIN/mm
|
||||
EOF
|
||||
227
scripts/setup.py
227
scripts/setup.py
@@ -32,6 +32,8 @@ import sys
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> None:
|
||||
@@ -132,6 +134,50 @@ def main() -> int:
|
||||
if sys.version_info < (3, 8):
|
||||
print("Warning: Python 3.8+ is recommended.", file=sys.stderr)
|
||||
|
||||
# Opinionated: always create or use a local venv at the project root (.venv)
|
||||
venv_dir = repo_root / ".venv"
|
||||
|
||||
def _venv_python(p: Path) -> Path:
|
||||
if platform.system().lower() == "windows":
|
||||
return p / "Scripts" / "python.exe"
|
||||
return p / "bin" / "python"
|
||||
|
||||
def _ensure_local_venv() -> Path:
|
||||
"""Create (if missing) and return the path to the venv's python executable.
|
||||
|
||||
This is intentionally opinionated: we keep a venv at `./.venv` in the repo root
|
||||
and use that for all package operations to keep developer environments reproducible.
|
||||
"""
|
||||
try:
|
||||
if not venv_dir.exists():
|
||||
print(f"Creating local virtualenv at: {venv_dir}")
|
||||
run([sys.executable, "-m", "venv", str(venv_dir)])
|
||||
else:
|
||||
print(f"Using existing virtualenv at: {venv_dir}")
|
||||
|
||||
py = _venv_python(venv_dir)
|
||||
if not py.exists():
|
||||
# Try recreating venv if python is missing
|
||||
print(f"Local venv python not found at {py}; recreating venv")
|
||||
run([sys.executable, "-m", "venv", str(venv_dir)])
|
||||
py = _venv_python(venv_dir)
|
||||
if not py.exists():
|
||||
raise RuntimeError(f"Unable to locate venv python at {py}")
|
||||
return py
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Failed to create or prepare local venv: {exc}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
# Ensure a local venv is present and use it for subsequent installs.
|
||||
venv_python = _ensure_local_venv()
|
||||
print(f"Using venv python: {venv_python}")
|
||||
|
||||
# Enforce opinionated behavior: install deps, playwright, deno, and install project in editable mode.
|
||||
# Ignore `--skip-deps` and `--install-editable` flags to keep the setup deterministic.
|
||||
args.skip_deps = False
|
||||
args.install_editable = True
|
||||
args.no_playwright = False
|
||||
|
||||
try:
|
||||
if args.playwright_only:
|
||||
if not playwright_package_installed():
|
||||
@@ -150,21 +196,21 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
if args.upgrade_pip:
|
||||
print("Upgrading pip, setuptools, and wheel...")
|
||||
run([sys.executable, "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"])
|
||||
print("Upgrading pip, setuptools, and wheel in local venv...")
|
||||
run([str(venv_python), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"])
|
||||
|
||||
if not args.skip_deps:
|
||||
req_file = repo_root / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
print(f"requirements.txt not found at {req_file}; skipping dependency installation.", file=sys.stderr)
|
||||
else:
|
||||
print(f"Installing Python dependencies from {req_file}...")
|
||||
run([sys.executable, "-m", "pip", "install", "-r", str(req_file)])
|
||||
print(f"Installing Python dependencies into local venv from {req_file}...")
|
||||
run([str(venv_python), "-m", "pip", "install", "-r", str(req_file)])
|
||||
|
||||
if not args.no_playwright:
|
||||
if not playwright_package_installed():
|
||||
print("'playwright' package not installed; installing it...")
|
||||
run([sys.executable, "-m", "pip", "install", "playwright"])
|
||||
print("'playwright' package not installed in venv; installing it...")
|
||||
run([str(venv_python), "-m", "pip", "install", "playwright"])
|
||||
|
||||
print("Installing Playwright browsers (this may download several hundred MB)...")
|
||||
try:
|
||||
@@ -173,12 +219,14 @@ def main() -> int:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Run Playwright install using the venv's python so binaries are available in venv
|
||||
cmd[0] = str(venv_python)
|
||||
run(cmd)
|
||||
|
||||
# Optional: install the project in editable mode so tests can import the package
|
||||
if args.install_editable:
|
||||
print("Installing project in editable mode (pip install -e .) ...")
|
||||
run([sys.executable, "-m", "pip", "install", "-e", "."])
|
||||
# Install the project into the local venv (editable mode is the default, opinionated)
|
||||
print("Installing project into local venv (editable mode)")
|
||||
run([str(venv_python), "-m", "pip", "install", "-e", "."])
|
||||
|
||||
# Optional: install Deno runtime (default: install unless --no-deno is passed)
|
||||
install_deno_requested = True
|
||||
@@ -188,12 +236,171 @@ def main() -> int:
|
||||
install_deno_requested = True
|
||||
|
||||
if install_deno_requested:
|
||||
print("Installing Deno runtime...")
|
||||
print("Installing Deno runtime (local/system)...")
|
||||
rc = _install_deno(args.deno_version)
|
||||
if rc != 0:
|
||||
print("Deno installation failed.", file=sys.stderr)
|
||||
return rc
|
||||
|
||||
# Write project-local launcher scripts (project root) that prefer the local .venv
|
||||
def _write_launchers():
|
||||
sh = repo_root / "mm"
|
||||
ps1 = repo_root / "mm.ps1"
|
||||
bat = repo_root / "mm.bat"
|
||||
|
||||
sh_text = """#!/usr/bin/env bash
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VENV="$SCRIPT_DIR/.venv"
|
||||
if [ -x "$VENV/bin/mm" ]; then
|
||||
exec "$VENV/bin/mm" "$@"
|
||||
elif [ -x "$VENV/bin/python" ]; then
|
||||
exec "$VENV/bin/python" -m medeia_entry "$@"
|
||||
else
|
||||
exec python -m medeia_entry "$@"
|
||||
fi
|
||||
"""
|
||||
try:
|
||||
sh.write_text(sh_text, encoding="utf-8")
|
||||
sh.chmod(sh.stat().st_mode | 0o111)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ps1_text = r"""Param([Parameter(ValueFromRemainingArguments=$true)] $args)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$venv = Join-Path $scriptDir '.venv'
|
||||
$exe = Join-Path $venv 'Scripts\mm.exe'
|
||||
if (Test-Path $exe) { & $exe @args; exit $LASTEXITCODE }
|
||||
$py = Join-Path $venv 'Scripts\python.exe'
|
||||
if (Test-Path $py) { & $py -m medeia_entry @args; exit $LASTEXITCODE }
|
||||
# fallback
|
||||
python -m medeia_entry @args
|
||||
"""
|
||||
try:
|
||||
ps1.write_text(ps1_text, encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bat_text = (
|
||||
"@echo off\r\n"
|
||||
"set SCRIPT_DIR=%~dp0\r\n"
|
||||
"if exist \"%SCRIPT_DIR%\\.venv\\Scripts\\mm.exe\" \"%SCRIPT_DIR%\\.venv\\Scripts\\mm.exe\" %*\r\n"
|
||||
"if exist \"%SCRIPT_DIR%\\.venv\\Scripts\\python.exe\" \"%SCRIPT_DIR%\\.venv\\Scripts\\python.exe\" -m medeia_entry %*\r\n"
|
||||
"python -m medeia_entry %*\r\n"
|
||||
)
|
||||
try:
|
||||
bat.write_text(bat_text, encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_write_launchers()
|
||||
|
||||
# Install user-global shims so `mm` can be executed from any shell session.
|
||||
def _install_user_shims(repo: Path) -> None:
|
||||
try:
|
||||
home = Path.home()
|
||||
system = platform.system().lower()
|
||||
|
||||
if system == "windows":
|
||||
user_bin = Path(os.environ.get("USERPROFILE", str(home))) / "bin"
|
||||
user_bin.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write mm.cmd (CMD shim)
|
||||
mm_cmd = user_bin / "mm.cmd"
|
||||
cmd_text = (
|
||||
f"@echo off\r\n"
|
||||
f"set REPO={repo}\r\n"
|
||||
f"if exist \"%REPO%\\.venv\\Scripts\\mm.exe\" \"%REPO%\\.venv\\Scripts\\mm.exe\" %*\r\n"
|
||||
f"if exist \"%REPO%\\.venv\\Scripts\\python.exe\" \"%REPO%\\.venv\\Scripts\\python.exe\" -m medeia_entry %*\r\n"
|
||||
f"python -m medeia_entry %*\r\n"
|
||||
)
|
||||
if mm_cmd.exists():
|
||||
bak = mm_cmd.with_suffix(f".bak{int(time.time())}")
|
||||
mm_cmd.replace(bak)
|
||||
mm_cmd.write_text(cmd_text, encoding="utf-8")
|
||||
|
||||
# Write mm.ps1 (PowerShell shim)
|
||||
mm_ps1 = user_bin / "mm.ps1"
|
||||
ps1_text = (
|
||||
"Param([Parameter(ValueFromRemainingArguments=$true)] $args)\n"
|
||||
f"$repo = \"{repo}\"\n"
|
||||
"$venv = Join-Path $repo '.venv'\n"
|
||||
"$exe = Join-Path $venv 'Scripts\\mm.exe'\n"
|
||||
"if (Test-Path $exe) { & $exe @args; exit $LASTEXITCODE }\n"
|
||||
"$py = Join-Path $venv 'Scripts\\python.exe'\n"
|
||||
"if (Test-Path $py) { & $py -m medeia_entry @args; exit $LASTEXITCODE }\n"
|
||||
"python -m medeia_entry @args\n"
|
||||
)
|
||||
if mm_ps1.exists():
|
||||
bak = mm_ps1.with_suffix(f".bak{int(time.time())}")
|
||||
mm_ps1.replace(bak)
|
||||
mm_ps1.write_text(ps1_text, encoding="utf-8")
|
||||
|
||||
# Attempt to add user_bin to the user's PATH if it's not present.
|
||||
try:
|
||||
cur = os.environ.get("PATH", "")
|
||||
str_bin = str(user_bin)
|
||||
if str_bin not in cur:
|
||||
ps_cmd = (
|
||||
"$bin = '{bin}';"
|
||||
"$cur = [Environment]::GetEnvironmentVariable('PATH','User');"
|
||||
"if ($cur -notlike \"*$bin*\") {[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User')}"
|
||||
).format(bin=str_bin.replace('\\','\\\\'))
|
||||
subprocess.run(["powershell","-NoProfile","-Command", ps_cmd], check=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"Installed global launchers to: {user_bin}")
|
||||
|
||||
else:
|
||||
# POSIX
|
||||
user_bin = Path(os.environ.get("XDG_BIN_HOME", str(home / ".local/bin")))
|
||||
user_bin.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mm_sh = user_bin / "mm"
|
||||
sh_text = (
|
||||
"#!/usr/bin/env bash\n"
|
||||
f"REPO=\"{repo}\"\n"
|
||||
"VENV=\"$REPO/.venv\"\n"
|
||||
"if [ -x \"$VENV/bin/mm\" ]; then\n"
|
||||
" exec \"$VENV/bin/mm\" \"$@\"\n"
|
||||
"elif [ -x \"$VENV/bin/python\" ]; then\n"
|
||||
" exec \"$VENV/bin/python\" -m medeia_entry \"$@\"\n"
|
||||
"else\n"
|
||||
" exec python -m medeia_entry \"$@\"\n"
|
||||
"fi\n"
|
||||
)
|
||||
if mm_sh.exists():
|
||||
bak = mm_sh.with_suffix(f".bak{int(time.time())}")
|
||||
mm_sh.replace(bak)
|
||||
mm_sh.write_text(sh_text, encoding="utf-8")
|
||||
mm_sh.chmod(mm_sh.stat().st_mode | 0o111)
|
||||
|
||||
# Ensure the user's bin is on PATH for future sessions by adding to ~/.profile
|
||||
cur_path = os.environ.get("PATH", "")
|
||||
if str(user_bin) not in cur_path:
|
||||
profile = home / ".profile"
|
||||
snippet = (
|
||||
"# Added by Medeia-Macina setup: ensure user local bin is on PATH\n"
|
||||
"if [ -d \"$HOME/.local/bin\" ] && [[ \":$PATH:\" != *\":$HOME/.local/bin:\"* ]]; then\n"
|
||||
" PATH=\"$HOME/.local/bin:$PATH\"\n"
|
||||
"fi\n"
|
||||
)
|
||||
try:
|
||||
txt = profile.read_text() if profile.exists() else ""
|
||||
if snippet.strip() not in txt:
|
||||
with profile.open("a", encoding="utf-8") as fh:
|
||||
fh.write("\n" + snippet)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"Installed global launcher to: {mm_sh}")
|
||||
|
||||
except Exception as exc: # pragma: no cover - best effort
|
||||
print(f"Failed to install global shims: {exc}", file=sys.stderr)
|
||||
|
||||
_install_user_shims(repo_root)
|
||||
|
||||
print("Setup complete.")
|
||||
return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user