Create Portable Python Package #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # .github/workflows/create_python_release_single.yml | |
| name: Create Portable Python Package | |
| on: | |
| # Allow manual workflow trigger | |
| workflow_dispatch: | |
| inputs: | |
| python_version: | |
| description: 'The Python version to build (e.g., 3.11.9)' | |
| required: true | |
| type: string | |
| # is_prerelease: | |
| # description: 'Mark as a pre-release?' | |
| # type: boolean | |
| # default: false | |
| # is_draft: | |
| # description: 'Create as a draft?' | |
| # type: boolean | |
| # default: false | |
| # Set permissions for the workflow | |
| permissions: | |
| contents: write # Allows creating Releases and uploading Assets | |
| actions: read # Allows reading workflow run info (if needed for tagging) | |
| jobs: | |
| # Job: Create a portable Zip package for the specified Python version | |
| package: | |
| # Specify the runner environment as the latest Windows | |
| runs-on: windows-latest | |
| steps: | |
| # Step 1: Checkout code (if you need to access scripts or files in the repository) | |
| # - uses: actions/checkout@v4 | |
| # Step 2: Determine Installer Info (URL, Type, Name) | |
| - name: Determine Installer Info for ${{ inputs.python_version }} | |
| id: info | |
| shell: pwsh | |
| run: | | |
| $version = "${{ inputs.python_version }}" | |
| $url = "" | |
| $installerType = "" | |
| $installerName = "" | |
| $baseVersion = $version | |
| # Check if it's Python 2.7 | |
| if ($version -eq '2.7.18') { | |
| $installerType = "msi" | |
| $installerName = "python-2.7.18.amd64.msi" | |
| $url = "https://www.python.org/ftp/python/2.7.18/$installerName" | |
| # Check if it's a pre-release version (contains letters) | |
| } elseif ($version -match '\d+\.\d+\.\d+[a-zA-Z]+\d*') { | |
| $installerType = "exe" | |
| $installerName = "python-${version}-amd64.exe" | |
| # Try to extract the base version number (e.g., 3.13.0 from 3.13.0a7) - Note: Python website URL rules may change | |
| $match = [regex]::Match($version, '^(\d+\.\d+\.\d+)') | |
| if ($match.Success) { $baseVersion = $match.Groups[1].Value } | |
| else { Write-Warning "Could not extract base version from pre-release $version, using full version for URL path." } | |
| # The URL for pre-release versions is usually under its base version directory | |
| $url = "https://www.python.org/ftp/python/$baseVersion/$installerName" | |
| # Otherwise, assume it's a standard release version | |
| } else { | |
| $installerType = "exe" | |
| $installerName = "python-${version}-amd64.exe" | |
| $url = "https://www.python.org/ftp/python/$version/$installerName" | |
| } | |
| Write-Host "Version: $version" | |
| Write-Host "Base Version for URL: $baseVersion" | |
| Write-Host "Installer Type: $installerType" | |
| Write-Host "Installer Name: $installerName" | |
| Write-Host "URL: $url" | |
| # Output to the GitHub Actions environment | |
| echo "installer_url=$url" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| echo "installer_name=$installerName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| echo "installer_type=$installerType" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| # Step 3: Download Python Installer | |
| - name: Download Python Installer (${{ inputs.python_version }}) | |
| id: download | |
| shell: pwsh | |
| run: | | |
| $url = "${{ steps.info.outputs.installer_url }}" | |
| $fileName = "${{ steps.info.outputs.installer_name }}" | |
| $downloadPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $fileName | |
| Write-Host "Downloading $url to $downloadPath..." | |
| try { | |
| Invoke-WebRequest -Uri $url -OutFile $downloadPath -ErrorAction Stop | |
| Write-Host "Download complete." | |
| # Output the download path for the uninstall step | |
| echo "installer_path=$downloadPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| } catch { | |
| Write-Error "Failed to download installer from $url. Error: $_" | |
| # Check if the file exists, delete if partially downloaded | |
| if (Test-Path $downloadPath) { Remove-Item $downloadPath -Force } | |
| exit 1 | |
| } | |
| # Step 4: Attempt Silent Uninstall of Existing Python | |
| - name: Attempt Silent Uninstall of Existing Python (${{ inputs.python_version }}) | |
| # Depends on the info and download steps | |
| if: always() && steps.info.outputs.installer_name && steps.download.outputs.installer_path | |
| shell: pwsh | |
| run: | | |
| $installerName = "${{ steps.info.outputs.installer_name }}" | |
| $installerType = "${{ steps.info.outputs.installer_type }}" | |
| $installerSourcePath = "${{ steps.download.outputs.installer_path }}" | |
| $version = "${{ inputs.python_version }}" | |
| # Define uninstall log paths | |
| $uninstallLogFile = "$env:GITHUB_WORKSPACE\uninstall_${version}.log" | |
| $uninstallMsiLogFile = "$env:GITHUB_WORKSPACE\uninstall_${version}_msi.log" | |
| Write-Host "Attempting silent uninstall of any existing Python $version using '$installerSourcePath'..." | |
| # Check if the downloaded installer exists | |
| if (-not (Test-Path $installerSourcePath)) { | |
| Write-Warning "Installer file '$installerSourcePath' not found (download might have failed). Skipping uninstall attempt." | |
| # Don't abort, continue to the next steps | |
| exit 0 | |
| } | |
| $arguments = "" | |
| $process = $null | |
| $exitCode = -1 | |
| try { | |
| if ($installerType -eq "msi") { | |
| # MSI uninstall uses the /x switch and the MSI file path | |
| $arguments = "/x `"$installerSourcePath`" /qn /L*v `"$uninstallMsiLogFile`"" | |
| Write-Host "Running: msiexec.exe $arguments" | |
| $process = Start-Process msiexec.exe -ArgumentList $arguments -Wait -NoNewWindow -PassThru -ErrorAction Stop | |
| } elseif ($installerType -eq "exe") { | |
| # EXE uninstall uses the /uninstall switch | |
| # Note: Some older versions might not support the /log parameter for uninstallation | |
| $arguments = "/uninstall /quiet /log `"$uninstallLogFile`"" | |
| Write-Host "Running: $installerSourcePath $arguments" | |
| $process = Start-Process -FilePath $installerSourcePath -ArgumentList $arguments -Wait -NoNewWindow -PassThru -ErrorAction Stop | |
| } else { | |
| Write-Warning "Unknown installer type '$installerType'. Skipping uninstall attempt." | |
| # Don't abort, continue to the next steps | |
| exit 0 | |
| } | |
| $exitCode = $process.ExitCode | |
| # Check the exit code | |
| # 0: Success | |
| # 1605: ERROR_UNKNOWN_PRODUCT (MSI - indicates not installed, which is acceptable) | |
| # 3010: ERROR_SUCCESS_REBOOT_REQUIRED (success but reboot needed, acceptable) | |
| # Other: Warning | |
| if ($exitCode -eq 0) { | |
| Write-Host "Silent uninstall command completed successfully (ExitCode: 0)." | |
| } elseif ($exitCode -eq 1605) { | |
| Write-Host "Silent uninstall command indicated product not installed (ExitCode: $exitCode). This is expected if Python wasn't present." | |
| } elseif ($exitCode -eq 3010) { | |
| Write-Host "Silent uninstall completed successfully, but may require a reboot (ExitCode: 3010). Continuing..." | |
| } else { | |
| Write-Warning "Silent uninstall command finished with unexpected ExitCode: $exitCode. Attempting to proceed with installation anyway." | |
| # Try to display the uninstall log (if it exists) | |
| if ($installerType -eq "exe" -and (Test-Path $uninstallLogFile)) { | |
| Write-Host "--- Uninstall Log ($uninstallLogFile) ---" | |
| Get-Content $uninstallLogFile -ErrorAction SilentlyContinue | |
| Write-Host "--- End Uninstall Log ---" | |
| } elseif ($installerType -eq "msi" -and (Test-Path $uninstallMsiLogFile)) { | |
| Write-Host "--- Uninstall Log ($uninstallMsiLogFile) ---" | |
| Get-Content $uninstallMsiLogFile -Encoding utf8 -Raw -ErrorAction SilentlyContinue | |
| Write-Host "--- End Uninstall Log ---" | |
| } | |
| } | |
| } catch { | |
| # Catch errors when starting or waiting for the process | |
| Write-Warning "Failed to execute or wait for the uninstall process: $_. Attempting to proceed with installation anyway." | |
| } | |
| Write-Host "Uninstall attempt finished. Proceeding to installation." | |
| # Do not abort the workflow here (exit 0 is implicit) | |
| # Step 5: Install Python (with RDP Fallback and user creation logic) | |
| - name: Install Python (${{ inputs.python_version }}) with RDP Fallback | |
| id: install | |
| # Depends on the info and download steps | |
| if: always() && steps.info.outputs.installer_name && steps.download.outputs.installer_path | |
| shell: pwsh | |
| env: | |
| # Pass ngrok Authtoken and RDP password from Secrets | |
| NGROK_AUTH_TOKEN: ${{ secrets.NGROK_AUTH_TOKEN }} | |
| RDP_PASSWORD: ${{ secrets.RDP_PASSWORD }} | |
| run: | | |
| $version = "${{ inputs.python_version }}" | |
| $installerName = "${{ steps.info.outputs.installer_name }}" | |
| $installerType = "${{ steps.info.outputs.installer_type }}" | |
| $installerSourcePath = "${{ steps.download.outputs.installer_path }}" | |
| $Architecture = "x64" | |
| $ngrokLogFile = "$env:GITHUB_WORKSPACE\ngrok.log" | |
| $ngrokProcess = $null | |
| $rdpUser = "runneradmin" | |
| # --- Define Installation Path --- | |
| $ToolcacheRoot = $env:RUNNER_TOOL_CACHE | |
| if ([string]::IsNullOrEmpty($ToolcacheRoot)) { Write-Error "RUNNER_TOOL_CACHE not set."; exit 1 } | |
| $PythonToolcachePath = Join-Path -Path $ToolcacheRoot -ChildPath "Python" | |
| $PythonVersionPath = Join-Path -Path $PythonToolcachePath -ChildPath $version | |
| $PythonArchPath = Join-Path -Path $PythonVersionPath -ChildPath $Architecture | |
| $PythonExePath = Join-Path -Path $PythonArchPath -ChildPath "python.exe" | |
| # --- Define Installation Log File Paths --- | |
| $installLogFile = "$env:GITHUB_WORKSPACE\install_${version}.log" | |
| $installMsiLogFile = "$env:GITHUB_WORKSPACE\install_${version}_msi.log" | |
| # --- Clean and Create Target Directory --- | |
| # Note: The uninstall step attempts to clean the system installation, this step cleans the target directory in the Runner Tool Cache | |
| if (Test-Path $PythonArchPath) { | |
| Write-Host "Removing existing target directory in ToolCache: $PythonArchPath" | |
| Remove-Item -Path $PythonArchPath -Recurse -Force -ErrorAction SilentlyContinue | |
| } | |
| Write-Host "Ensuring target directory exists: $PythonArchPath" | |
| New-Item -ItemType Directory -Path $PythonArchPath -Force | Out-Null | |
| # --- Check Installer Source File --- | |
| if (-not (Test-Path $installerSourcePath)) { | |
| Write-Error "Installer source file '$installerSourcePath' not found for installation. Download might have failed earlier." | |
| exit 1 | |
| } | |
| # --- Attempt Silent Installation --- | |
| Write-Host "Attempting silent installation of Python $version to $PythonArchPath..." | |
| $arguments = "" | |
| $process = $null | |
| $exitCode = -1 | |
| try { | |
| if ($installerType -eq "msi") { | |
| # MSI installation uses the /i switch | |
| $arguments = "/i `"$installerSourcePath`" /qn TARGETDIR=`"$PythonArchPath`" ALLUSERS=1 /L*v `"$installMsiLogFile`"" | |
| Write-Host "Running: msiexec.exe $arguments" | |
| $process = Start-Process msiexec.exe -ArgumentList $arguments -Wait -NoNewWindow -PassThru -ErrorAction Stop | |
| } elseif ($installerType -eq "exe") { | |
| # EXE installation specifies target directory and other parameters | |
| $arguments = "DefaultAllUsersTargetDir=`"$PythonArchPath`" InstallAllUsers=1 /quiet /log `"$installLogFile`"" | |
| Write-Host "Running: $installerSourcePath $arguments" | |
| $process = Start-Process -FilePath $installerSourcePath -ArgumentList $arguments -Wait -NoNewWindow -PassThru -ErrorAction Stop | |
| } else { | |
| Write-Error "Unknown installer type: $installerType"; exit 1 | |
| } | |
| $exitCode = $process.ExitCode | |
| } catch { | |
| Write-Warning "Failed to start or wait for installer process: $_" | |
| $exitCode = -2 | |
| } | |
| # --- Post-Installation Verification --- | |
| $installationVerified = Test-Path $PythonExePath | |
| Write-Host "Checking for python.exe at $PythonExePath... Found: $installationVerified" | |
| # --- RDP Fallback Logic --- | |
| if (($exitCode -ne 0 -and $exitCode -ne 3010) -or (-not $installationVerified)) { | |
| if ($exitCode -ne 3010) { | |
| Write-Warning "Silent installation failed (ExitCode: $exitCode) or python.exe not found. Attempting RDP fallback." | |
| } else { | |
| Write-Host "Silent installation requires reboot (ExitCode: 3010), but python.exe was found. Treating as success for now." | |
| # If python.exe IS found despite 3010, skip RDP fallback | |
| if ($installationVerified) { | |
| Write-Host "Skipping RDP fallback as python.exe exists." | |
| # Jump to Pip installation logic needs restructuring, or just let it continue below | |
| } else { | |
| Write-Warning "Silent installation requires reboot (ExitCode: 3010), AND python.exe was NOT found. Attempting RDP fallback." | |
| # Proceed with RDP fallback logic below | |
| } | |
| } | |
| # Only proceed with RDP if *really* needed (failed or needs reboot *and* python.exe is missing) | |
| if (($exitCode -ne 0 -and $exitCode -ne 3010) -or ($exitCode -eq 3010 -and -not $installationVerified)) { | |
| # Display installation logs (if available and installation failed) | |
| if ($exitCode -ne 0 -and $exitCode -ne 3010) { | |
| Write-Host "Displaying installation logs (if available)..." | |
| if ($installerType -eq "exe" -and (Test-Path $installLogFile)) { | |
| Write-Host "--- Installer Log ($installLogFile) ---" | |
| Get-Content $installLogFile -ErrorAction SilentlyContinue | |
| Write-Host "--- End Installer Log ---" | |
| } elseif ($installerType -eq "msi" -and (Test-Path $installMsiLogFile)) { | |
| Write-Host "--- Installer Log ($installMsiLogFile) ---" | |
| Get-Content $installMsiLogFile -Encoding utf8 -Raw -ErrorAction SilentlyContinue | |
| Write-Host "--- End Installer Log ---" | |
| } else { Write-Warning "Installer log file not found or not applicable for this failure." } | |
| } | |
| # Check if NGROK_AUTH_TOKEN and RDP_PASSWORD are set | |
| if ([string]::IsNullOrEmpty($env:NGROK_AUTH_TOKEN)) { | |
| Write-Error "NGROK_AUTH_TOKEN secret is not set. Cannot start RDP session." | |
| exit 1 | |
| } | |
| if ([string]::IsNullOrEmpty($env:RDP_PASSWORD)) { | |
| Write-Error "RDP_PASSWORD secret is not set. Cannot create RDP user." | |
| exit 1 | |
| } | |
| # --- Create RDP User and Set Password --- | |
| Write-Host "Creating RDP user '$rdpUser'..." | |
| try { | |
| Write-Host "Setting password for user '$rdpUser'..." | |
| $securePassword = ConvertTo-SecureString -String $env:RDP_PASSWORD -AsPlainText -Force | |
| try { Get-LocalUser -Name $rdpUser | Set-LocalUser -Password $securePassword -ErrorAction Stop; Write-Host "Password set via Set-LocalUser."} | |
| catch { Write-Warning "Set-LocalUser failed: $($_.Exception.Message). Trying 'net user'..."; iex "net user $rdpUser '$($env:RDP_PASSWORD)'"; if ($LASTEXITCODE -ne 0) { throw "net user failed too."}; Write-Host "Password set via 'net user'." } | |
| } catch { | |
| Write-Error "Failed to create or configure RDP user '$rdpUser': $_" | |
| if (Get-LocalUser -Name $rdpUser -ErrorAction SilentlyContinue) { Remove-LocalUser -Name $rdpUser -ErrorAction SilentlyContinue } | |
| exit 1 | |
| } | |
| Write-Host "Ensuring RDP is enabled..." | |
| try { | |
| Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -Value 0 -Force -ErrorAction Stop | |
| Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction Stop | |
| Write-Host "RDP enabled and firewall rule checked." | |
| } catch { Write-Warning "Failed to explicitly enable RDP or firewall rule: $_. Assuming it's already configured." } | |
| Write-Host "Setting up ngrok..." | |
| try { choco install ngrok -y --force --no-progress --ignore-checksums } | |
| catch { | |
| Write-Warning "Chocolatey install failed or choco not found. Attempting manual download..." | |
| $ngrokZip = "$env:TEMP\ngrok.zip"; $ngrokExe = "$env:TEMP\ngrok.exe" | |
| Invoke-WebRequest "https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-windows-amd64.zip" -OutFile $ngrokZip | |
| Expand-Archive $ngrokZip -DestinationPath $env:TEMP -Force | |
| Move-Item "$env:TEMP\ngrok.exe" $ngrokExe -Force | |
| Remove-Item $ngrokZip -Force | |
| $env:PATH += ";$env:TEMP" | |
| } | |
| if (-not (Get-Command ngrok -ErrorAction SilentlyContinue)) { Write-Error "ngrok command not found after installation attempts."; Remove-LocalUser -Name $rdpUser -ErrorAction SilentlyContinue; exit 1 } | |
| Write-Host "Configuring ngrok authtoken..." | |
| ngrok config add-authtoken $env:NGROK_AUTH_TOKEN --log=stdout | |
| Write-Host "Starting ngrok RDP tunnel (TCP port 3389)..." | |
| $ngrokArgs = "tcp 3389 --log `"$ngrokLogFile`"" | |
| try { | |
| $ngrokProcess = Start-Process ngrok -ArgumentList $ngrokArgs -WindowStyle Hidden -PassThru -ErrorAction Stop | |
| Write-Host "ngrok process started (PID: $($ngrokProcess.Id)). Waiting for tunnel info..." | |
| Start-Sleep -Seconds 15 | |
| $rdpUrl = $null; $maxAttempts = 5; $attempt = 0 | |
| while ($attempt -lt $maxAttempts -and -not $rdpUrl) { | |
| $attempt++; if (Test-Path $ngrokLogFile) { $logContent = Get-Content $ngrokLogFile -Raw -ErrorAction SilentlyContinue; $match = $logContent | Select-String -Pattern 'url=(tcp://[^ ]+)'; if ($match) { $rdpUrl = $match.Matches[0].Groups[1].Value; Write-Host "RDP Connection URL found: $rdpUrl"; break } } | |
| Write-Host "Waiting for ngrok URL in log... (Attempt $attempt/$maxAttempts)"; Start-Sleep -Seconds 5 | |
| } | |
| if (-not $rdpUrl) { Write-Error "Failed to retrieve RDP connection URL from ngrok log ($ngrokLogFile) after $maxAttempts attempts."; if ($ngrokProcess) { Stop-Process -Id $ngrokProcess.Id -Force -ErrorAction SilentlyContinue }; Remove-LocalUser -Name $rdpUser -ErrorAction SilentlyContinue; exit 1 } | |
| Write-Host "----------------------------------------------------------------------" | |
| Write-Host "ACTION REQUIRED: Manual installation needed via RDP." | |
| Write-Host "Connect using an RDP client to: $rdpUrl" | |
| Write-Host "Username: $rdpUser" | |
| Write-Host "Password: Use the value from your RDP_PASSWORD secret." | |
| Write-Host "The installer is located at: $installerSourcePath" | |
| Write-Host "Install Python to the target directory: $PythonArchPath" | |
| Write-Host "The workflow will wait for python.exe to appear in the target directory." | |
| Write-Host "Timeout: 30 minutes." | |
| Write-Host "----------------------------------------------------------------------" | |
| } catch { Write-Error "Failed to start ngrok process: $_"; if ($ngrokProcess) { Stop-Process -Id $ngrokProcess.Id -Force -ErrorAction SilentlyContinue }; Remove-LocalUser -Name $rdpUser -ErrorAction SilentlyContinue; exit 1 } | |
| $timeoutMinutes = 30; $checkIntervalSeconds = 15; $startTime = Get-Date; $timedOut = $false | |
| Write-Host "Waiting for python.exe to appear at '$PythonExePath'..." | |
| while (-not (Test-Path $PythonExePath)) { | |
| $elapsedTime = (Get-Date) - $startTime; if ($elapsedTime.TotalMinutes -ge $timeoutMinutes) { $timedOut = $true; Write-Error "Timeout reached ($timeoutMinutes minutes). python.exe was not found."; break } | |
| Write-Host "($([int]$elapsedTime.TotalSeconds)s / $($timeoutMinutes * 60)s) Still waiting for python.exe..."; Start-Sleep -Seconds $checkIntervalSeconds | |
| } | |
| Write-Host "Stopping ngrok process..."; if ($ngrokProcess) { Stop-Process -Id $ngrokProcess.Id -Force -ErrorAction SilentlyContinue; Write-Host "ngrok process (PID: $($ngrokProcess.Id)) stopped." } else { Write-Warning "Could not find ngrok process object to stop it directly. Attempting taskkill."; taskkill /F /IM ngrok.exe /T | Out-Null } | |
| if ($timedOut) { if (Test-Path $PythonArchPath) { Remove-Item -Recurse -Force $PythonArchPath -ErrorAction SilentlyContinue }; Remove-LocalUser -Name $rdpUser -ErrorAction SilentlyContinue; exit 1 } | |
| Write-Host "python.exe detected! Manual installation assumed complete." | |
| Write-Host "Waiting 3 minutes to ensure all processes finalize..." | |
| Start-Sleep -Seconds 180 | |
| $installationVerified = Test-Path $PythonExePath | |
| if (-not $installationVerified) { Write-Error "VERIFICATION FAILED even after RDP intervention: python.exe not found at '$PythonExePath'."; if (Test-Path $PythonArchPath) { Remove-Item -Recurse -Force $PythonArchPath -ErrorAction SilentlyContinue }; Remove-LocalUser -Name $rdpUser -ErrorAction SilentlyContinue; exit 1 } | |
| else { Write-Host "Verification successful after RDP intervention."; } | |
| } # End of RDP fallback block | |
| # --- If initial installation or RDP intervention was successful --- | |
| } else { | |
| if ($exitCode -eq 3010) { | |
| Write-Host "Initial installation completed successfully but requires reboot (ExitCode: 3010). python.exe found at '$PythonExePath'." | |
| } else { | |
| Write-Host "Initial verification successful: python.exe found at '$PythonExePath'." | |
| } | |
| } | |
| # --- Pip Installation/Upgrade --- | |
| # Ensure pip is installed here, whether the initial install or RDP succeeded | |
| Write-Host "Ensuring pip is installed/upgraded..." | |
| if (Test-Path $PythonExePath) { | |
| # 1. Run ensurepip first | |
| Write-Host "Running ensurepip..." | |
| $argumentsEnsure = "-m ensurepip" | |
| $processEnsure = $null | |
| try { | |
| # Execute ensurepip using Start-Process | |
| $processEnsure = Start-Process -FilePath $PythonExePath -ArgumentList $argumentsEnsure -Wait -NoNewWindow -PassThru -ErrorAction Stop | |
| if ($processEnsure.ExitCode -ne 0) { | |
| # Throw an error if ensurepip fails | |
| throw "ensurepip failed with exit code $($processEnsure.ExitCode)." | |
| } | |
| Write-Host "ensurepip completed successfully." | |
| # 2. Run pip install/upgrade only if ensurepip succeeded | |
| Write-Host "Upgrading pip..." | |
| $argumentsUpgrade = "-m pip install --upgrade --force-reinstall pip --no-warn-script-location" | |
| $processUpgrade = $null | |
| # Execute pip upgrade using Start-Process | |
| $processUpgrade = Start-Process -FilePath $PythonExePath -ArgumentList $argumentsUpgrade -Wait -NoNewWindow -PassThru -ErrorAction Stop | |
| if ($processUpgrade.ExitCode -ne 0) { | |
| # Throw an error if pip upgrade fails | |
| throw "pip upgrade failed with exit code $($processUpgrade.ExitCode)." | |
| } | |
| Write-Host "Pip upgraded successfully." | |
| } catch { | |
| # Catch errors from either Start-Process call or non-zero exit codes | |
| Write-Warning "Pip installation/upgrade command failed: $_" | |
| # Decide if this failure should stop the workflow | |
| # exit 1 # Uncomment this line if a pip failure should be fatal | |
| } | |
| } else { | |
| Write-Error "Cannot proceed with pip installation because Python executable '$PythonExePath' was not found after all attempts." | |
| exit 1 | |
| } | |
| # Output the installation directory path for the subsequent packaging step | |
| echo "install_dir=$PythonArchPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| # Step 6: Package the Installed Python Directory | |
| - name: Package Installed Python (${{ inputs.python_version }}) | |
| id: package | |
| if: success() && steps.install.outputs.install_dir | |
| shell: pwsh | |
| run: | | |
| $installDir = "${{ steps.install.outputs.install_dir }}" | |
| $zipFileName = "python-${{ inputs.python_version }}-win-x64.zip" | |
| $destinationPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $zipFileName | |
| if (-not (Test-Path $installDir)) { Write-Error "Installation directory not found at '$installDir'. Cannot package."; exit 1 } | |
| Write-Host "Listing contents of installation directory '$installDir' before packaging:" | |
| Get-ChildItem -Path $installDir -Recurse -Depth 1 | Out-String | |
| $itemCount = (Get-ChildItem -Path $installDir).Count | |
| if ($itemCount -eq 0) { Write-Error "Installation directory '$installDir' is empty. Cannot package."; exit 1 } | |
| # Copy python27.dll for Python 2.7 if needed | |
| $version = "${{ inputs.python_version }}" | |
| if ($version -eq '2.7.18') { | |
| $python27DllSource = "C:\Windows\System32\python27.dll" | |
| $python27DllDest = Join-Path -Path $installDir -ChildPath "python27.dll" | |
| if (Test-Path $python27DllSource) { | |
| Write-Host "Copying python27.dll from '$python27DllSource' to '$python27DllDest'..." | |
| try { | |
| Copy-Item -Path $python27DllSource -Destination $python27DllDest -Force -ErrorAction Stop | |
| Write-Host "python27.dll copied successfully." | |
| } catch { | |
| Write-Warning "Failed to copy python27.dll: $_. The package may not work properly on systems without Python 2.7 installed." | |
| } | |
| } else { | |
| Write-Warning "python27.dll not found at '$python27DllSource'. The package may not work properly on systems without Python 2.7 installed." | |
| } | |
| } | |
| Write-Host "Compressing installed Python from '$installDir' to '$destinationPath'..." | |
| $parentDir = Split-Path $installDir -Parent; $dirName = Split-Path $installDir -Leaf | |
| Push-Location $parentDir | |
| try { Compress-Archive -Path $dirName -DestinationPath $destinationPath -Force -ErrorAction Stop } | |
| catch { Write-Error "Failed to compress archive: $_"; Pop-Location; exit 1 } | |
| Pop-Location | |
| if (-not (Test-Path $destinationPath)) { Write-Error "Failed to create zip file at '$destinationPath'."; exit 1 } | |
| Write-Host "Packaging complete: $destinationPath" | |
| echo "zip_name=$zipFileName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| echo "zip_path=$destinationPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| # Step 7: Create Release and Upload Asset (for the current version) | |
| - name: Create Release and Upload Asset for ${{ inputs.python_version }} | |
| uses: softprops/action-gh-release@v2 | |
| if: success() && steps.package.outputs.zip_path | |
| with: | |
| tag_name: python-${{ inputs.python_version }}-win-x64 | |
| name: Portable Python ${{ inputs.python_version }} for Windows x64 | |
| body: | | |
| Python ${{ inputs.python_version }} for Windows x64 Portable (ServBay). | |
| draft: false # ${{ inputs.is_draft || false }} | |
| prerelease: ${{ contains(inputs.python_version, 'a') || contains(inputs.python_version, 'b') || contains(inputs.python_version, 'rc') }} | |
| files: ${{ steps.package.outputs.zip_path }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Step 8: Clean up Installation (remove ToolCache, installer, logs) | |
| - name: Clean up Installation (${{ inputs.python_version }}) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $installDir = "${{ steps.install.outputs.install_dir }}" | |
| $installerSourcePath = "${{ steps.download.outputs.installer_path }}" | |
| $zipPath = "${{ steps.package.outputs.zip_path }}" | |
| if ($installDir -and (Test-Path $installDir)) { | |
| Write-Host "Removing installed directory from ToolCache: $installDir" | |
| Remove-Item -Recurse -Force $installDir -ErrorAction SilentlyContinue | |
| } else { | |
| Write-Host "Install directory path not found or directory doesn't exist, skipping removal." | |
| } | |
| if ($installerSourcePath -and (Test-Path $installerSourcePath)) { | |
| Write-Host "Removing downloaded installer file: $installerSourcePath" | |
| Remove-Item -Force $installerSourcePath -ErrorAction SilentlyContinue | |
| } | |
| if ($zipPath -and (Test-Path $zipPath)) { | |
| Write-Host "Removing packaged zip file: $zipPath" | |
| Remove-Item -Force $zipPath -ErrorAction SilentlyContinue | |
| } | |
| Write-Host "Removing installation, uninstallation and ngrok log files (if any)..." | |
| Remove-Item -Path "$env:GITHUB_WORKSPACE\install_*.log" -ErrorAction SilentlyContinue | |
| Remove-Item -Path "$env:GITHUB_WORKSPACE\install_*_msi.log" -ErrorAction SilentlyContinue | |
| Remove-Item -Path "$env:GITHUB_WORKSPACE\uninstall_*.log" -ErrorAction SilentlyContinue | |
| Remove-Item -Path "$env:GITHUB_WORKSPACE\uninstall_*_msi.log" -ErrorAction SilentlyContinue | |
| Remove-Item -Path "$env:GITHUB_WORKSPACE\ngrok.log" -ErrorAction SilentlyContinue | |
| Write-Host "Cleanup finished." |