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.",
)