Skip to content

Update README.md

Update README.md #75

name: Build
on:
push:
branches:
- main
tags:
- "v*"
pull_request:
workflow_dispatch:
inputs:
tag:
description: "Tag Name"
required: false
type: string
prerelease:
description: "Mark as pre-release."
required: false
type: boolean
default: false
permissions:
contents: read
env:
JAVA_VERSION: "21"
APP_NAME: Tritium
MAIN_CLASS: io.github.tritium_launcher.launcher.Main
WINGET_PACKAGE_IDENTIFIER: TritiumLauncher.Tritium
WINDOWS_UPGRADE_UUID: "36525b46-a7ad-4ff2-8be7-b760908f7d83"
jobs:
build:
name: Build ${{ matrix.platform }}-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
platform: linux
arch: x64
- runner: windows-2025
platform: windows
arch: x64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set Up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ env.JAVA_VERSION }}
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Resolve App Version
id: version
shell: bash
env:
GH_TOKEN: ${{ github.token }}
DISPATCH_TAG: ${{ github.event.inputs.tag }}
run: |
set -euo pipefail
raw=""
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
raw="${GITHUB_REF#refs/tags/v}"
fi
if [[ -z "$raw" && "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" && -n "${DISPATCH_TAG:-}" ]]; then
raw="${DISPATCH_TAG#v}"
fi
if [[ -z "$raw" ]]; then
raw="$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' build.gradle.kts | head -n1 || true)"
fi
cleaned="$(echo "$raw" | sed -E 's/[^0-9.]+/./g; s/\.+/./g; s/^\.//; s/\.$//')"
if [[ -z "$cleaned" ]]; then
cleaned="0.1"
fi
IFS='.' read -r -a parts <<< "$cleaned"
major="${parts[0]:-0}"
minor="${parts[1]:-0}"
build_from_source="${parts[2]:-}"
major_num="$((10#$major))"
minor_num="$((10#$minor))"
is_release_build=false
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
is_release_build=true
fi
if [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" && -n "${DISPATCH_TAG:-}" ]]; then
is_release_build=true
fi
next_series_build() {
local major_series="$1"
local minor_series="$2"
local page=1
local max_seen=0
while :; do
local payload
if ! payload="$(curl -fsSL \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100&page=${page}")"; then
break
fi
local page_count
page_count="$(echo "$payload" | sed -nE 's/^[[:space:]]*"tag_name":[[:space:]]*"([^"]+)".*/\1/p' | wc -l | tr -d '[:space:]')"
while IFS= read -r tag; do
if [[ "$tag" =~ ^v${major_series}\.${minor_series}\.([0-9]+)($|[^0-9].*) ]]; then
local build_patch="${BASH_REMATCH[1]}"
local build_patch_num="$((10#$build_patch))"
if [[ "$build_patch_num" -gt "$max_seen" ]]; then
max_seen="$build_patch_num"
fi
fi
done < <(echo "$payload" | sed -nE 's/^[[:space:]]*"tag_name":[[:space:]]*"([^"]+)".*/\1/p')
if [[ ! "$page_count" =~ ^[0-9]+$ || "$page_count" -lt 100 ]]; then
break
fi
page=$((page + 1))
done
echo "$((max_seen + 1))"
}
build_number=""
if [[ "$is_release_build" == "true" ]]; then
if [[ "$build_from_source" =~ ^[0-9]+$ ]]; then
build_number="$((10#$build_from_source))"
else
build_number="$(next_series_build "$major_num" "$minor_num")"
fi
elif [[ "$build_from_source" =~ ^[0-9]+$ ]]; then
build_number="$((10#$build_from_source))"
else
build_number="${GITHUB_RUN_NUMBER:-0}"
fi
build_num="$((10#$build_number))"
app_version="${major_num}.${minor_num}.${build_num}"
echo "app_version=$app_version" >> "$GITHUB_OUTPUT"
echo "Resolved app version: $app_version"
- name: Resolve Qt Version
id: qtver
shell: bash
run: |
set -euo pipefail
qt_version="$(sed -nE 's/^qt[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' gradle/libs.versions.toml | head -n1 || true)"
if [[ -z "$qt_version" ]]; then
echo "Failed to resolve Qt version from gradle/libs.versions.toml" >&2
exit 1
fi
echo "value=$qt_version" >> "$GITHUB_OUTPUT"
echo "Resolved Qt version: $qt_version"
- name: Prepare Package Input (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: .\gradlew.bat --no-daemon clean preparePackageInput
- name: Prepare Package Input (Linux)
if: runner.os == 'Linux'
run: ./gradlew --no-daemon clean preparePackageInput
- name: Bundle Qt Libraries (Windows)
if: runner.os == 'Windows'
shell: pwsh
env:
QT_VERSION: ${{ steps.qtver.outputs.value }}
run: |
$ErrorActionPreference = "Stop"
python -m pip install --upgrade pip
python -m pip install --upgrade aqtinstall
$qtHost = "windows"
$archOutput = & python -m aqt list-qt $qtHost desktop --arch $env:QT_VERSION 2>&1
if ($LASTEXITCODE -ne 0) { throw "list-qt --arch failed" }
$archs = ($archOutput -join " ") -split '\s+' | Where-Object { $_ -ne '' }
$qtArch = $archs | Where-Object { $_ -match 'msvc' -and $_ -match '64' } | Select-Object -First 1
if (-not $qtArch) { $qtArch = $archs | Where-Object { $_ -match '64' } | Select-Object -First 1 }
if (-not $qtArch) { throw "Unable to resolve Qt architecture. Available: $($archs -join ', ')" }
$qtPlatform = "windows-x64"
$modulesOutput = & python -m aqt list-qt $qtHost desktop --modules $env:QT_VERSION $qtArch 2>&1
if ($LASTEXITCODE -ne 0) { throw "list-qt --modules failed" }
if ($modulesOutput -match '\bqtsvg\b') {
Write-Host "Installing Qt with optional module qtsvg"
& python -m aqt install-qt $qtHost desktop $env:QT_VERSION $qtArch -m qtsvg -O "$PWD\\.qt"
if ($LASTEXITCODE -ne 0) { throw "install-qt (with qtsvg) failed" }
} else {
Write-Host "qtsvg module not available; installing base Qt only"
& python -m aqt install-qt $qtHost desktop $env:QT_VERSION $qtArch -O "$PWD\\.qt"
if ($LASTEXITCODE -ne 0) { throw "install-qt failed" }
}
$qtRoot = "$PWD\\.qt\\$env:QT_VERSION"
$qtDir = Join-Path $qtRoot $qtArch
$qtLibDir = Join-Path $qtDir "bin"
if (-not (Test-Path $qtLibDir)) {
$candidate = Get-ChildItem -Path $qtRoot -Directory -ErrorAction SilentlyContinue |
Where-Object { Test-Path (Join-Path $_.FullName "bin") } |
Select-Object -First 1
if ($candidate) {
$qtDir = $candidate.FullName
$qtLibDir = Join-Path $qtDir "bin"
}
}
if (-not (Test-Path $qtLibDir)) {
$dirs = Get-ChildItem -Path $qtRoot -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
throw "Qt library path not found under $qtRoot (initial: $qtArch). Candidates: $($dirs -join ', ')"
}
Write-Host "Resolved Qt directory: $qtDir"
$toolsDir = "$PWD\\.qtjambi-deployer"
New-Item -ItemType Directory -Force $toolsDir | Out-Null
$base = "https://repo1.maven.org/maven2/io/qtjambi"
Invoke-WebRequest "$base/qtjambi/$env:QT_VERSION/qtjambi-$env:QT_VERSION.jar" -OutFile "$toolsDir\\qtjambi-$env:QT_VERSION.jar"
Invoke-WebRequest "$base/qtjambi-deployer/$env:QT_VERSION/qtjambi-deployer-$env:QT_VERSION.jar" -OutFile "$toolsDir\\qtjambi-deployer-$env:QT_VERSION.jar"
Invoke-WebRequest "$base/qtjambi-native-$qtPlatform/$env:QT_VERSION/qtjambi-native-$qtPlatform-$env:QT_VERSION.jar" -OutFile "$toolsDir\\qtjambi-native-$qtPlatform-$env:QT_VERSION.jar"
$cp = "$toolsDir\\qtjambi-deployer-$env:QT_VERSION.jar;$toolsDir\\qtjambi-$env:QT_VERSION.jar;$toolsDir\\qtjambi-native-$qtPlatform-$env:QT_VERSION.jar"
java "-Djava.library.path=$qtLibDir" -cp $cp io.qt.qtjambi.deployer.Main qt "--qtdir=$qtDir" "--platform=$qtPlatform" "--target-version=$env:QT_VERSION" -d "$PWD\\build\\package-input"
Get-ChildItem "$PWD\\build\\package-input\\qt-lib-*.jar" | ForEach-Object { Write-Host "Bundled: $($_.Name)" }
- name: Bundle Qt Libraries (Linux)
if: runner.os == 'Linux'
shell: bash
env:
QT_VERSION: ${{ steps.qtver.outputs.value }}
run: |
set -euo pipefail
python3 -m pip install --user --break-system-packages --upgrade pip aqtinstall
host="linux"
qt_platform="linux-x64"
arch_pattern='(linux_)?gcc_64'
if ! archs="$(python3 -m aqt list-qt "$host" desktop --arch "$QT_VERSION" | tr ' \r\t' '\n' | sed '/^$/d')"; then
echo "Failed to query Qt architectures for host '$host'." >&2
exit 1
fi
qt_arch="$(echo "$archs" | grep -E "$arch_pattern" | head -n1 || true)"
if [[ -z "$qt_arch" ]]; then
echo "Unable to resolve Qt architecture for linux/x64." >&2
echo "$archs" >&2
exit 1
fi
modules="$(python3 -m aqt list-qt "$host" desktop --modules "$QT_VERSION" "$qt_arch" | tr ' \r\t' '\n' | sed '/^$/d' || true)"
if echo "$modules" | grep -qx "qtsvg"; then
echo "Installing Qt with optional module qtsvg"
python3 -m aqt install-qt "$host" desktop "$QT_VERSION" "$qt_arch" -m qtsvg -O "$PWD/.qt"
else
echo "qtsvg module not available for $host/$qt_arch on Qt $QT_VERSION; installing base Qt only"
python3 -m aqt install-qt "$host" desktop "$QT_VERSION" "$qt_arch" -O "$PWD/.qt"
fi
qt_root="$PWD/.qt/$QT_VERSION"
qt_dir="$qt_root/$qt_arch"
if [[ ! -d "$qt_dir/lib" ]]; then
qt_dir="$(find "$qt_root" -mindepth 1 -maxdepth 2 -type d | while read -r d; do [[ -d "$d/lib" ]] && { echo "$d"; break; }; done)"
fi
qt_lib_dir="$qt_dir/lib"
if [[ ! -d "$qt_lib_dir" ]]; then
echo "Qt library path not found under: $qt_root (initial: $qt_arch)" >&2
find "$qt_root" -maxdepth 3 -type d | sed 's/^/ /' >&2 || true
exit 1
fi
echo "Resolved Qt directory: $qt_dir"
tools_dir="$PWD/.qtjambi-deployer"
mkdir -p "$tools_dir"
base="https://repo1.maven.org/maven2/io/qtjambi"
curl -fsSL -o "$tools_dir/qtjambi-$QT_VERSION.jar" "$base/qtjambi/$QT_VERSION/qtjambi-$QT_VERSION.jar"
curl -fsSL -o "$tools_dir/qtjambi-deployer-$QT_VERSION.jar" "$base/qtjambi-deployer/$QT_VERSION/qtjambi-deployer-$QT_VERSION.jar"
curl -fsSL -o "$tools_dir/qtjambi-native-$qt_platform-$QT_VERSION.jar" "$base/qtjambi-native-$qt_platform/$QT_VERSION/qtjambi-native-$qt_platform-$QT_VERSION.jar"
java -Djava.library.path="$qt_lib_dir" \
-cp "$tools_dir/qtjambi-deployer-$QT_VERSION.jar:$tools_dir/qtjambi-$QT_VERSION.jar:$tools_dir/qtjambi-native-$qt_platform-$QT_VERSION.jar" \
io.qt.qtjambi.deployer.Main qt \
--qtdir="$qt_dir" \
--platform="$qt_platform" \
--target-version="$QT_VERSION" \
-d "$PWD/build/package-input"
ls -1 build/package-input/qt-lib-*.jar
- name: Prepare Dist
shell: bash
run: |
set -euo pipefail
mkdir -p dist
- name: Package Windows Bundles
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force package | Out-Null
jpackage `
--type app-image `
--name "${{ env.APP_NAME }}" `
--vendor "FooterManDev" `
--input build/package-input `
--main-jar tritium-app.jar `
--main-class "${{ env.MAIN_CLASS }}" `
--app-version "${{ steps.version.outputs.app_version }}" `
--java-options "-Dio.qt.deploymentdir=user" `
--icon "packaging/windows/jpackage/${{ env.APP_NAME }}.ico" `
--dest package
$cfgFile = Get-ChildItem -Path "package" -Recurse -Filter "${{ env.APP_NAME }}.cfg" | Select-Object -First 1
if (-not $cfgFile) {
throw "Launcher config file not found in generated Windows app-image."
}
$appDir = $cfgFile.DirectoryName
$bundledQtLibJars = Get-ChildItem -Path "build/package-input" -Filter "qt-lib-*.jar" -ErrorAction SilentlyContinue | Sort-Object Name
if ($bundledQtLibJars.Count -eq 0) {
throw "No qt-lib-*.jar files were generated in build/package-input; Qt deployment is incomplete."
}
$appQtLibJars = Get-ChildItem -Path $appDir -Filter "qt-lib-*.jar" -ErrorAction SilentlyContinue | Sort-Object Name
if ($appQtLibJars.Count -eq 0) {
throw "Generated Windows app-image is missing qt-lib-*.jar files in $appDir."
}
$nativeQtJambiJars = Get-ChildItem -Path $appDir -Filter "qtjambi-native-*.jar" -ErrorAction SilentlyContinue | Sort-Object Name
if ($nativeQtJambiJars.Count -eq 0) {
throw "Generated Windows app-image is missing qtjambi-native-*.jar files in $appDir."
}
$cfgClasspathLines = Get-Content $cfgFile.FullName | Where-Object { $_ -match '^app\.classpath=' }
if (-not ($cfgClasspathLines | Where-Object { $_ -match 'qt-lib-' }) -or -not ($cfgClasspathLines | Where-Object { $_ -match 'qtjambi-native-' })) {
throw "Generated Windows launcher config does not reference the required QtJambi runtime jars: $($cfgFile.FullName)"
}
$cfgClasspathLines | ForEach-Object { Write-Host "Launcher cfg: $($_)" }
$nativeQtJambiJars | ForEach-Object { Write-Host "App-image includes QtJambi native jar: $($_.Name)" }
$appQtLibJars | ForEach-Object { Write-Host "App-image includes Qt jar: $($_.Name)" }
New-Item -ItemType Directory -Force "package/${{ env.APP_NAME }}/licenses" | Out-Null
Copy-Item "LICENSE" "package/${{ env.APP_NAME }}/"
Copy-Item "THIRD_PARTY_NOTICES.md" "package/${{ env.APP_NAME }}/"
Copy-Item "third_party/licenses/*" "package/${{ env.APP_NAME }}/licenses/"
Compress-Archive -Path "package/${{ env.APP_NAME }}/*" -DestinationPath "dist/tritium-${{ matrix.platform }}-${{ matrix.arch }}.zip" -Force
jpackage `
--type msi `
--name "${{ env.APP_NAME }}" `
--vendor "FooterManDev" `
--app-version "${{ steps.version.outputs.app_version }}" `
--app-image "package/${{ env.APP_NAME }}" `
--icon "packaging/windows/jpackage/${{ env.APP_NAME }}.ico" `
--win-menu `
--win-shortcut `
--win-dir-chooser `
--win-per-user-install `
--win-upgrade-uuid "${{ env.WINDOWS_UPGRADE_UUID }}" `
--dest dist
$msi = Get-ChildItem -Path dist -Filter "*.msi" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $msi) {
throw "Failed to find generated MSI in dist/."
}
Move-Item -Path $msi.FullName -Destination "dist/tritium-${{ matrix.platform }}-${{ matrix.arch }}.msi" -Force
- name: Package Linux Portable Bundles
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
mkdir -p package
jpackage \
--type app-image \
--name "${{ env.APP_NAME }}" \
--input build/package-input \
--main-jar tritium-app.jar \
--main-class "${{ env.MAIN_CLASS }}" \
--app-version "${{ steps.version.outputs.app_version }}" \
--dest package
cfg_file="$(find package -type f -name "${{ env.APP_NAME }}.cfg" | head -n1 || true)"
if [[ -z "$cfg_file" ]]; then
echo "Launcher config file not found in generated Linux app-image." >&2
exit 1
fi
app_dir="$(dirname "$cfg_file")"
bundled_qt_lib_jars="$(find build/package-input -maxdepth 1 -type f -name 'qt-lib-*.jar' -exec basename {} \; | sort || true)"
if [[ -z "$bundled_qt_lib_jars" ]]; then
echo "No qt-lib-*.jar files were generated in build/package-input; Qt deployment is incomplete." >&2
exit 1
fi
app_qt_lib_jars="$(find "$app_dir" -maxdepth 1 -type f -name 'qt-lib-*.jar' -exec basename {} \; | sort || true)"
if [[ -z "$app_qt_lib_jars" ]]; then
echo "Generated Linux app-image is missing qt-lib-*.jar files in $app_dir." >&2
exit 1
fi
app_native_qtjambi_jars="$(find "$app_dir" -maxdepth 1 -type f -name 'qtjambi-native-*.jar' -exec basename {} \; | sort || true)"
if [[ -z "$app_native_qtjambi_jars" ]]; then
echo "Generated Linux app-image is missing qtjambi-native-*.jar files in $app_dir." >&2
exit 1
fi
cfg_classpath_lines="$(grep '^app\.classpath=' "$cfg_file" || true)"
if ! grep -q 'qt-lib-' <<< "$cfg_classpath_lines" || ! grep -q 'qtjambi-native-' <<< "$cfg_classpath_lines"; then
echo "Generated Linux launcher config does not reference the required QtJambi runtime jars: $cfg_file" >&2
exit 1
fi
printf '%s\n' "$cfg_classpath_lines" | sed 's/^/Launcher cfg: /'
printf '%s\n' "$app_native_qtjambi_jars" | sed 's/^/App-image includes QtJambi native jar: /'
printf '%s\n' "$app_qt_lib_jars" | sed 's/^/App-image includes Qt jar: /'
mkdir -p "package/${{ env.APP_NAME }}/licenses"
cp LICENSE "package/${{ env.APP_NAME }}/"
cp THIRD_PARTY_NOTICES.md "package/${{ env.APP_NAME }}/"
cp third_party/licenses/* "package/${{ env.APP_NAME }}/licenses/"
tar -C package -czf "dist/tritium-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz" "${{ env.APP_NAME }}"
appimage_arch="x86_64"
if curl -fsSL -o appimagetool.AppImage \
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${appimage_arch}.AppImage"; then
chmod +x appimagetool.AppImage
rm -rf AppDir
mkdir -p AppDir
cp -a "package/${{ env.APP_NAME }}/." AppDir/
printf '%s\n' \
'#!/bin/sh' \
'HERE="$(dirname "$(readlink -f "$0")")"' \
'exec "$HERE/bin/Tritium" "$@"' \
> AppDir/AppRun
chmod +x AppDir/AppRun
printf '%s\n' \
'[Desktop Entry]' \
'Type=Application' \
'Name=Tritium' \
'Exec=Tritium' \
'Icon=tritium' \
'Categories=Development;' \
'Terminal=false' \
> AppDir/Tritium.desktop
cp src/main/resources/icons/tritium.png AppDir/tritium.png
ln -sf tritium.png AppDir/.DirIcon
ARCH="$appimage_arch" APPIMAGE_EXTRACT_AND_RUN=1 \
./appimagetool.AppImage AppDir "dist/tritium-${{ matrix.platform }}-${{ matrix.arch }}.AppImage"
chmod +x "dist/tritium-${{ matrix.platform }}-${{ matrix.arch }}.AppImage"
else
echo "AppImage tooling unavailable for $appimage_arch, skipping AppImage artifact."
fi
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: tritium-${{ matrix.platform }}-${{ matrix.arch }}
path: dist/*
if-no-files-found: error
release:
name: Publish Release
runs-on: ubuntu-24.04
permissions:
contents: write
needs: build
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'))
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Build Artifacts
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: Generate Checksums
run: |
set -euo pipefail
cd release-assets
sha256sum * > SHA256SUMS.txt
- name: Resolve Release Tag
id: tag
shell: bash
env:
DISPATCH_TAG: ${{ github.event.inputs.tag }}
run: |
set -euo pipefail
raw=""
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
raw="${GITHUB_REF#refs/tags/v}"
fi
if [[ -z "$raw" && "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" && -n "${DISPATCH_TAG:-}" ]]; then
raw="${DISPATCH_TAG#v}"
fi
if [[ -z "$raw" ]]; then
raw="$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' build.gradle.kts | head -n1 || true)"
fi
numeric="$(echo "$raw" | sed -E 's/[^0-9.]+/./g; s/\.+/./g; s/^\.//; s/\.$//')"
if [[ -z "$numeric" ]]; then
echo "Unable to resolve numeric version from build.gradle.kts version='$raw'" >&2
exit 1
fi
IFS='.' read -r -a parts <<< "$numeric"
major="${parts[0]:-0}"
minor="${parts[1]:-0}"
patch="${parts[2]:-0}"
major_num="$((10#$major))"
minor_num="$((10#$minor))"
patch_num="$((10#$patch))"
echo "name=v${major_num}.${minor_num}.${patch_num}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.name }}
target_commitish: ${{ github.sha }}
name: Tritium ${{ steps.tag.outputs.name }}
prerelease: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.prerelease == 'true' }}
generate_release_notes: true
files: |
release-assets/*