Skip to content

Publish PyKotor Tools CI/CD #143

Publish PyKotor Tools CI/CD

Publish PyKotor Tools CI/CD #143

name: Publish PyKotor Tools CI/CD
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
OS_RUNNERS_JSON: '["windows-2019", "ubuntu-20.04", "macos-12"]'
PYTHON_VERSIONS_JSON: '["3.8", "3.9", "3.10", "3.11", "3.12"]'
ARCHITECTURES_JSON: '["x86", "x64"]'
INTERPRETERS_JSON: '["python"]'
UPX_VERSION: '4.2.2'
USE_UPX: true
BUILD_BOOTLOADERS: false # TODO(th3w1zard1):
on:
# push:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'
permissions:
contents: write
jobs:
setup:
runs-on: ubuntu-latest
outputs:
os: ${{ steps.set_env.outputs.os }}
python-version: ${{ steps.set_env.outputs.python-version }}
architecture: ${{ steps.set_env.outputs.architecture }}
python-pypy: ${{ steps.set_env.outputs.python-pypy }}
#qt_version: ${#{ steps.set_env.outputs.qt_version }}
steps:
- name: Set environment variables
id: set_env
run: |
$singleLineJson = '${{ env.OS_RUNNERS_JSON }}' -replace "`r", "" -replace "`n", ""
echo "os<<EOF" >> $env:GITHUB_OUTPUT
echo $singleLineJson >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
$singleLineJson = '${{ env.PYTHON_VERSIONS_JSON }}' -replace "`r", "" -replace "`n", ""
echo "python-version<<EOF" >> $env:GITHUB_OUTPUT
echo $singleLineJson >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
$singleLineJson = '${{ env.ARCHITECTURES_JSON }}' -replace "`r", "" -replace "`n", ""
echo "architecture<<EOF" >> $env:GITHUB_OUTPUT
echo $singleLineJson >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
$singleLineJson = '${{ env.INTERPRETERS_JSON }}' -replace "`r", "" -replace "`n", ""
echo "python-pypy<<EOF" >> $env:GITHUB_OUTPUT
echo $singleLineJson >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
shell: pwsh
build:
needs: setup
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # Disable automatic cancellation of other jobs
matrix:
os: ${{ fromJson(needs.setup.outputs.os) }}
python-version: ${{ fromJson(needs.setup.outputs.python-version) }}
architecture: ${{ fromJson(needs.setup.outputs.architecture) }}
python_pypy: ${{ fromJson(needs.setup.outputs.python-pypy) }}
qt_version: ['pyqt5', 'pyqt6', 'pyside2', 'pyside6']
include:
- arch: x86
vc_redist2015: "https://download.microsoft.com/download/9/3/F/93FCF1E7-E6A4-478B-96E7-D4B285925B00/vc_redist.x86.exe"
vc_redist-latest: "https://aka.ms/vs/17/release/vc_redist.x86.exe"
vc_redist2019: "https://aka.ms/vs/17/release/vc_redist.x86.exe"
- arch: x64
vc_redist2015: "https://download.microsoft.com/download/9/3/F/93FCF1E7-E6A4-478B-96E7-D4B285925B00/vc_redist.x64.exe"
vc_redist-latest: "https://aka.ms/vs/17/release/vc_redist.x64.exe"
vc_redist2019: "https://aka.ms/vs/17/release/vc_redist.x64.exe"
exclude:
# unix x86 is definitely not supported.
- os: ubuntu-20.04
architecture: x86
- os: macos-12
architecture: x86
# latest PyPy version as of 17/03/2024 is 3.10.
- python_pypy: 'pypy'
python-version: '3.11'
- python_pypy: 'pypy'
python-version: '3.12'
# PyQt5 doesn't seem to be compatible with PyPy
- python_pypy: "pypy"
qt_version: 'pyqt5'
- python_pypy: "pypy"
qt_version: 'pyside2'
- python_pypy: "pypy"
qt_version: 'pyqt6'
- python_pypy: "pypy"
python-version: '3.10'
os: 'windows-2019'
steps:
- name: Determine Python version string
id: set-python-version
run: |
if ( "${{ matrix.python_pypy }}" -eq "pypy" ) {
Add-Content -Path $env:GITHUB_ENV -Value "PYTHON_VERSION=pypy-${{ matrix.python-version }}"
} else {
Add-Content -Path $env:GITHUB_ENV -Value "PYTHON_VERSION=${{ matrix.python-version }}"
}
shell: pwsh
env:
MATRIX_ARCH: ${{ matrix.architecture }}
- uses: actions/checkout@v4
- name: Test Powershell installer
if: ${{ runner.os != 'Windows' }}
id: setup-powershell
run: |
bash ./install_powershell.sh
shell: bash
- name: Set up pypy ${{ matrix.python-version }}
if: ${{ matrix.python_pypy == 'pypy' }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.architecture }}
- name: Reset APT sources to default
if: ${{ runner.os == 'Linux' }}
run: |
echo "Resetting APT sources to default Ubuntu repositories"
sudo rm /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu $(lsb_release -cs) main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu $(lsb_release -cs)-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb http://archive.ubuntu.com/ubuntu $(lsb_release -cs)-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
echo "deb http://security.ubuntu.com/ubuntu $(lsb_release -cs)-security main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
sudo apt-get update -y
shell: bash
- name: Set UPX download URL
# upx docs express that crashes are happening on ventura and above with upx, don't use on mac.
if: runner.os != 'macOS'
id: upx_setup
run: |
$build = "no"
$archiveName = ""
if ("${{ runner.os }}" -eq "Windows") {
if ("${{ matrix.architecture }}" -eq "x86") {
$archiveName = "upx-${{ env.UPX_VERSION }}-win32.zip"
} else {
$archiveName = "upx-${{ env.UPX_VERSION }}-win64.zip"
}
} elseif ("${{ runner.os }}" -eq "Linux") {
$archiveName = "upx-${{ env.UPX_VERSION }}-amd64_linux.tar.xz"
} elseif ("${{ runner.os }}" -eq "macOS") {
$build = "yes"
$archiveName = "upx-${{ env.UPX_VERSION }}-src.tar.xz"
}
$url = "https://github.com/upx/upx/releases/download/v${{ env.UPX_VERSION }}/$archiveName"
Add-Content -Path $env:GITHUB_OUTPUT -Value "build=$build"
Add-Content -Path $env:GITHUB_OUTPUT -Value "url=$url"
Add-Content -Path $env:GITHUB_OUTPUT -Value "archiveName=$archiveName"
shell: pwsh
- name: Download and prepare UPX
if: runner.os != 'macOS'
run: |
$ext = "${{ runner.os }}" -eq "Windows" ? "zip" : "tar.xz"
$url = "${{ steps.upx_setup.outputs.url }}"
$archiveName = "${{ steps.upx_setup.outputs.archiveName }}"
$outputPath = "upx-dir"
# Use Invoke-WebRequest or curl depending on the OS
if ("${{ runner.os }}" -eq "Windows") {
Invoke-WebRequest -Uri $url -OutFile $archiveName
} elseif ("${{ runner.os }}" -eq "Linux") {
curl -L $url -o $archiveName
}
New-Item -ItemType Directory -Force -Path "upx-dir" -ErrorAction SilentlyContinue
if ("${{ runner.os }}" -ne "macOS") {
if ($ext -eq "zip") {
$fileNameWithoutExtension = [System.IO.Path]::GetFileNameWithoutExtension($archiveName)
Expand-Archive -Path $archiveName -DestinationPath "temp_folder_upx"
if (-not (Test-Path -Path "upx-dir")) {
New-Item -ItemType Directory -Path "upx-dir"
}
Get-ChildItem -Path "temp_folder_upx/$fileNameWithoutExtension" -Recurse | Move-Item -Destination "upx-dir"
Remove-Item "temp_folder_upx" -Recurse -Force -ErrorAction SilentlyContinue
} else {
tar -xvf $archiveName --strip-components=1 -C "upx-dir"
}
Remove-Item $archiveName # Clean up downloaded archive
}
shell: pwsh
- name: Set UPX directory path
if: runner.os != 'macOS'
id: upx_dir
run: |
$upx_dir = "./upx-dir"
$upx_dir = $([System.IO.Path]::GetFullPath('./upx-dir'))
Dir -Recurse $upx_dir | Get-Childitem
echo "UPX_DIR=$upx_dir" | Out-File -FilePath $env:GITHUB_ENV -Append
Write-Output "UPX_DIR set to '$upx_dir'"
shell: pwsh
- name: Install Visual Studio 2015 C++ Redistributable
if: runner.os == 'Windows'
run: |
$url = "${{ matrix.vc_redist2015 }}"
$output = "vc_redist.exe"
Invoke-WebRequest -Uri $url -OutFile $output
Start-Process $output -ArgumentList '/install', '/quiet', '/norestart' -Wait
Remove-Item -Path $output
#choco install vcredist2015 -y
shell: pwsh
- name: Install Visual Studio 2019 C++ Redistributable
if: runner.os == 'Windows'
run: |
$url = "${{ matrix.vc_redist2019 }}"
$output = "vc_redist.exe"
Invoke-WebRequest -Uri $url -OutFile $output
Start-Process $output -ArgumentList '/install', '/quiet', '/norestart' -Wait
Remove-Item -Path $output
#choco install vcredist2019 -y
shell: pwsh
- name: Install Visual Studio latest C++ Redistributable
if: runner.os == 'Windows'
run: |
$url = "${{ matrix.vc_redist-latest }}"
$output = "vc_redist.exe"
Invoke-WebRequest -Uri $url -OutFile $output
Start-Process $output -ArgumentList '/install', '/quiet', '/norestart' -Wait
Remove-Item -Path $output
shell: pwsh
- name: Install Holocron Toolset dependencies
if: ${{ success() || failure() }}
env:
MATRIX_ARCH: ${{ matrix.architecture }}
id: toolset_deps
run: | # known pyinstaller versions that work with upx compressions, not all are available on all python versions.
try {
. ./install_python_venv.ps1 -noprompt -venv_name .venv_toolset_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -force_python_version ${{ matrix.python-version }}
Write-Host "Python executable path: $pythonExePath"
& $pythonExePath -m pip install --upgrade pip
if ("${{ runner.os }}" -eq "Windows") {
if ("${{ matrix.python-version }}" -eq "3.12") {
pip install pyinstaller --prefer-binary -U
} elseif ("${{ matrix.python-version }}" -eq "3.11") {
pip install "pyinstaller==5.13.2" --prefer-binary
} else {
pip install "pyinstaller==5.4" --prefer-binary
}
} else {
pip install pyinstaller -U --prefer-binary
}
$env:QT_API = if (-not [string]::IsNullOrEmpty($env:QT_API)) { $env:QT_API } else { "${{ matrix.qt_version }}" }
$output = ""
$errorLines = @()
. ./compile/deps_toolset.ps1 -noprompt -venv_name .venv_toolset_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} 2>&1 | ForEach-Object {
Write-Output $_.ToString()
$output += $_.ToString() + "`n"
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
} catch {
Write-Host -ForegroundColor Red "Detailed Error Report:"
Write-Host -ForegroundColor Red "Message: $($_.Exception.Message)"
# Attempt to provide a more detailed location of the error
if ($_.InvocationInfo -and $_.InvocationInfo.MyCommand) {
Write-Host -ForegroundColor Red "Command Name: $($_.InvocationInfo.MyCommand.Name)"
Write-Host -ForegroundColor Red "Script Name: $($_.InvocationInfo.ScriptName)"
Write-Host -ForegroundColor Red "Line Number: $($_.InvocationInfo.ScriptLineNumber)"
Write-Host -ForegroundColor Red "Line: $($_.InvocationInfo.Line)"
} else {
Write-Host -ForegroundColor Red "No invocation information available."
}
# Extract and display the script stack trace if available
if ($_.ScriptStackTrace) {
Write-Host -ForegroundColor Red "Script Stack Trace:"
Write-Host -ForegroundColor Red $_.ScriptStackTrace
} else {
Write-Host -ForegroundColor Red "No script stack trace available."
}
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
}
shell: pwsh
- name: HolocronToolset - Adjust RPATH for shared libraries in the virtual environment
if: ${{ (success() || failure()) && runner.os == 'Disabled_Linux' && steps.toolset_deps.outputs.success == 'true' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_toolset_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
# Ensure patchelf is installed
sudo apt-get update && sudo apt-get install -y patchelf
# Find the virtual environment's site-packages directory
$venvPath = $env:VIRTUAL_ENV
Write-Host "Virtual environment path: $venvPath"
# Dynamically find the Python version directory inside the venv
$pythonLibPath = Get-ChildItem -Path "$venvPath/lib" | Where-Object { $_.PSIsContainer } | Select-Object -First 1
$sitePackagesPath = Join-Path -Path $pythonLibPath.FullName -ChildPath "site-packages"
Write-Host "Site-packages path: $sitePackagesPath"
# Use patchelf to adjust RPATH for all shared libraries in the virtual environment
Get-ChildItem -Path $sitePackagesPath -Filter *.so -Recurse | ForEach-Object {
$libPath = $_.FullName
Write-Host "Patching $libPath"
sudo patchelf --set-rpath '$ORIGIN' $libPath
}
shell: pwsh
- name: Search for DLL files
if: ${{ matrix.python-version == '3.12_Disabled' && runner.os == 'Windows' }}
run: |
Get-PSDrive -PSProvider FileSystem | ForEach-Object {
$drive = $_.Root
Write-Host "Searching in $drive"
Get-ChildItem -Path $drive -Filter 'api-ms-win-crt-*.dll' -Recurse -ErrorAction SilentlyContinue -Force | ForEach-Object {
Write-Host $_.FullName
}
}
shell: pwsh
- name: Compile Holocron Toolset
if: ${{ (success() || failure()) && steps.toolset_deps.outputs.success == 'true' }}
run: |
$upxDir = $env:UPX_DIR
Write-Host "Using UPX directory at '$upxDir'"
$output = ""
$env:PYTHONOPTIMIZE = "1"
. ./compile/compile_toolset.ps1 -noprompt -venv_name .venv_toolset_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -upx_dir $upxDir 2>&1 | ForEach-Object {
Write-Output $_.ToString()
$output += $_.ToString() + "`n"
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
$warningCount = 0
$output -split "`n" | ForEach-Object {
if ($_ -match 'WARNING: Library not found: could not resolve' -or
$_ -match 'WARNING: Cannot find ' -or
$_ -match 'WARNING: lib not found:' -or
$_ -match 'WARNING: Tcl modules directory' -or
$_ -match 'WARNING: Failed to upx strip') {
$warningCount++
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} elseif ($warningCount -ge 3) {
Write-Output "Many warnings raised, pyinstaller was probably unsuccessful."
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
shell: pwsh
- name: HolocronToolset - Create Static Binary
if: ${{ (success() || failure()) && runner.os == 'Disabled_Linux' && steps.toolset_deps.outputs.success == 'true' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_toolset_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
& $pythonExePath -m pip install staticx
staticx ./dist/HolocronToolset ./dist/HolocronToolset-static
# Rename the static binary for clarity and consistency
Write-Output "./dist/HolocronToolset-static --> ./dist/HolocronToolset"
Move-Item -LiteralPath ./dist/HolocronToolset-static -Destination ./dist/HolocronToolset -Force
shell: pwsh
- name: Install HoloPatcher dependencies
if: ${{ success() || failure() }}
env:
MATRIX_ARCH: ${{ matrix.architecture }}
id: holopatcher_deps
run: | # known pyinstaller versions that work with upx compressions, not all are available on all python versions.
try {
. ./install_python_venv.ps1 -noprompt -venv_name .venv_holopatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -force_python_version ${{ matrix.python-version }}
& $pythonExePath -m pip install --upgrade pip
if ("${{ runner.os }}" -eq "Windows") {
if ("${{ matrix.python-version }}" -eq "3.12") {
pip install "pyinstaller==5.13.2" --prefer-binary
} elseif ("${{ matrix.python-version }}" -eq "3.11") {
pip install "pyinstaller==5.13.2" --prefer-binary
} else {
pip install "pyinstaller==5.4" --prefer-binary
}
} else {
pip install pyinstaller -U --prefer-binary
}
$output = ""
$errorLines = @()
. ./compile/deps_holopatcher.ps1 -noprompt -venv_name .venv_holopatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} 2>&1 | ForEach-Object {
Write-Output $_.ToString()
$output += $_.ToString() + "`n"
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
} catch {
Write-Host -ForegroundColor Red "Detailed Error Report:"
Write-Host -ForegroundColor Red "Message: $($_.Exception.Message)"
# Attempt to provide a more detailed location of the error
if ($_.InvocationInfo -and $_.InvocationInfo.MyCommand) {
Write-Host -ForegroundColor Red "Command Name: $($_.InvocationInfo.MyCommand.Name)"
Write-Host -ForegroundColor Red "Script Name: $($_.InvocationInfo.ScriptName)"
Write-Host -ForegroundColor Red "Line Number: $($_.InvocationInfo.ScriptLineNumber)"
Write-Host -ForegroundColor Red "Line: $($_.InvocationInfo.Line)"
} else {
Write-Host -ForegroundColor Red "No invocation information available."
}
# Extract and display the script stack trace if available
if ($_.ScriptStackTrace) {
Write-Host -ForegroundColor Red "Script Stack Trace:"
Write-Host -ForegroundColor Red $_.ScriptStackTrace
} else {
Write-Host -ForegroundColor Red "No script stack trace available."
}
# Log the error to GitHub Actions output or another logging mechanism
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
}
shell: pwsh
- name: HoloPatcher - Adjust RPATH for shared libraries in the virtual environment
if: ${{ (success() || failure()) && runner.os == 'Disabled_Linux' && steps.holopatcher_deps.outputs.success == 'true' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_holopatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
# Ensure patchelf is installed
sudo apt-get update && sudo apt-get install -y patchelf
# Find the virtual environment's site-packages directory
$venvPath = $env:VIRTUAL_ENV
Write-Host "Virtual environment path: $venvPath"
# Dynamically find the Python version directory inside the venv
$pythonLibPath = Get-ChildItem -Path "$venvPath/lib" | Where-Object { $_.PSIsContainer } | Select-Object -First 1
$sitePackagesPath = Join-Path -Path $pythonLibPath.FullName -ChildPath "site-packages"
Write-Host "Site-packages path: $sitePackagesPath"
# Use patchelf to adjust RPATH for all shared libraries in the virtual environment
Get-ChildItem -Path $sitePackagesPath -Filter *.so -Recurse | ForEach-Object {
$libPath = $_.FullName
Write-Host "Patching $libPath"
sudo patchelf --set-rpath '$ORIGIN' $libPath
}
shell: pwsh
- name: Compile HoloPatcher
if: ${{ (success() || failure()) && steps.holopatcher_deps.outputs.success == 'true' }}
run: |
$upxDir = $env:UPX_DIR
Write-Host "Using UPX directory at '$upxDir'"
$output = ""
$env:PYTHONOPTIMIZE = "1"
. ./compile/compile_holopatcher.ps1 -noprompt -venv_name .venv_holopatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -upx_dir $upxDir 2>&1 | ForEach-Object {
Write-Output $_.ToString()
$output += $_.ToString() + "`n"
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
$warningCount = 0
$output -split "`n" | ForEach-Object {
if ($_ -match 'WARNING: Library not found: could not resolve' -or
$_ -match 'WARNING: Cannot find ' -or
$_ -match 'WARNING: lib not found:' -or
$_ -match 'WARNING: Tcl modules directory' -or
$_ -match 'WARNING: Failed to upx strip') {
$warningCount++
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} elseif ($warningCount -ge 3) {
Write-Output "Many warnings raised, pyinstaller was probably unsuccessful."
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
shell: pwsh
- name: HoloPatcher - Create Static Binary
if: ${{ (success() || failure()) && runner.os == 'Disabled_Linux' && steps.holopatcher_deps.outputs.success == 'true' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_holopatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
& $pythonExePath -m pip install staticx
staticx ./dist/HoloPatcher ./dist/HoloPatcher-static
# Rename the static binary for clarity and consistency
Write-Output "./dist/HoloPatcher-static --> ./dist/HoloPatcher"
Move-Item -LiteralPath ./dist/HoloPatcher-static -Destination ./dist/HoloPatcher -Force
shell: pwsh
- name: Install BatchPatcher dependencies
if: ${{ success() || failure() }}
env:
MATRIX_ARCH: ${{ matrix.architecture }}
id: batchpatcher_deps
run: | # known pyinstaller versions that work with upx compressions, not all are available on all python versions.
try {
. ./install_python_venv.ps1 -noprompt -venv_name .venv_batchpatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -force_python_version ${{ matrix.python-version }}
& $pythonExePath -m pip install --upgrade pip
if ("${{ runner.os }}" -eq "Windows") {
if ("${{ matrix.python-version }}" -eq "3.12") {
pip install pyinstaller --prefer-binary -U
} elseif ("${{ matrix.python-version }}" -eq "3.11") {
pip install "pyinstaller==5.13.2" --prefer-binary
} else {
pip install "pyinstaller==5.4" --prefer-binary
}
} else {
pip install pyinstaller -U --prefer-binary
}
$output = ""
$errorLines = @()
. ./compile/deps_batchpatcher.ps1 -noprompt -venv_name .venv_batchpatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} 2>&1 | ForEach-Object {
Write-Output $_.ToString()
$output += $_.ToString() + "`n"
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
} catch {
Write-Host -ForegroundColor Red "Detailed Error Report:"
Write-Host -ForegroundColor Red "Message: $($_.Exception.Message)"
# Attempt to provide a more detailed location of the error
if ($_.InvocationInfo -and $_.InvocationInfo.MyCommand) {
Write-Host -ForegroundColor Red "Command Name: $($_.InvocationInfo.MyCommand.Name)"
Write-Host -ForegroundColor Red "Script Name: $($_.InvocationInfo.ScriptName)"
Write-Host -ForegroundColor Red "Line Number: $($_.InvocationInfo.ScriptLineNumber)"
Write-Host -ForegroundColor Red "Line: $($_.InvocationInfo.Line)"
} else {
Write-Host -ForegroundColor Red "No invocation information available."
}
# Extract and display the script stack trace if available
if ($_.ScriptStackTrace) {
Write-Host -ForegroundColor Red "Script Stack Trace:"
Write-Host -ForegroundColor Red $_.ScriptStackTrace
} else {
Write-Host -ForegroundColor Red "No script stack trace available."
}
# Log the error to GitHub Actions output or another logging mechanism
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
}
shell: pwsh
- name: BatchPatcher - Adjust RPATH for shared libraries in the virtual environment
if: ${{ (success() || failure()) && runner.os == 'Disabled_Linux' && steps.batchpatcher_deps.outputs.success == 'true' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_batchpatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
# Ensure patchelf is installed
sudo apt-get update && sudo apt-get install -y patchelf
# Find the virtual environment's site-packages directory
$venvPath = $env:VIRTUAL_ENV
Write-Host "Virtual environment path: $venvPath"
# Dynamically find the Python version directory inside the venv
$pythonLibPath = Get-ChildItem -Path "$venvPath/lib" | Where-Object { $_.PSIsContainer } | Select-Object -First 1
$sitePackagesPath = Join-Path -Path $pythonLibPath.FullName -ChildPath "site-packages"
Write-Host "Site-packages path: $sitePackagesPath"
# Use patchelf to adjust RPATH for all shared libraries in the virtual environment
Get-ChildItem -Path $sitePackagesPath -Filter *.so -Recurse | ForEach-Object {
$libPath = $_.FullName
Write-Host "Patching $libPath"
sudo patchelf --set-rpath '$ORIGIN' $libPath
}
shell: pwsh
- name: Compile BatchPatcher
if: ${{ (success() || failure()) && steps.batchpatcher_deps.outputs.success == 'true' }}
run: |
$upxDir = $env:UPX_DIR
Write-Host "Using UPX directory at '$upxDir'"
$output = ""
$env:PYTHONOPTIMIZE = "1"
. ./compile/compile_batchpatcher.ps1 -noprompt -venv_name .venv_batchpatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -upx_dir $upxDir 2>&1 | ForEach-Object {
Write-Output $_.ToString()
$output += $_.ToString() + "`n"
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
$warningCount = 0
$output -split "`n" | ForEach-Object {
if ($_ -match 'WARNING: Library not found: could not resolve' -or
$_ -match 'WARNING: Cannot find ' -or
$_ -match 'WARNING: lib not found:' -or
$_ -match 'WARNING: Tcl modules directory' -or
$_ -match 'WARNING: Failed to upx strip') {
$warningCount++
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} elseif ($warningCount -ge 3) {
Write-Output "Many warnings raised, pyinstaller was probably unsuccessful."
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
shell: pwsh
- name: BatchPatcher - Create Static Binary
if: ${{ (success() || failure()) && runner.os == 'Disabled_Linux' && steps.batchpatcher_deps.outputs.success == 'true' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_batchpatcher_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
& $pythonExePath -m pip install staticx
staticx ./dist/K_BatchPatcher ./dist/K_BatchPatcher-static
# Rename the static binary for clarity and consistency
Write-Output "./dist/K_BatchPatcher-static --> ./dist/K_BatchPatcher"
Move-Item -LiteralPath ./dist/K_BatchPatcher-static -Destination ./dist/K_BatchPatcher -Force
shell: pwsh
- name: Compile KotorDiff
if: ${{ success() || failure() }}
env:
MATRIX_ARCH: ${{ matrix.architecture }}
run: |
$upxDir = $env:UPX_DIR
Write-Host "Using UPX directory at '$upxDir'"
$env:PYTHONOPTIMIZE = "1"
. ./install_python_venv.ps1 -noprompt -venv_name .venv_kotordiff_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -force_python_version ${{ matrix.python-version }}
& $pythonExePath -m pip install --upgrade pip
if ("${{ runner.os }}" -eq "Windows") {
if ("${{ matrix.python-version }}" -eq "3.12") {
pip install "pyinstaller==5.13.2" --prefer-binary
} elseif ("${{ matrix.python-version }}" -eq "3.11") {
pip install "pyinstaller==5.13.2" --prefer-binary
} else {
pip install "pyinstaller==5.4" --prefer-binary
}
} else {
pip install pyinstaller -U --prefer-binary
}
pip install -r Tools/KotorDiff/requirements.txt --prefer-binary
$output = ""
. ./compile/compile_kotordiff.ps1 -noprompt -venv_name .venv_kotordiff_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -upx_dir $upxDir 2>&1 | ForEach-Object {
Write-Output $_.ToString()
$output += $_.ToString() + "`n"
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
$warningCount = 0
$output -split "`n" | ForEach-Object {
if ($_ -match 'WARNING: Library not found: could not resolve' -or
$_ -match 'WARNING: Cannot find ' -or
$_ -match 'WARNING: lib not found:' -or
$_ -match 'WARNING: Tcl modules directory' -or
$_ -match 'WARNING: Failed to upx strip') {
$warningCount++
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} elseif ($warningCount -ge 3) {
Write-Output "Many warnings raised, pyinstaller was probably unsuccessful."
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
shell: pwsh
- name: KotorDiff - Create Static Binary
if: ${{ (success() || failure()) && runner.os == 'Linux' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_kotordiff_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
& $pythonExePath -m pip install staticx
staticx ./dist/KotorDiff ./dist/KotorDiff-static
# Rename the static binary for clarity and consistency
Write-Output "./dist/KotorDiff-static --> ./dist/KotorDiff"
Move-Item -LiteralPath ./dist/KotorDiff-static -Destination ./dist/KotorDiff -Force
shell: pwsh
- name: Compile GUIDuplicator
if: ${{ success() || failure() }}
env:
MATRIX_ARCH: ${{ matrix.architecture }}
run: |
$upxDir = $env:UPX_DIR
Write-Host "Using UPX directory at '$upxDir'"
$env:PYTHONOPTIMIZE = "1"
. ./install_python_venv.ps1 -noprompt -venv_name .venv_guiduplicator_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -force_python_version ${{ matrix.python-version }}
& $pythonExePath -m pip install --upgrade pip
if ("${{ runner.os }}" -eq "Windows") {
if ("${{ matrix.python-version }}" -eq "3.12") {
pip install "pyinstaller==5.13.2" --prefer-binary
} elseif ("${{ matrix.python-version }}" -eq "3.11") {
pip install "pyinstaller==5.13.2" --prefer-binary
} else {
pip install "pyinstaller==5.4" --prefer-binary
}
} else {
pip install pyinstaller -U --prefer-binary
}
pip install -r Tools/GuiDuplicator/requirements.txt --prefer-binary
$output = ""
. ./compile/compile_gui_duplicator.ps1 -noprompt -venv_name .venv_guiduplicator_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }} -upx_dir $upxDir 2>&1 | ForEach-Object {
Write-Output $_.ToString()
if($_ -match 'ERROR:') {
$errorLines += $_.ToString()
}
}
$warningCount = 0
$output -split "`n" | ForEach-Object {
if ($_ -match 'WARNING: Library not found: could not resolve' -or
$_ -match 'WARNING: Cannot find ' -or
$_ -match 'WARNING: lib not found:' -or
$_ -match 'WARNING: Tcl modules directory' -or
$_ -match 'WARNING: Failed to upx strip') {
$warningCount++
}
}
if ($errorLines.Count -gt 0) {
$errorLines | ForEach-Object { Write-Error $_ }
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} elseif ($warningCount -ge 3) {
Write-Output "Many warnings raised, pyinstaller was probably unsuccessful."
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=false"
exit 1
} else {
Add-Content -Path $env:GITHUB_OUTPUT -Value "success=true"
}
shell: pwsh
- name: GUIDuplicator - Create Static Binary
if: ${{ (success() || failure()) && runner.os == 'Linux' }}
run: |
. ./install_python_venv.ps1 -noprompt -venv_name .venv_guiduplicator_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
& $pythonExePath -m pip install staticx
staticx ./dist/GuiDuplicator ./dist/GuiDuplicator-static
# Rename the static binary for clarity and consistency
Write-Output "./dist/GuiDuplicator-static --> ./dist/GuiDuplicator"
Move-Item -LiteralPath ./dist/GuiDuplicator-static -Destination ./dist/GuiDuplicator -Force
shell: pwsh
- name: Upload compiled binaries attempt 1
if: ${{ success() || failure() }}
id: upload_attempt_1
uses: actions/upload-artifact@v4
with:
name: publish_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
path: ./dist/**
retention-days: 90
continue-on-error: true
- name: Upload compiled binaries attempt 2
id: upload_attempt_2
if: ${{ (success() || failure()) && steps.upload_attempt_1.outcome == 'failure' }}
uses: actions/upload-artifact@v4
with:
name: publish_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
path: ./dist/**
retention-days: 90
continue-on-error: true
- name: Upload compiled binaries attempt 3
id: upload_attempt_3
if: ${{ (success() || failure()) && steps.upload_attempt_2.outcome == 'failure' }}
uses: actions/upload-artifact@v4
with:
name: publish_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
path: ./dist/**
retention-days: 90
continue-on-error: true
- name: Upload compiled binaries attempt 4
id: upload_attempt_4
if: ${{ (success() || failure()) && steps.upload_attempt_3.outcome == 'failure' }}
uses: actions/upload-artifact@v4
with:
name: publish_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
path: ./dist/**
retention-days: 90
continue-on-error: true
- name: Upload compiled binaries attempt 5
id: upload_attempt_5
if: ${{ (success() || failure()) && steps.upload_attempt_4.outcome == 'failure' }}
uses: actions/upload-artifact@v4
with:
name: publish_${{ matrix.os }}_${{ matrix.python_pypy }}_${{ matrix.python-version }}_${{ matrix.architecture }}
path: ./dist/**
retention-days: 90
package: # The goal of this job is to repackage by toolname, rather than all tools by os_pyversion_arch
needs: build # do not start this job until all 'build' jobs complete
if: ${{ success() || failure() }}
runs-on: ubuntu-latest
outputs:
filesToArchive: ${{ steps.set-matrix.outputs.matrixJson }}
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: published_workflow_builds/
pattern: publish_*
- name: Re-package artifacts by Python version and architecture
shell: pwsh
run: |
$here_dirpath = (Get-Location)
Write-Output "here: '$here_dirpath'"
$packagedArtifactsPath = Join-Path -Path $here_dirpath -ChildPath "packaged_artifacts"
New-Item -ItemType Directory -Force -Path $packagedArtifactsPath
# Navigate to the directory with downloaded artifacts
Set-Location -Path "published_workflow_builds"
$artifactNames = New-Object 'System.Collections.Generic.HashSet[string]'
# Iterate through each os_pythonversion_arch folder.
Get-ChildItem -Filter "publish_*" | ForEach-Object {
$matrixPackagePath = $_.FullName
Write-Output "Found matrix package '$matrixPackagePath'"
$os, $python_or_pypy, $pythonVersion, $architecture = $_.BaseName -replace '^publish_', '' -split '_'
$shortOsName = $os
if ($os -eq "ubuntu-20.04" -or $os -eq "ubuntu-22.04" -or $os -eq "ubuntu-latest") {
$shortOsName = "Linux"
} elseif ($os -eq "macos-11" -or $os -eq "macos-12" -or $os -eq "macos-latest") {
$shortOsName = "Mac"
} elseif ($os -eq "windows-2019" -or $os -eq "windows-2022" -or $os -eq "windows-latest") {
$shortOsName = "Win"
}
# Iterate through each tool in the folder
Get-ChildItem -Path $matrixPackagePath | ForEach-Object {
$toolExePath = $_.FullName
$toolFileName = $_.Name
$fileBaseName = [IO.Path]::GetFileNameWithoutExtension($_.Name)
$fileExtension = $_.Extension
$outerArchiveName = "$fileBaseName`_$pythonVersion"
$outerZipPath = Join-Path -Path $packagedArtifactsPath -ChildPath $outerArchiveName
$osSpecificArchiveName = "$fileBaseName`_$shortOsName-$architecture"
$osSpecificArchivePath = Join-Path -Path $outerZipPath -ChildPath $osSpecificArchiveName
Write-Output " ToolFileName: '$toolFileName'"
Write-Output " Tool original filepath: '$toolExePath'"
Write-Output "outerArchiveName: '$outerArchiveName'"
Write-Output " - osSpecificArchiveName: '$osSpecificArchiveName'"
Write-Output "outerZipPath: '$outerZipPath'"
Write-Output " - osSpecificArchivePath: '$osSpecificArchivePath'"
New-Item -ItemType Directory -Force -Path $outerZipPath
chmod 777 -R $toolExePath
$toolExeParentDirPath = Split-Path -Parent $toolExePath
Write-Output "Creating archive for '$toolFileName' at '$toolExeParentDirPath'..."
Write-Output ("Push-Location to start in '$toolExeParentDirPath' (originally at '$(Get-Location)')")
Push-Location -Path $toolExeParentDirPath
# Archive the tool by os identifier.
if ((-not $fileExtension) -or (-not $fileExtension.Trim()) -or ($fileExtension.ToLower().Trim() -eq ".app")) {
$osSpecificArchivePath = "$osSpecificArchivePath.tar.gz"
if (Test-Path $toolExePath -PathType Container -ErrorAction SilentlyContinue) { # It's a directory
tar -czf "$osSpecificArchivePath" -C "$toolExePath" .
} else { # It's a file
tar -czf "$osSpecificArchivePath" -C "$toolExeParentDirPath" $toolFileName
}
} else {
$osSpecificArchivePath = "$osSpecificArchivePath.zip"
zip -r -9 "$osSpecificArchivePath" $toolFileName
}
Pop-Location
Write-Output ("Pop-Location to return to $(Get-Location) (was pushed to '$toolExeParentDirPath')")
Write-Output "Compressed archive saved to '$osSpecificArchivePath'"
chmod 777 -R "$osSpecificArchivePath"
$artifactNames.Add($outerArchiveName)
}
}
# Save artifact names to a file
$artifactNames | Out-File -FilePath "../artifact-names.txt" -Encoding UTF8
# use for debug
# Get-ChildItem -Force | ForEach-Object {
# $size = if ($_.PSIsContainer) { "N/A" } else { $_.Length }
# $attrs = $_.Attributes.ToString() -replace 'ReadOnly', 'RO' -replace 'Hidden', 'H' -replace 'System', 'S' -replace 'Archive', 'A' -replace 'Directory', 'D' -replace ', ', '|'
# [PSCustomObject]@{
# Name = $_.Name
# Size = $size
# Attributes = $attrs
# }
# } | Format-Table -AutoSize
# Navigate back to the root of the workspace
Set-Location -Path "../"
- name: Generate matrix for uploading
if: ${{ success() || failure() }}
id: set-matrix
shell: pwsh
run: |
$artifactNames = Get-Content 'artifact-names.txt' -ReadCount 0
# Initialize an array to hold the artifact names directly
$matrixArray = @()
foreach ($name in $artifactNames) {
if (-not [string]::IsNullOrEmpty($name)) {
Write-Host "Processing artifact name: $name"
# Trim the name and add directly to the array
$matrixArray += $name.trim()
}
}
# Convert the array directly to JSON
$jsonMatrix = $matrixArray | ConvertTo-Json -Depth 5 -Compress
# Use a single line of JSON for the matrix to avoid issues
$singleLineJsonMatrix = $jsonMatrix -replace "`r", ""
$singleLineJsonMatrix = $singleLineJsonMatrix -replace "`n", ""
Write-Host "Matrix JSON:"
Write-Host $singleLineJsonMatrix
echo "matrixJson<<EOF" >> $env:GITHUB_OUTPUT
echo $singleLineJsonMatrix >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
- name: Upload all repackages for next job
if: ${{ success() || failure() }}
uses: actions/upload-artifact@v4
with:
name: all_tool_dists_onearchive
path: packaged_artifacts/**
retention-days: 1 # only used for the next job.
compression-level: 0
upload:
needs: package
if: ${{ success() || failure() }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
artifactb: ${{ fromJson(needs.package.outputs.filesToArchive) }}
steps:
- name: Download repackages from package job
if: ${{ success() || failure() }}
uses: actions/download-artifact@v4
with:
path: all_tool_dists_onearchive
pattern: all_tool_dists*
- name: Upload re-packaged artifact
if: ${{ success() || failure() }}
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifactb }}
path: all_tool_dists_onearchive/all_tool_dists_onearchive/${{ matrix.artifactb }}
compression-level: 9
if-no-files-found: error