Skip to content

Build

Build #13

Workflow file for this run

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.footermandev.tritium.Main
jobs:
build:
name: Build ${{ matrix.platform }}-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
platform: linux
arch: x64
experimental: false
- runner: ubuntu-24.04-arm
platform: linux
arch: arm64
experimental: false
- runner: windows-2025
platform: windows
arch: x64
experimental: false
- runner: windows-11-arm
platform: windows
arch: arm64
experimental: true
- runner: macos-15-intel
platform: macos
arch: x64
experimental: false
- runner: macos-15
platform: macos
arch: arm64
experimental: false
steps:
- name: Checkout
uses: actions/checkout@v4
- 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" ]]; 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]:-}"
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
build_number=""
if [[ "$is_release_build" == "true" ]]; then
headers_file="$(mktemp)"
body_file="$(mktemp)"
release_count=""
if curl -fsSL \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-D "$headers_file" \
-o "$body_file" \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=1&page=1"; then
body_compact="$(tr -d '[:space:]' < "$body_file")"
if [[ "$body_compact" == "[]" ]]; then
release_count="0"
else
last_page="$(grep -i '^link:' "$headers_file" | sed -nE 's/.*[?&]page=([0-9]+)>; rel=\"last\".*/\1/p' | tail -n1)"
if [[ "$last_page" =~ ^[0-9]+$ ]]; then
release_count="$last_page"
else
release_count="1"
fi
fi
fi
rm -f "$headers_file" "$body_file"
if [[ "$release_count" =~ ^[0-9]+$ ]]; then
build_number="$((release_count + 1))"
else
build_number="${GITHUB_RUN_NUMBER:-0}"
fi
elif [[ "$build_from_source" =~ ^[0-9]+$ ]]; then
build_number="$build_from_source"
else
build_number="${GITHUB_RUN_NUMBER:-0}"
fi
major_num="$((10#$major))"
minor_num="$((10#$minor))"
build_num="$((10#$build_number))"
app_version="${major_num}.${minor_num}.${build_num}"
mac_major_num="$major_num"
if [[ "$mac_major_num" -eq 0 ]]; then
mac_major_num=1
fi
mac_app_version="${mac_major_num}.${minor_num}.${build_num}"
echo "app_version=$app_version" >> "$GITHUB_OUTPUT"
echo "mac_app_version=$mac_app_version" >> "$GITHUB_OUTPUT"
echo "Resolved app version: $app_version"
echo "Resolved macOS package version: $mac_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: Build Shadow Jar (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: .\gradlew.bat --no-daemon clean shadowJar
- name: Build Shadow Jar (Non-Windows)
if: runner.os != 'Windows'
run: ./gradlew --no-daemon clean shadowJar
- 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
if ("${{ matrix.arch }}" -eq "arm64") {
$qtHost = "windows_arm64"
} else {
$qtHost = "windows"
}
$archOutput = python -m aqt list-qt $qtHost desktop --arch $env:QT_VERSION
$archs = $archOutput -split '\s+' | Where-Object { $_ -ne '' }
if ("${{ matrix.arch }}" -eq "arm64") {
$qtArch = $archs | Where-Object { $_ -match 'arm64' } | Select-Object -First 1
$qtPlatform = "windows-arm64"
} else {
$qtArch = $archs | Where-Object { $_ -match 'msvc' -and $_ -match '64' -and $_ -notmatch 'arm64' } | Select-Object -First 1
if (-not $qtArch) {
$qtArch = $archs | Where-Object { $_ -match '64' -and $_ -notmatch 'arm64' } | Select-Object -First 1
}
$qtPlatform = "windows-x64"
}
if (-not $qtArch) {
throw "Unable to resolve Qt architecture for windows/${{ matrix.arch }}. Available: $($archs -join ', ')"
}
$modulesOutput = python -m aqt list-qt $qtHost desktop --modules $env:QT_VERSION $qtArch
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"
} else {
Write-Host "qtsvg module not available for $qtHost/$qtArch on Qt $env:QT_VERSION; installing base Qt only"
python -m aqt install-qt $qtHost desktop $env:QT_VERSION $qtArch -O "$PWD\\.qt"
}
$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\\libs"
Get-ChildItem "$PWD\\build\\libs\\qt-lib-*.jar" | ForEach-Object { Write-Host "Bundled: $($_.Name)" }
- name: Bundle Qt Libraries (Non-Windows)
if: runner.os != 'Windows'
shell: bash
env:
QT_VERSION: ${{ steps.qtver.outputs.value }}
run: |
set -euo pipefail
python3 -m pip install --user --break-system-packages --upgrade pip aqtinstall
if [[ "${{ matrix.platform }}" == "linux" ]]; then
if [[ "${{ matrix.arch }}" == "arm64" ]]; then
host="linux_arm64"
else
host="linux"
fi
qt_platform="linux-${{ matrix.arch }}"
if [[ "${{ matrix.arch }}" == "x64" ]]; then
arch_pattern='(linux_)?gcc_64'
else
arch_pattern='(arm64|aarch64)'
fi
else
host="mac"
qt_platform="macos"
if [[ "${{ matrix.arch }}" == "x64" ]]; then
arch_pattern='clang_64'
else
arch_pattern='(clang_arm64|arm64|clang_64)'
fi
fi
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 $host/${{ matrix.arch }}." >&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/libs"
ls -1 build/libs/qt-lib-*.jar
- name: Prepare Dist
shell: bash
run: |
set -euo pipefail
mkdir -p dist
- name: Package Windows Portable Bundle
if: runner.os == 'Windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force package | Out-Null
jpackage `
--type app-image `
--name "${{ env.APP_NAME }}" `
--input build/libs `
--main-jar tritium.jar `
--main-class "${{ env.MAIN_CLASS }}" `
--app-version "${{ steps.version.outputs.app_version }}" `
--dest package
$cfgFile = Get-ChildItem -Path "package" -Recurse -Filter "${{ env.APP_NAME }}.cfg" | Select-Object -First 1
if ($cfgFile) {
$appDir = $cfgFile.DirectoryName
$qtLibJars = Get-ChildItem -Path $appDir -Filter "qt-lib-*.jar" | Sort-Object Name
if ($qtLibJars.Count -gt 0) {
$classpath = '$APPDIR/tritium.jar'
foreach ($jar in $qtLibJars) {
$classpath += ';$APPDIR/' + $jar.Name
}
$lines = Get-Content $cfgFile.FullName
$replaced = $false
$newLines = foreach ($line in $lines) {
if ($line -match '^app\.classpath=') {
$replaced = $true
"app.classpath=$classpath"
} else {
$line
}
}
if (-not $replaced) {
$newLines += "app.classpath=$classpath"
}
Set-Content -Path $cfgFile.FullName -Value $newLines -Encoding utf8
Write-Host "Updated launcher classpath in $($cfgFile.FullName)"
} else {
Write-Host "No qt-lib-*.jar files found in launcher app dir: $appDir"
}
} else {
Write-Host "Launcher config file not found for classpath patching."
}
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
- name: Package macOS Portable Bundle
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
mkdir -p package
jpackage \
--type app-image \
--name "${{ env.APP_NAME }}" \
--input build/libs \
--main-jar tritium.jar \
--main-class "${{ env.MAIN_CLASS }}" \
--app-version "${{ steps.version.outputs.mac_app_version }}" \
--dest package
cfg_file="$(find package -type f -name "${{ env.APP_NAME }}.cfg" | head -n1 || true)"
if [[ -n "$cfg_file" ]]; then
app_dir="$(dirname "$cfg_file")"
qt_lib_jars="$(find "$app_dir" -maxdepth 1 -type f -name 'qt-lib-*.jar' -exec basename {} \; | sort || true)"
if [[ -n "$qt_lib_jars" ]]; then
classpath="\$APPDIR/tritium.jar"
while IFS= read -r jar; do
[[ -z "$jar" ]] && continue
classpath="${classpath}:\$APPDIR/${jar}"
done <<< "$qt_lib_jars"
awk -v cp="$classpath" 'BEGIN{r=0} /^app\.classpath=/{print "app.classpath=" cp; r=1; next} {print} END{if(!r) print "app.classpath=" cp}' "$cfg_file" > "${cfg_file}.tmp"
mv "${cfg_file}.tmp" "$cfg_file"
echo "Updated launcher classpath in $cfg_file"
else
echo "No qt-lib-*.jar files found in launcher app dir: $app_dir"
fi
else
echo "Launcher config file not found for classpath patching."
fi
mkdir -p "package/${{ env.APP_NAME }}.app/Contents/Resources/licenses"
cp LICENSE "package/${{ env.APP_NAME }}.app/Contents/Resources/"
cp THIRD_PARTY_NOTICES.md "package/${{ env.APP_NAME }}.app/Contents/Resources/"
cp third_party/licenses/* "package/${{ env.APP_NAME }}.app/Contents/Resources/licenses/"
tar -C package -czf "dist/tritium-${{ matrix.platform }}-${{ matrix.arch }}.tar.gz" "${{ env.APP_NAME }}.app"
- 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/libs \
--main-jar tritium.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 [[ -n "$cfg_file" ]]; then
app_dir="$(dirname "$cfg_file")"
qt_lib_jars="$(find "$app_dir" -maxdepth 1 -type f -name 'qt-lib-*.jar' -exec basename {} \; | sort || true)"
if [[ -n "$qt_lib_jars" ]]; then
classpath="\$APPDIR/tritium.jar"
while IFS= read -r jar; do
[[ -z "$jar" ]] && continue
classpath="${classpath}:\$APPDIR/${jar}"
done <<< "$qt_lib_jars"
awk -v cp="$classpath" 'BEGIN{r=0} /^app\.classpath=/{print "app.classpath=" cp; r=1; next} {print} END{if(!r) print "app.classpath=" cp}' "$cfg_file" > "${cfg_file}.tmp"
mv "${cfg_file}.tmp" "$cfg_file"
echo "Updated launcher classpath in $cfg_file"
else
echo "No qt-lib-*.jar files found in launcher app dir: $app_dir"
fi
else
echo "Launcher config file not found for classpath patching."
fi
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 }}"
if [[ "${{ matrix.arch }}" == "arm64" ]]; then
appimage_arch="aarch64"
else
appimage_arch="x86_64"
fi
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: startsWith(github.ref, 'refs/tags/v') || 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
run: |
set -euo pipefail
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "name=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
elif [[ -n "${{ github.event.inputs.tag }}" ]]; then
input_tag="${{ github.event.inputs.tag }}"
if [[ "$input_tag" == v* ]]; then
echo "name=$input_tag" >> "$GITHUB_OUTPUT"
else
echo "name=v$input_tag" >> "$GITHUB_OUTPUT"
fi
else
raw="$(sed -nE 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' build.gradle.kts | head -n1 || true)"
cleaned="$(echo "$raw" | sed -E 's/[^0-9A-Za-z._-]+/-/g; s/^-+//; s/-+$//')"
if [[ -z "$cleaned" ]]; then
cleaned="0.0.0"
fi
echo "name=v${cleaned}-build.${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
fi
- 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/*