# One-click build: console -> conda-pack -> NSIS .exe. Run from repo root. # Requires: conda, node/npm (for console), NSIS (makensis) on PATH. $ErrorActionPreference = "Stop" $RepoRoot = (Get-Item $PSScriptRoot).Parent.Parent.FullName Set-Location $RepoRoot Write-Host "[build_win] REPO_ROOT=$RepoRoot" $PackDir = $PSScriptRoot $Dist = if ($env:DIST) { $env:DIST } else { "dist" } $Archive = Join-Path $Dist "qwenpaw-env.zip" $Unpacked = Join-Path $Dist "win-unpacked" $NsiPath = Join-Path $PackDir "desktop.nsi" # Packages affected by conda-unpack bug on Windows (conda-pack Issue #154) # conda-unpack corrupts Python string escaping when replacing path prefixes. # Example: "\\\\?\\" (correct) -> "\\" (SyntaxError) # Solution: Reinstall these packages after conda-unpack to restore correct files. # See: issue.md, scripts/pack/WINDOWS_FIX.md $CondaUnpackAffectedPackages = @( "huggingface_hub" # Uses Windows extended-length path prefix (\\?\) "discord.py" # ARG_NAME_SUBREGEX contains \\?\* which gets corrupted ) New-Item -ItemType Directory -Force -Path $Dist | Out-Null Write-Host "== Building wheel (includes console frontend) ==" # Skip wheel_build if dist already has a wheel for current version $VersionFile = Join-Path $RepoRoot "src\qwenpaw\__version__.py" $CurrentVersion = "" if (Test-Path $VersionFile) { $m = (Get-Content $VersionFile -Raw) -match '__version__\s*=\s*"([^"]+)"' if ($m) { $CurrentVersion = $Matches[1] } } $RunWheelBuild = $true if ($CurrentVersion) { $wheelGlob = Join-Path $Dist "qwenpaw-$CurrentVersion-*.whl" $existingWheels = Get-ChildItem -Path $wheelGlob -ErrorAction SilentlyContinue if ($existingWheels.Count -gt 0) { Write-Host "dist/ already has wheel for version $CurrentVersion, skipping." $RunWheelBuild = $false } else { # Clean up old wheels to avoid confusion $oldWheels = Get-ChildItem -Path (Join-Path $Dist "qwenpaw-*.whl") -ErrorAction SilentlyContinue if ($oldWheels.Count -gt 0) { Write-Host "Removing old wheel files: $($oldWheels | ForEach-Object { $_.Name })" $oldWheels | Remove-Item -Force } } } if ($RunWheelBuild) { $WheelBuildScript = Join-Path $RepoRoot "scripts\wheel_build.ps1" if (-not (Test-Path $WheelBuildScript)) { throw "wheel_build.ps1 not found: $WheelBuildScript" } & $WheelBuildScript if ($LASTEXITCODE -ne 0) { throw "wheel_build.ps1 failed with exit code $LASTEXITCODE" } } Write-Host "== Building conda-packed env ==" & python $PackDir\build_common.py --output $Archive --format zip --cache-wheels if ($LASTEXITCODE -ne 0) { throw "build_common.py failed with exit code $LASTEXITCODE" } if (-not (Test-Path $Archive)) { throw "Archive not created: $Archive" } Write-Host "== Unpacking env ==" if (Test-Path $Unpacked) { Remove-Item -Recurse -Force $Unpacked } Expand-Archive -Path $Archive -DestinationPath $Unpacked -Force $unpackedRoot = Get-ChildItem -Path $Unpacked -ErrorAction SilentlyContinue | Measure-Object Write-Host "[build_win] Unpacked entries in $Unpacked : $($unpackedRoot.Count)" # Resolve env root: conda-pack usually puts python.exe at archive root; allow one nested dir. $EnvRoot = $Unpacked if (-not (Test-Path (Join-Path $EnvRoot "python.exe"))) { $found = Get-ChildItem -Path $Unpacked -Directory -ErrorAction SilentlyContinue | Where-Object { Test-Path (Join-Path $_.FullName "python.exe") } | Select-Object -First 1 if ($found) { $EnvRoot = $found.FullName; Write-Host "[build_win] Env root: $EnvRoot" } } if (-not (Test-Path (Join-Path $EnvRoot "python.exe"))) { throw "python.exe not found in unpacked env (checked $Unpacked and one level down)." } if (-not [System.IO.Path]::IsPathRooted($EnvRoot)) { $EnvRoot = Join-Path $RepoRoot $EnvRoot } Write-Host "[build_win] python.exe found at env root: $EnvRoot" # Rewrite prefix in packed env so paths point to current location (required after move). $CondaUnpack = Join-Path $EnvRoot "Scripts\conda-unpack.exe" if (Test-Path $CondaUnpack) { Write-Host "[build_win] Running conda-unpack..." & $CondaUnpack if ($LASTEXITCODE -ne 0) { throw "conda-unpack failed with exit code $LASTEXITCODE" } # Fix conda-unpack bug: it corrupts Python string escaping on Windows # See: issue.md and https://github.com/conda/conda-pack/issues/154 # Solution: Reinstall affected packages using cached wheels Write-Host "[build_win] Fixing conda-unpack corruption by reinstalling affected packages..." $WheelsCache = Join-Path $RepoRoot ".cache\conda_unpack_wheels" if (Test-Path $WheelsCache) { $pythonExe = Join-Path $EnvRoot "python.exe" foreach ($pkg in $CondaUnpackAffectedPackages) { Write-Host " Reinstalling $pkg..." & $pythonExe -m pip install --force-reinstall --no-deps ` --find-links $WheelsCache --no-index $pkg if ($LASTEXITCODE -ne 0) { Write-Host " WARN: Failed to reinstall $pkg (exit code: $LASTEXITCODE)" -ForegroundColor Yellow } } # Verify the fix worked Write-Host "[build_win] Verifying fix..." # Create a verification script that handles SSL certificate store issues on Windows $verifyScript = @" import sys import ssl import os # Set SSL certificate paths before importing anything that uses SSL try: import certifi cert_path = certifi.where() os.environ['SSL_CERT_FILE'] = cert_path os.environ['REQUESTS_CA_BUNDLE'] = cert_path os.environ['CURL_CA_BUNDLE'] = cert_path except ImportError: print("WARNING: certifi not available, using system certificates") # Monkey-patch ssl.SSLContext.load_default_certs to handle Windows certificate store issues # This prevents SSL errors when the Windows certificate store contains corrupted certificates _original_load_default_certs = ssl.SSLContext.load_default_certs def _safe_load_default_certs(self, purpose=ssl.Purpose.SERVER_AUTH): try: # Try loading from Windows certificate store first _original_load_default_certs(self, purpose) except ssl.SSLError as e: # If Windows store fails, fall back to certifi CA bundle print(f"WARNING: Windows certificate store load failed ({e}), using certifi CA bundle") try: import certifi self.load_verify_locations(cafile=certifi.where()) except Exception as cert_err: print(f"ERROR: Failed to load certifi CA bundle: {cert_err}") raise ssl.SSLContext.load_default_certs = _safe_load_default_certs # Now verify imports try: from huggingface_hub import file_download print('✓ huggingface_hub import OK') except Exception as e: print(f'✗ huggingface_hub import failed: {e}') sys.exit(1) try: import discord print('✓ discord.py import OK') except Exception as e: print(f'✗ discord.py import failed: {e}') sys.exit(1) "@ $verifyScriptPath = Join-Path $EnvRoot "verify_imports.py" Set-Content -Path $verifyScriptPath -Value $verifyScript -Encoding UTF8 $pythonExe = Join-Path $EnvRoot "python.exe" & $pythonExe $verifyScriptPath $verifyExitCode = $LASTEXITCODE # Clean up verification script Remove-Item -Path $verifyScriptPath -Force -ErrorAction SilentlyContinue if ($verifyExitCode -ne 0) { throw "CRITICAL: Package verification failed after reinstall. See output above for details." } Write-Host "[build_win] ✓ conda-unpack corruption fixed successfully." } else { Write-Host "[build_win] WARN: wheels_cache not found at $WheelsCache" -ForegroundColor Yellow Write-Host "[build_win] WARN: Cannot fix conda-unpack corruption. App may fail to start." -ForegroundColor Yellow } } else { Write-Host "[build_win] WARN: conda-unpack.exe not found at $CondaUnpack, skipping." } Write-Host "== Pre-compiling Python bytecode for faster startup ==" $pythonExe = Join-Path $EnvRoot "python.exe" if (Test-Path $pythonExe) { Write-Host "[build_win] Compiling all .py files to .pyc..." $compileStart = Get-Date # Compile all Python files to bytecode # -q: quiet mode (only show errors) # -j 0: use all CPU cores for parallel compilation & $pythonExe -m compileall -q -j 0 $EnvRoot if ($LASTEXITCODE -eq 0) { $compileEnd = Get-Date $compileTime = ($compileEnd - $compileStart).TotalSeconds Write-Host "[build_win] ✓ Bytecode compilation completed in $($compileTime.ToString('F1')) seconds" # Count compiled files for reporting $pycCount = (Get-ChildItem -Path $EnvRoot -Recurse -Filter "*.pyc" -ErrorAction SilentlyContinue | Measure-Object).Count Write-Host "[build_win] Generated $pycCount .pyc files (these will be included in installer)" } else { Write-Host "[build_win] WARN: Bytecode compilation had some errors (exit code: $LASTEXITCODE)" -ForegroundColor Yellow Write-Host "[build_win] This is usually not critical - app will compile on first run" -ForegroundColor Yellow } } else { Write-Host "[build_win] WARN: python.exe not found at $pythonExe, skipping bytecode compilation" -ForegroundColor Yellow } # Main launcher .bat (will be hidden by VBS) $LauncherBat = Join-Path $EnvRoot "PineAgents.bat" @" @echo off cd /d "%~dp0" REM Isolate packaged Python from user site-packages to prevent conflicts set "PYTHONNOUSERSITE=1" REM Preserve system PATH for accessing system commands REM Prepend packaged env to PATH so packaged Python takes precedence set "PATH=%~dp0;%~dp0Scripts;%PATH%" REM Log level: env var QWENPAW_LOG_LEVEL or default to "info" if not defined QWENPAW_LOG_LEVEL set "QWENPAW_LOG_LEVEL=info" REM Set SSL certificate paths for packaged environment REM Use temp file to avoid for /f blocking issue in bat scripts set "CERT_TMP=%TEMP%\qwenpaw_cert_%RANDOM%.txt" "%~dp0python.exe" -u -c "import certifi; print(certifi.where())" > "%CERT_TMP%" 2>nul set /p CERT_FILE=<"%CERT_TMP%" del "%CERT_TMP%" 2>nul if defined CERT_FILE ( if exist "%CERT_FILE%" ( set "SSL_CERT_FILE=%CERT_FILE%" set "REQUESTS_CA_BUNDLE=%CERT_FILE%" set "CURL_CA_BUNDLE=%CERT_FILE%" ) ) if not exist "%USERPROFILE%\.qwenpaw\config.json" ( "%~dp0python.exe" -u -m pineagents init --defaults --accept-security ) "%~dp0python.exe" -u -m pineagents desktop --log-level %QWENPAW_LOG_LEVEL% "@ | Set-Content -Path $LauncherBat -Encoding ASCII # Debug launcher .bat (shows console) $DebugBat = Join-Path $EnvRoot "PineAgents (Debug).bat" @" @echo off cd /d "%~dp0" REM Isolate packaged Python from user site-packages to prevent conflicts set "PYTHONNOUSERSITE=1" REM Preserve system PATH for accessing system commands REM Prepend packaged env to PATH so packaged Python takes precedence set "PATH=%~dp0;%~dp0Scripts;%PATH%" REM Debug mode: use debug log level by default (can override with QWENPAW_LOG_LEVEL) if not defined QWENPAW_LOG_LEVEL set "QWENPAW_LOG_LEVEL=debug" REM Set SSL certificate paths for packaged environment REM Use temp file to avoid for /f blocking issue in bat scripts set "CERT_TMP=%TEMP%\qwenpaw_cert_%RANDOM%.txt" "%~dp0python.exe" -u -c "import certifi; print(certifi.where())" > "%CERT_TMP%" 2>nul set /p CERT_FILE=<"%CERT_TMP%" del "%CERT_TMP%" 2>nul if defined CERT_FILE ( if exist "%CERT_FILE%" ( set "SSL_CERT_FILE=%CERT_FILE%" set "REQUESTS_CA_BUNDLE=%CERT_FILE%" set "CURL_CA_BUNDLE=%CERT_FILE%" ) ) echo ==================================== echo PineAgents - Debug Mode echo ==================================== echo Working Directory: %cd% echo Python: "%~dp0python.exe" echo PATH: %PATH% echo PYTHONNOUSERSITE: %PYTHONNOUSERSITE% echo Log Level: %QWENPAW_LOG_LEVEL% echo SSL_CERT_FILE: %SSL_CERT_FILE% echo REQUESTS_CA_BUNDLE: %REQUESTS_CA_BUNDLE% echo CURL_CA_BUNDLE: %CURL_CA_BUNDLE% echo. if not exist "%USERPROFILE%\.qwenpaw\config.json" ( echo [Init] Creating config... "%~dp0python.exe" -u -m pineagents init --defaults --accept-security ) echo [Launch] Starting PineAgents with log-level=%QWENPAW_LOG_LEVEL%... echo Press Ctrl+C to stop echo. "%~dp0python.exe" -u -m pineagents desktop --log-level %QWENPAW_LOG_LEVEL% echo. echo [Exit] PineAgents closed pause "@ | Set-Content -Path $DebugBat -Encoding ASCII # VBScript launcher (no console window) $LauncherVbs = Join-Path $EnvRoot "PineAgents.vbs" @" Set WshShell = CreateObject("WScript.Shell") batPath = CreateObject("Scripting.FileSystemObject").GetParentFolderName(WScript.ScriptFullName) & "\PineAgents.bat" WshShell.Run Chr(34) & batPath & Chr(34), 0, False Set WshShell = Nothing "@ | Set-Content -Path $LauncherVbs -Encoding ASCII # Create qwenpaw.cmd wrapper in env root so "qwenpaw" resolves to this # instead of Scripts\qwenpaw.exe whose embedded Python path may be stale # after conda-pack/unpack. $QwenpawCmd = Join-Path $EnvRoot "qwenpaw.cmd" @" @"%~dp0python.exe" -u -m pineagents %* "@ | Set-Content -Path $QwenpawCmd -Encoding ASCII # Copy icon.ico to env root so NSIS can find it $IconSrc = Join-Path $PackDir "assets\icon.ico" if (Test-Path $IconSrc) { Copy-Item $IconSrc -Destination $EnvRoot -Force Write-Host "[build_win] Copied icon.ico to env root" } else { Write-Host "[build_win] WARN: icon.ico not found at $IconSrc" } Write-Host "== Building NSIS installer ==" # Debug: Print EnvRoot directory contents Write-Host "=== EnvRoot=$EnvRoot ===" Write-Host "=== EnvRoot top files ===" Get-ChildItem -LiteralPath $EnvRoot -Force | Select-Object -First 50 | ForEach-Object { Write-Host $_.FullName } # Prioritize version from __version__.py to ensure accuracy $Version = $CurrentVersion if (-not $Version) { # Fallback: try to get version from packed env metadata try { $Version = (& (Join-Path $EnvRoot "python.exe") -c "from importlib.metadata import version; print(version('qwenpaw'))" 2>&1) -replace '\s+$', '' Write-Host "[build_win] Using version from packed env metadata: $Version" } catch { Write-Host "[build_win] version from packed env failed: $_" } } if (-not $Version) { $Version = "0.0.0"; Write-Host "[build_win] WARN: Using fallback version 0.0.0" } Write-Host "[build_win] Version determined: $Version" Write-Host "[build_win] QWENPAW_VERSION=$Version OUTPUT_EXE will be under $Dist" $OutInstaller = Join-Path (Join-Path $RepoRoot $Dist) "PineAgents-Setup-$Version.exe" # Pass absolute paths to NSIS (keep backslashes). $UnpackedFull = (Resolve-Path $EnvRoot).Path $OutputExeNsi = [System.IO.Path]::GetFullPath($OutInstaller) $nsiArgs = @( "/DQWENPAW_VERSION=$Version", "/DOUTPUT_EXE=$OutputExeNsi", "/DUNPACKED=$UnpackedFull", $NsiPath ) # Debug: Check if makensis is available Write-Host "=== Checking makensis availability ===" try { $makensisPath = (Get-Command makensis -ErrorAction Stop).Source Write-Host "[build_win] makensis found at: $makensisPath" } catch { throw "makensis not found in PATH. Please install NSIS and ensure makensis.exe is in PATH." } Write-Host "[build_win] Running: makensis $($nsiArgs -join ' ')" Write-Host "=== NSIS will compile from: $NsiPath ===" Write-Host "=== NSIS unpacked source: $UnpackedFull ===" Write-Host "=== NSIS output installer: $OutputExeNsi ===" $nsisOutput = & makensis @nsiArgs 2>&1 | Out-String Write-Host "=== NSIS Output Begin ===" Write-Host $nsisOutput Write-Host "=== NSIS Output End ===" $makensisExit = $LASTEXITCODE Write-Host "[build_win] makensis exit code: $makensisExit" if ($makensisExit -ne 0) { Write-Host "ERROR: makensis compilation failed!" Write-Host "Check the NSIS output above for specific errors." throw "makensis failed with exit code $makensisExit" } if (-not (Test-Path $OutInstaller)) { throw "NSIS did not create installer: $OutInstaller" } Write-Host "== Built $OutInstaller =="