-
Notifications
You must be signed in to change notification settings - Fork 0
523 lines (470 loc) · 31 KB
/
package_python_install.yml
File metadata and controls
523 lines (470 loc) · 31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# .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."