Fix portable Python: ship python/runtime and relink .venv on install (0.4.6).
Some checks failed
Release Windows / build (push) Has been cancelled

0.4.5 venv pointed at the build machine user profile; other PCs failed to start API.
Bundle stdlib under python/runtime, repair pyvenv.cfg, and health-check before start.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-07 15:48:57 +08:00
parent 3a89438e5b
commit 5429c07342
7 changed files with 158 additions and 27 deletions

View file

@ -19,7 +19,7 @@ This directory builds a Windows deliverable with:
**Installer:** English + 简体中文 (language dialog). Icons use `packaging/assets/netx.ico`. **Installer:** English + 简体中文 (language dialog). Icons use `packaging/assets/netx.ico`.
**Offline:** Setup ships portable PostgreSQL, `.venv` (when built with `-CreateVenv`), and WinSW. The installer wizard chooses **built-in or external** PostgreSQL and validates external credentials (`psql SELECT 1`) before files are installed — **no GitHub/EDB download on the target PC**. Service install uses bundled WinSW (pass `-AllowDownload` only on a build/dev machine if the binary is missing). Auto-update still needs network later, and is unchecked by default. **Offline:** Setup ships portable PostgreSQL, **`python/runtime` + `.venv`** (portable; not tied to the build PC’s user profile), and WinSW. The installer wizard chooses **built-in or external** PostgreSQL and validates external credentials (`psql SELECT 1`) before files are installed — **no GitHub/EDB download on the target PC**. Service install uses bundled WinSW (pass `-AllowDownload` only on a build/dev machine if the binary is missing). Auto-update still needs network later, and is unchecked by default.
**OS:** Windows 10/11 or Windows Server **2016+** recommended. Packaging scripts are UTF-8 **with BOM** so Chinese UI works on Windows PowerShell 5.x. Bundled Python in current releases is **3.13+**, which does **not** support Windows Server 2012 R2 — use Server 2016+ or a newer desktop OS. **OS:** Windows 10/11 or Windows Server **2016+** recommended. Packaging scripts are UTF-8 **with BOM** so Chinese UI works on Windows PowerShell 5.x. Bundled Python in current releases is **3.13+**, which does **not** support Windows Server 2012 R2 — use Server 2016+ or a newer desktop OS.

View file

@ -276,12 +276,21 @@ function Start-NetxPowerShell {
function Get-NetxSystemPython { function Get-NetxSystemPython {
# Prefer a real interpreter; skip the WindowsApps Store stub. # Prefer a real interpreter; skip the WindowsApps Store stub.
$candidates = @() $candidates = @()
foreach ($cmd in @("python", "python3", "py")) { foreach ($cmd in @("python", "python3")) {
$c = Get-Command $cmd -ErrorAction SilentlyContinue $c = Get-Command $cmd -ErrorAction SilentlyContinue
if ($c -and $c.Source -and ($c.Source -notmatch '\\WindowsApps\\')) { if ($c -and $c.Source -and ($c.Source -notmatch '\\WindowsApps\\')) {
$candidates += $c.Source $candidates += $c.Source
} }
} }
$pyLauncher = Get-Command "py" -ErrorAction SilentlyContinue
if ($pyLauncher -and $pyLauncher.Source -and ($pyLauncher.Source -notmatch '\\WindowsApps\\')) {
try {
$resolved = (& $pyLauncher.Source -3 -c "import sys; print(sys.executable)" 2>$null | Out-String).Trim()
if ($resolved -and (Test-Path -LiteralPath $resolved)) {
$candidates += $resolved
}
} catch {}
}
foreach ($p in @( foreach ($p in @(
"$env:LOCALAPPDATA\Python\pythoncore-3.14-64\python.exe", "$env:LOCALAPPDATA\Python\pythoncore-3.14-64\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe", "$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
@ -300,6 +309,65 @@ function Get-NetxSystemPython {
return $null return $null
} }
function Get-NetxBundledPythonRoot {
param([Parameter(Mandatory = $true)][string]$ProgramRoot)
return Join-Path $ProgramRoot "python\runtime"
}
function Repair-NetxShippedVenv {
param([Parameter(Mandatory = $true)][string]$ProgramRoot)
$cfgPath = Join-Path $ProgramRoot ".venv\pyvenv.cfg"
$rtRoot = Get-NetxBundledPythonRoot -ProgramRoot $ProgramRoot
$rtPy = Join-Path $rtRoot "python.exe"
if (-not (Test-Path -LiteralPath $rtPy)) { return $false }
if (-not (Test-Path -LiteralPath $cfgPath)) { return $false }
$rtRootAbs = (Resolve-Path -LiteralPath $rtRoot).Path
$rtPyAbs = (Resolve-Path -LiteralPath $rtPy).Path
$ver = "3.14.0"
try {
$verOut = & $rtPyAbs -c "import sys; print('%d.%d.%d' % sys.version_info[:3])" 2>$null
if ($verOut) { $ver = ($verOut | Out-String).Trim() }
} catch {}
$lines = @(
"home = $rtRootAbs",
"include-system-site-packages = false",
"version = $ver",
"executable = $rtPyAbs"
)
Set-Content -LiteralPath $cfgPath -Value ($lines -join "`r`n") -Encoding ascii
return $true
}
function Test-NetxVenvRunnable {
param([Parameter(Mandatory = $true)][string]$VenvPython)
if (-not (Test-Path -LiteralPath $VenvPython)) { return $false }
$outFile = Join-Path $env:TEMP ("netx-venv-out-" + [Guid]::NewGuid().ToString("n") + ".txt")
$errFile = Join-Path $env:TEMP ("netx-venv-err-" + [Guid]::NewGuid().ToString("n") + ".txt")
try {
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $VenvPython
$psi.Arguments = '-c "import encodings, sys; sys.exit(0 if sys.prefix else 1)"'
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.CreateNoWindow = $true
$p = [Diagnostics.Process]::Start($psi)
$stderr = $p.StandardError.ReadToEnd()
$null = $p.StandardOutput.ReadToEnd()
$p.WaitForExit()
if ($stderr -match 'did not find executable|No Python at|Fatal Python error') {
return $false
}
if ($p.ExitCode -ne 0) { return $false }
return $true
} finally {
Remove-Item -LiteralPath $outFile, $errFile -Force -ErrorAction SilentlyContinue
}
}
function Ensure-NetxVenv { function Ensure-NetxVenv {
param( param(
[Parameter(Mandatory = $true)][string]$ProgramRoot, [Parameter(Mandatory = $true)][string]$ProgramRoot,
@ -307,9 +375,28 @@ function Ensure-NetxVenv {
) )
$venvPy = Join-Path $ProgramRoot ".venv\Scripts\python.exe" $venvPy = Join-Path $ProgramRoot ".venv\Scripts\python.exe"
$req = Join-Path $ProgramRoot "requirements.txt" $req = Join-Path $ProgramRoot "requirements.txt"
if ((Test-Path $venvPy) -and -not $ForcePip) { $rtPy = Join-Path (Get-NetxBundledPythonRoot -ProgramRoot $ProgramRoot) "python.exe"
if (Test-Path -LiteralPath $rtPy) {
$null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot
}
if ((Test-Path -LiteralPath $venvPy) -and -not $ForcePip) {
if (Test-NetxVenvRunnable -VenvPython $venvPy) {
return $venvPy return $venvPy
} }
Write-Host "[WARN] Shipped .venv exists but Python cannot start (broken pyvenv.cfg or missing runtime)." -ForegroundColor Yellow
if (Test-Path -LiteralPath $rtPy) {
$null = Repair-NetxShippedVenv -ProgramRoot $ProgramRoot
if (Test-NetxVenvRunnable -VenvPython $venvPy) {
Write-Host "==> Repaired .venv to use bundled python/runtime" -ForegroundColor Green
return $venvPy
}
}
if (-not $ForcePip) {
throw "venv_not_runnable: reinstall NetX Setup (ships python/runtime + .venv) or run repair as admin"
}
}
if (-not (Test-Path $venvPy)) { if (-not (Test-Path $venvPy)) {
$py = Get-NetxSystemPython $py = Get-NetxSystemPython
if (-not $py) { if (-not $py) {

View file

@ -168,16 +168,50 @@ foreach ($cmdName in @("NetX-FirstRun.cmd", "NetX-Start.cmd", "NetX-Tray.cmd", "
} }
if ($CreateVenv) { if ($CreateVenv) {
Write-Host "==> Creating .venv in stage (ships with Setup; target should NOT pip-install)" Write-Host "==> Creating portable python/runtime + .venv (must not reference build-machine paths)"
$pyPath = Get-NetxSystemPython $pyPath = Get-NetxSystemPython
if (-not $pyPath) { throw "python_not_found_for_venv: need Python 3.11+ (not WindowsApps stub)" } if (-not $pyPath) { throw "python_not_found_for_venv: need Python 3.11+ (not WindowsApps stub)" }
Write-Host " Using: $pyPath" Write-Host " Build Python: $pyPath"
& $pyPath -m venv (Join-Path $stage ".venv")
$venvPy = Join-Path $stage ".venv\Scripts\python.exe" $prefix = (& $pyPath -c "import sys; print(sys.base_prefix)" 2>$null | Out-String).Trim()
if (-not $prefix -or -not (Test-Path -LiteralPath $prefix)) {
throw "python_base_prefix_not_found: $prefix"
}
$rtDir = Join-Path $stage "python\runtime"
if (Test-Path $rtDir) { Remove-Item -Recurse -Force $rtDir }
New-Item -ItemType Directory -Path $rtDir -Force | Out-Null
Write-Host "==> Copying Python stdlib/runtime to $rtDir (exclude base site-packages)"
$spEx = Join-Path $prefix "Lib\site-packages"
$robocopy = Join-Path $env:SystemRoot "System32\robocopy.exe"
if (-not (Test-Path -LiteralPath $robocopy)) { throw "robocopy_not_found" }
& $robocopy $prefix $rtDir /E /XD $spEx __pycache__ /XF *.pyc /NFL /NDL /NJH /NJS /nc /ns /np | Out-Null
if ($LASTEXITCODE -ge 8) { throw "robocopy_python_runtime_failed: exit $LASTEXITCODE" }
$rtPy = Join-Path $rtDir "python.exe"
if (-not (Test-Path -LiteralPath $rtPy)) { throw "python_runtime_missing: $rtPy" }
$venvDir = Join-Path $stage ".venv"
if (Test-Path $venvDir) { Remove-Item -Recurse -Force $venvDir }
Write-Host "==> Creating .venv from shipped runtime (--copies)"
& $rtPy -m venv $venvDir --copies
if ($LASTEXITCODE -ne 0) { throw "venv_create_failed" }
$venvPy = Join-Path $venvDir "Scripts\python.exe"
$null = Repair-NetxShippedVenv -ProgramRoot $stage
if (-not (Test-NetxVenvRunnable -VenvPython $venvPy)) {
throw "venv_not_runnable_after_build: check python/runtime copy"
}
& $venvPy -m pip install --upgrade pip & $venvPy -m pip install --upgrade pip
& $venvPy -m pip install -r (Join-Path $stage "requirements.txt") & $venvPy -m pip install -r (Join-Path $stage "requirements.txt")
if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" } if ($LASTEXITCODE -ne 0) { throw "pip_install_failed" }
$null = Repair-NetxShippedVenv -ProgramRoot $stage
if (-not (Test-NetxVenvRunnable -VenvPython $venvPy)) {
throw "venv_not_runnable_after_pip"
}
Write-Host "==> Slimming .venv (drop tests / __pycache__ / *.pyc)" Write-Host "==> Slimming .venv (drop tests / __pycache__ / *.pyc)"
$site = Join-Path $stage ".venv\Lib\site-packages" $site = Join-Path $stage ".venv\Lib\site-packages"
if (Test-Path $site) { if (Test-Path $site) {

View file

@ -5,7 +5,7 @@
#define MyAppName "NetX" #define MyAppName "NetX"
#ifndef MyAppVersion #ifndef MyAppVersion
#define MyAppVersion "0.4.5" #define MyAppVersion "0.4.6"
#endif #endif
#define MyAppPublisher "NetX" #define MyAppPublisher "NetX"
#define MyAppURL "https://github.com/hansjone/netx" #define MyAppURL "https://github.com/hansjone/netx"

View file

@ -293,21 +293,19 @@ Write-DotEnvValue -Path $envPath -Values $values
Write-Host "" Write-Host ""
Write-Host "Wrote $envPath" -ForegroundColor Green Write-Host "Wrote $envPath" -ForegroundColor Green
# Official Setup ships .venv (build_release -CreateVenv). Only bootstrap if missing # Official Setup ships python/runtime + .venv (build_release -CreateVenv).
# (e.g. zip without venv). Prefer not to pip-install on the target machine.
$venvPy = Join-Path $prog ".venv\Scripts\python.exe" $venvPy = Join-Path $prog ".venv\Scripts\python.exe"
if (Test-Path $venvPy) {
Write-Host "==> Using bundled Python venv: $venvPy" -ForegroundColor Green
} else {
Write-Host "==> Bundled .venv missing — creating one (Setup should normally ship it)." -ForegroundColor Yellow
try { try {
$null = Ensure-NetxVenv -ProgramRoot $prog $null = Ensure-NetxVenv -ProgramRoot $prog
Write-Host "==> Python venv ready: $venvPy" -ForegroundColor Green Write-Host "==> Bundled Python venv OK: $venvPy" -ForegroundColor Green
} catch { } catch {
if (Test-Path $venvPy) {
throw
}
Write-Host "==> Bundled .venv missing — creating one (Setup should normally ship it)." -ForegroundColor Yellow
Write-Host "[WARN] $($_.Exception.Message)" -ForegroundColor Yellow Write-Host "[WARN] $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host " Install a Setup built with: packaging\build_release.ps1 -CreateVenv" -ForegroundColor Yellow Write-Host " Install a Setup built with: packaging\build_release.ps1 -CreateVenv" -ForegroundColor Yellow
} }
}
Write-Host "" Write-Host ""
Write-Host (T "Setup complete." "首次配置完成。") -ForegroundColor Green Write-Host (T "Setup complete." "首次配置完成。") -ForegroundColor Green

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "netx-ops" name = "netx-ops"
version = "0.4.5" version = "0.4.6"
description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP" description = "netx operations tool: alarm-centric workflows with REST API and stdio MCP"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"

View file

@ -23,6 +23,14 @@ if ($Backgtound -and -not $Background) {
$projectRoot = Split-Path -Parent $PSScriptRoot $projectRoot = Split-Path -Parent $PSScriptRoot
Set-Location $projectRoot Set-Location $projectRoot
$packCommon = Join-Path $projectRoot "packaging\_common.ps1"
if (Test-Path -LiteralPath $packCommon) {
. $packCommon
if (Get-Command Repair-NetxShippedVenv -ErrorAction SilentlyContinue) {
$null = Repair-NetxShippedVenv -ProgramRoot $projectRoot
}
}
# Prefer writable data-root runtime dir (Program Files is not writable after Setup install). # Prefer writable data-root runtime dir (Program Files is not writable after Setup install).
if ($env:NETX_RUN_DIR) { if ($env:NETX_RUN_DIR) {
$runDir = $env:NETX_RUN_DIR $runDir = $env:NETX_RUN_DIR
@ -46,14 +54,18 @@ $webPidFile = Join-Path $runDir "web.pid"
$webLogFile = Join-Path $runDir "web.out.log" $webLogFile = Join-Path $runDir "web.out.log"
$webErrFile = Join-Path $runDir "web.err.log" $webErrFile = Join-Path $runDir "web.err.log"
$venvPython = Join-Path $projectRoot ".venv\\Scripts\\python.exe" $venvPython = Join-Path $projectRoot ".venv\Scripts\python.exe"
if (-not (Test-Path $venvPython)) { if (Get-Command Test-NetxVenvRunnable -ErrorAction SilentlyContinue) {
if (-not (Test-NetxVenvRunnable -VenvPython $venvPython)) {
throw "venv_not_runnable: reinstall NetX or run setup as administrator"
}
} elseif (-not (Test-Path -LiteralPath $venvPython)) {
Write-Host "==> .venv not found, creating virtual environment" Write-Host "==> .venv not found, creating virtual environment"
python -m venv (Join-Path $projectRoot ".venv") python -m venv (Join-Path $projectRoot ".venv")
} if (-not (Test-Path -LiteralPath $venvPython)) {
if (-not (Test-Path $venvPython)) {
throw "failed_to_create_venv" throw "failed_to_create_venv"
} }
}
$pythonExe = $venvPython $pythonExe = $venvPython
Write-Host "==> Project root: $projectRoot" Write-Host "==> Project root: $projectRoot"