Create Portable Python Package #8
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: | |
| # 允许手动触发工作流 | |
| workflow_dispatch: | |
| inputs: | |
| python_version: | |
| description: '要构建的 Python 版本 (例如 3.11.9)' | |
| required: true | |
| type: string | |
| # is_prerelease: | |
| # description: '是否标记为预发布版本?' | |
| # type: boolean | |
| # default: false | |
| # is_draft: | |
| # description: '是否创建为草稿?' | |
| # type: boolean | |
| # default: false | |
| # 为工作流设置权限 | |
| permissions: | |
| contents: write # 允许创建 Release 和上传 Assets | |
| actions: read # 允许读取 workflow run 相关信息 (如果需要用于 tag) | |
| jobs: | |
| # 作业:为指定的 Python 版本创建便携 Zip 包 | |
| package: | |
| # 指定运行环境为最新的 Windows | |
| runs-on: windows-latest | |
| steps: | |
| # 步骤 1: 检出代码 (如果需要访问仓库中的脚本或文件) | |
| # - uses: actions/checkout@v4 | |
| # 步骤 2: 确定安装包信息 (URL, 类型, 名称) | |
| - name: Determine Installer Info for ${{ inputs.python_version }} | |
| id: info | |
| shell: pwsh | |
| run: | | |
| $version = "${{ inputs.python_version }}" | |
| $url = "" | |
| $installerType = "" | |
| $installerName = "" | |
| $baseVersion = $version | |
| # 检查是否为 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" | |
| # 检查是否为预发布版本 (包含字母) | |
| } elseif ($version -match '\d+\.\d+\.\d+[a-zA-Z]+\d*') { | |
| $installerType = "exe" | |
| $installerName = "python-${version}-amd64.exe" | |
| # 尝试提取基础版本号 (例如 3.13.0 从 3.13.0a7) - 注意: Python 官网路径规则可能变化 | |
| $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." } | |
| # 预发布版本的 URL 通常在其基础版本目录下 | |
| $url = "https://www.python.org/ftp/python/$baseVersion/$installerName" | |
| # 否则认为是标准发布版本 | |
| } 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" | |
| # 输出到 GitHub Actions 环境 | |
| 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 | |
| # 步骤 3: 下载 Python 安装包 | |
| - 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: $_" | |
| # 检查文件是否存在,如果部分下载则删除 | |
| if (Test-Path $downloadPath) { Remove-Item $downloadPath -Force } | |
| exit 1 | |
| } | |
| # 步骤 4: 尝试静默卸载已存在的 Python 版本 | |
| - name: Attempt Silent Uninstall of Existing Python (${{ inputs.python_version }}) | |
| # 依赖于 info 和 download 步骤 | |
| 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 }}" | |
| # 定义卸载日志路径 | |
| $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'..." | |
| # 检查下载的安装包是否存在 | |
| if (-not (Test-Path $installerSourcePath)) { | |
| Write-Warning "Installer file '$installerSourcePath' not found (download might have failed). Skipping uninstall attempt." | |
| # 不中止,继续执行后续步骤 | |
| exit 0 | |
| } | |
| $arguments = "" | |
| $process = $null | |
| $exitCode = -1 | |
| try { | |
| if ($installerType -eq "msi") { | |
| # MSI 卸载使用 /x 开关和 MSI 文件路径 | |
| $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 开关 | |
| # 注意:某些旧版本可能不支持 /log 参数进行卸载 | |
| $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." | |
| # 不中止,继续执行后续步骤 | |
| exit 0 | |
| } | |
| $exitCode = $process.ExitCode | |
| # 检查退出码 | |
| # 0: 成功 | |
| # 1605: ERROR_UNKNOWN_PRODUCT (MSI - 表示未安装,这是可接受的) | |
| # 3010: ERROR_SUCCESS_REBOOT_REQUIRED (成功但需要重启,可接受) | |
| # 其他: 警告 | |
| 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." | |
| # 尝试显示卸载日志(如果存在) | |
| 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 { | |
| # 捕获启动或等待进程时的错误 | |
| 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." | |
| # 不在此处中止工作流 (exit 0 隐式执行) | |
| # 步骤 5: 安装 Python (包含 RDP 回退和用户创建逻辑) | |
| - name: Install Python (${{ inputs.python_version }}) with RDP Fallback | |
| id: install | |
| # 依赖于 info 和 download 步骤 | |
| if: always() && steps.info.outputs.installer_name && steps.download.outputs.installer_path | |
| shell: pwsh | |
| env: | |
| # 从 Secrets 传入 ngrok Authtoken 和 RDP 密码 | |
| 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" | |
| # --- 定义安装路径 --- | |
| $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" | |
| # --- 定义安装日志文件路径 --- | |
| $installLogFile = "$env:GITHUB_WORKSPACE\install_${version}.log" | |
| $installMsiLogFile = "$env:GITHUB_WORKSPACE\install_${version}_msi.log" | |
| # --- 清理和创建目标目录 --- | |
| # 注意:卸载步骤尝试清理系统安装,这里清理的是 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 | |
| # --- 检查安装包源文件 --- | |
| if (-not (Test-Path $installerSourcePath)) { | |
| Write-Error "Installer source file '$installerSourcePath' not found for installation. Download might have failed earlier." | |
| exit 1 | |
| } | |
| # --- 尝试静默安装 --- | |
| Write-Host "Attempting silent installation of Python $version to $PythonArchPath..." | |
| $arguments = "" | |
| $process = $null | |
| $exitCode = -1 | |
| try { | |
| if ($installerType -eq "msi") { | |
| # MSI 安装使用 /i 开关 | |
| $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 安装指定目标目录等参数 | |
| $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 | |
| } | |
| # --- 安装后验证 --- | |
| $installationVerified = Test-Path $PythonExePath | |
| Write-Host "Checking for python.exe at $PythonExePath... Found: $installationVerified" | |
| # --- RDP 回退逻辑 --- | |
| 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)) { | |
| # 输出安装日志 (如果存在且安装失败) | |
| 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." } | |
| } | |
| # 检查 NGROK_AUTH_TOKEN 和 RDP_PASSWORD 是否设置 | |
| 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 | |
| } | |
| # --- 创建 RDP 用户并设置密码 --- | |
| 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 | |
| # --- 如果初始安装或 RDP 后验证成功 --- | |
| } 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 --- | |
| # 确保在此处安装 Pip,无论初始安装成功还是 RDP 后成功 | |
| 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 | |
| } | |
| # 输出安装目录路径,供后续打包步骤使用 | |
| echo "install_dir=$PythonArchPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append | |
| # 步骤 6: 打包已安装的 Python 目录 | |
| - 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 } | |
| 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 | |
| # 步骤 7: 创建 Release 并上传 Asset (为当前版本) | |
| - 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 }} | |
| # 步骤 8: 清理安装(删除 ToolCache、安装包、日志) | |
| - 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." |