diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml index 017e0af1..1cb070e5 100644 --- a/.github/workflows/lint-and-build.yml +++ b/.github/workflows/lint-and-build.yml @@ -27,6 +27,9 @@ on: - "*.toml" - "uv.lock" +permissions: + contents: read + env: GITHUB_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} GITHUB_EXCLUDE_BUILD_NUMBER: ${{ inputs.excludeBuildNumber }} @@ -44,7 +47,31 @@ defaults: shell: pwsh jobs: + PySentry: + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v7 + - run: uvx pysentry-rs --sources=pypa,pypi,osv --forbid-unmaintained + + # Single source of truth for the shipped Python version(s). + # GitHub Actions has no length() and `strategy` can't read `env`, so the matrix and the + # "more than one Python version" flag are both derived here and consumed via `needs`. + Setup: + runs-on: ubuntu-slim + outputs: + python-versions: ${{ steps.set.outputs.python-versions }} + include-python-version-tag: ${{ steps.set.outputs.include-python-version-tag }} + steps: + - id: set + # Only the Python version(s) we plan on shipping matter. + run: | + $pythonVersions = @('3.14') + "python-versions=$($pythonVersions | ConvertTo-Json -Compress -AsArray)" >> $Env:GITHUB_OUTPUT + "include-python-version-tag=$(if ($pythonVersions.Count -gt 1) { '-IncludePythonVersionTag' })" >> $Env:GITHUB_OUTPUT + Pyright: + needs: Setup runs-on: ${{ matrix.os }} env: # Prevent accidentally slower type-checking due to missing arm wheels. @@ -60,7 +87,7 @@ jobs: matrix: # windows arm runner slower as long as opencv doesn't provide windows arm64 wheels os: [windows-latest, ubuntu-24.04-arm] - python-version: ["3.14"] + python-version: ${{ fromJson(needs.Setup.outputs.python-versions) }} steps: - uses: actions/checkout@v6 - name: Set up uv for Python ${{ matrix.python-version }} @@ -76,23 +103,16 @@ jobs: working-directory: src/ python-version: ${{ matrix.python-version }} - PySentry: - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@v6 - - uses: astral-sh/setup-uv@v7 - - run: uvx pysentry-rs --sources=pypa,pypi,osv --forbid-unmaintained - Build: + needs: Setup runs-on: ${{ matrix.os }} outputs: AUTOSPLIT_VERSION: ${{ steps.artifact_vars.outputs.AUTOSPLIT_VERSION }} strategy: fail-fast: false - # Only the Python version we plan on shipping matters. matrix: os: [windows-latest, windows-11-arm, ubuntu-24.04, ubuntu-24.04-arm] - python-version: ["3.14"] + python-version: ${{ fromJson(needs.Setup.outputs.python-versions) }} wine-compat: [""] include: - os: windows-latest @@ -122,14 +142,12 @@ jobs: || null }} # endregion - run: scripts/install.ps1 ${{ matrix.wine-compat }} - - run: scripts/build.ps1 ${{ matrix.wine-compat }} + - run: scripts/build.ps1 ${{ matrix.wine-compat }} ${{ needs.Setup.outputs.include-python-version-tag }} - name: Run test suite # pywinctl/pymonctl connect to the X display at import-time, hence xvfb run: >- ${{ startsWith(matrix.os, 'ubuntu') && 'xvfb-run --auto-servernum' || '' }} uv run -m unittest discover --start-directory tests --verbose - - name: Add empty profile - run: echo "" > dist/settings.toml - name: Extract AutoSplit version id: artifact_vars working-directory: src @@ -138,41 +156,54 @@ jobs: echo "AUTOSPLIT_VERSION=$Env:AUTOSPLIT_VERSION" >> $Env:GITHUB_OUTPUT echo "OS=$([System.Runtime.InteropServices.RuntimeInformation]::RuntimeIdentifier)" >> $Env:GITHUB_OUTPUT - name: Upload Build Artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: > AutoSplit v${{ steps.artifact_vars.outputs.AUTOSPLIT_VERSION }} for ${{ steps.artifact_vars.outputs.OS }}${{ matrix.wine-compat }} (Python ${{ matrix.python-version }}) - path: | - dist/AutoSplit* - dist/settings.toml + path: dist/AutoSplit* if-no-files-found: error - name: Upload Build logs - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: > Build logs for ${{ steps.artifact_vars.outputs.OS }}${{ matrix.wine-compat }} (Python ${{ matrix.python-version }}) path: | - build/AutoSplit/*.toc - build/AutoSplit/*.txt - build/AutoSplit/*.html + build/AutoSplit*/*.toc + build/AutoSplit*/*.txt + build/AutoSplit*/*.html if-no-files-found: error - Release-Template: + Merge-Build-Artifacts: needs: Build - runs-on: ubuntu-latest + runs-on: ubuntu-slim + steps: + - name: Merge all executables into a single artifact + uses: actions/upload-artifact/merge@v7 + with: + name: AutoSplit v${{ needs.Build.outputs.AUTOSPLIT_VERSION }} + # Only the executables, not the per-OS build logs. Keep the individual downloads + pattern: AutoSplit v* + + Release-Template: + needs: [Setup, Build] + runs-on: ubuntu-slim steps: - name: Annotate release template URL run: | $version = "v${{ needs.Build.outputs.AUTOSPLIT_VERSION }}" + $pythonBullet = if ('${{ needs.Setup.outputs.include-python-version-tag }}') { + "- Python: This is the Python version bundled with AutoSplit. Try the newer version, it should be functionally identical, with a marginal performance boost. If you have any issue with it, please [report it here](https://github.com/Toufool/AutoSplit/issues) or on the Discord server and use an older Python version in the mean time." + } + else { '' } $body = [uri]::EscapeDataString(((@' # Which Asset should I download? - - Python: This is the Python version bundled with AutoSplit. Try the newer version, it should be functionally identical, with a marginal performance boost. If you have any issue with it, please [report it here](https://github.com/Toufool/AutoSplit/issues) or on the Discord server and use an older Python version in the mean time. - - `arm64` vs `x64`: [Check your Processor Platform Architecture](https://www.checkadevice.com/tests/system/) (note that `x86_64` and `x64` means the same). If you're still unsure, `x64` will work either way. `arm64` should be more efficient. + {{PYTHON}} + - `arm64` vs `x64`: [Check your Processor Platform Architecture](https://www.checkadevice.com/tests/system/) (note that `x86_64`==`x64` and `aarch64`==`arm64`). If you're still unsure, `x64` will work either way. `arm64` should be more efficient. - WineCompat: This is for running the Windows executable under Wine on Linux - '@).Trim() -replace "`n", '')) + '@).Replace('{{PYTHON}}', $pythonBullet).Trim() -replace "`n", '')) # ^ Removing newline from template because GitHub will actually decode %0A echo "::notice::${{ github.server_url }}/${{ github.repository }}/releases/new?target=main&tag=$version&title=$version&body=$body" diff --git a/res/design.ui b/res/design.ui index 62eb6630..0c200696 100644 --- a/res/design.ui +++ b/res/design.ui @@ -1147,7 +1147,7 @@ ClickableLabel:hover { background-color: palette(midlight); } Toggle Logs - Ctrl+L + Ctrl+J Qt::ShortcutContext::ApplicationShortcut diff --git a/scripts/build.ps1 b/scripts/build.ps1 index da71fbdb..f5f47992 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,6 +1,9 @@ #! /usr/bin/pwsh -param([switch]$WineCompat) +param( + [switch]$WineCompat, + [switch]$IncludePythonVersionTag +) $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true @@ -10,8 +13,15 @@ Push-Location "$PSScriptRoot/.." # Avoid issues with space in path try { & 'scripts/compile_resources.ps1' + $version = (Select-String 'pyproject.toml' -Pattern '^version = "(.+)"').Matches.Groups[1].Value + # Semver-compliant Python version tag + $pythonVersionTag = if ($IncludePythonVersionTag) { + (uv run --active python --version) -replace '^Python (\d+\.\d+).*', '+Python$1' + } + else { '' } + # CI not allowed to skip splash screen, it MUST build (will fail when calling PyInstaller) - $SupportsSplashScreen = $Env:GITHUB_JOB -or [System.Convert]::ToBoolean( + $supportsSplashScreen = $Env:GITHUB_JOB -or [System.Convert]::ToBoolean( $(uv run --active scripts/check_splash_support.py)) $arguments = @( @@ -30,13 +40,15 @@ try { # Missing upx executable should be enough, but let's be explicit $arguments += '--noupx' } - if ($SupportsSplashScreen) { + if ($supportsSplashScreen) { # https://github.com/pyinstaller/pyinstaller/issues/9022 $arguments += @('--splash=res/splash.png') } if ($IsWindows) { + $arch = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)".ToLower() $arguments += @( '--onefile', + "--name=AutoSplit-$version$pythonVersionTag-$arch$(if ($WineCompat) {'-WineCompat'} else {''})" # Hidden import by winrt.windows.graphics.imaging.SoftwareBitmap.create_copy_from_surface_async '--hidden-import=winrt.windows.foundation') } @@ -58,7 +70,13 @@ try { Move-Item build/AppDir/AutoSplit/_internal build/AppDir/_internal Remove-Item build/AppDir/AutoSplit - if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'X64') { + $arch = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { + 'X64' { 'x86_64' } + 'Arm64' { 'aarch64' } + default { throw "Unsupported arch: $_" } + } + + if ($arch -eq 'x86_64') { # Technically UPX works for Linux executables, but trying to compress .so can still result in Segmentation fault # https://github.com/orgs/pyinstaller/discussions/8922#discussioncomment-13185670 # https://github.com/pyinstaller/pyinstaller/blob/4d28a528f8ab8632f7cfa7662fc6fcc45881e741/PyInstaller/building/utils.py#L281-L288 @@ -87,8 +105,12 @@ try { # Create AppImage ### Copy-Item res/AutoSplit.desktop build/AppDir/AutoSplit.desktop - Copy-Item res/splash.png build/AppDir/AutoSplit.png - $version = (Select-String 'pyproject.toml' -Pattern '^version = "(.+)"').Matches.Groups[1].Value + # Icon as PNG (freedesktop doesn't support .ico), converted from res/icon.ico. + # Not splash.png, which uses hard transparency for the Tcl/Tk splash. + New-Item -ItemType Directory -Path build/AppDir/usr/share/icons/hicolor/256x256/apps -Force | Out-Null + uv run --active python -c "from PIL import Image; Image.open('res/icon.ico').save('build/AppDir/AutoSplit.png')" + # Top-level -> .DirIcon (file thumbnail); hicolor copy -> desktop integration (menu/taskbar). + Copy-Item build/AppDir/AutoSplit.png build/AppDir/usr/share/icons/hicolor/256x256/apps/AutoSplit.png $date = Get-Date -Format 'yyyy-MM-dd' New-Item -ItemType Directory -Path build/AppDir/usr/share/metainfo -Force | Out-Null @@ -99,10 +121,28 @@ try { if (Test-Path dist) { Remove-Item dist -Recurse -Force } New-Item -ItemType Directory -Path dist | Out-Null - & 'scripts/appimagetool.AppImage' build/AppDir dist/AutoSplit.AppImage - chmod +x dist/AutoSplit.AppImage + # AppImage naming nomenclature: + # - https://github.com/AppImage/AppImageSpec/blob/master/draft.md#type-2-image-format + # - https://github.com/AppImage/appimage.github.io#:~:text=Standard%20nomenclature + $appImageName = "AutoSplit-$version$pythonVersionTag-$arch.AppImage" + $arguments = @('build/AppDir', "dist/$appImageName") + # Update information + # https://docs.appimage.org/packaging-guide/optional/updates.html#using-appimagetool + # https://github.com/AppImage/AppImageSpec/blob/master/draft.md#github-releases + if ($Env:GITHUB_REPOSITORY) { + # Skip update information if not doing a GitHub build + $owner, $repo = $Env:GITHUB_REPOSITORY -split '/' + $arguments += @('-u', "gh-releases-zsync|$owner|$repo|latest|AutoSplit-*-$arch.AppImage.zsync") + } + & 'scripts/appimagetool.AppImage' @arguments + + # appimagetool writes the .zsync file to the working directory (repo root) as the AppImage + # basename, not next to the AppImage. Move it into dist/. + if (Test-Path "$appImageName.zsync") { + Move-Item "$appImageName.zsync" "dist/$appImageName.zsync" -Force + } - Write-Host 'Created dist/AutoSplit.AppImage' + Write-Host "Created dist/$appImageName" } } finally { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index fab99241..05d5f760 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -57,7 +57,7 @@ if ($IsLinux) { Write-Output 'Installing appimagetool' Invoke-WebRequest ` - -Uri "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(uname -m).AppImage" ` + -Uri "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$(uname -m).AppImage" ` -OutFile "$PSScriptRoot/appimagetool.AppImage" chmod +x "$PSScriptRoot/appimagetool.AppImage" } diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 0a6b6b24..bb65efa8 100755 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -1200,6 +1200,9 @@ def main(): myappid = f"Toufool.AutoSplit.v{AUTOSPLIT_VERSION}" shell32.SetCurrentProcessExplicitAppUserModelID(myappid) + # Decouple from the executable basename (which varies per build) + app.setApplicationName("AutoSplit") + app.setApplicationVersion(AUTOSPLIT_VERSION) app.setWindowIcon(QtGui.QIcon(":/resources/icon.ico")) if is_already_open(): diff --git a/src/error_messages.py b/src/error_messages.py index 40461308..f264772c 100644 --- a/src/error_messages.py +++ b/src/error_messages.py @@ -43,7 +43,7 @@ def _set_text_message( ): # Also surface the error message in the logs plain_message = QtGui.QTextDocumentFragment.fromHtml(message).toPlainText() - sys.stderr.write(f"{plain_message}\n{details}\n" if details else f"{plain_message}\n") + print(f"{plain_message}\n{details}\n" if details else f"{plain_message}", sys.stderr) message_box = QtWidgets.QMessageBox() message_box.setWindowTitle("Error") @@ -144,9 +144,9 @@ def invalid_hotkey(hotkey_name: str): def no_settings_file_on_open(): - _set_text_message( + print( "No settings file found. " - + "One can be loaded on open if placed in the same folder as the AutoSplit executable." + + "One can be loaded on open if placed in the same folder as the AutoSplit executable.", )