mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
desktop: release workflow, AppImage and tar.xz for Linux
- desktop-release.yml: on v* tags, builds every desktop installer with the tag's version (via desktop.yml, now also a reusable workflow) and attaches them to the tag's GitHub release. - desktop/packaging/linux.sh: builds a distro-independent .tar.xz and an .AppImage from the createDistributable image; CI smoke-tests the AppImage. - Installers are named Amber-<version>-<platform>.<ext> so the arm64 and x64 DMGs no longer collide. - packageVersion can be overridden with -PdesktopVersion. - Inside an AppImage, use $APPIMAGE for the nostrconnect:// handler and the start-on-boot unit: the binary's own path is a per-launch mount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8d90f2cc1b
commit
573690eebd
@@ -0,0 +1,70 @@
|
|||||||
|
name: Release Desktop
|
||||||
|
|
||||||
|
# On a v* tag, builds the desktop installers for Linux (.deb, .rpm, .AppImage,
|
||||||
|
# .tar.xz), Windows (.msi, .exe) and macOS (.dmg, arm64 + x64) with the tag's
|
||||||
|
# version and attaches them to the tag's GitHub release, next to the Android
|
||||||
|
# assets from create-release.yml.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
# To rebuild an existing release: Run workflow, "Use workflow from" the tag.
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
tag: ${{ steps.v.outputs.tag }}
|
||||||
|
version: ${{ steps.v.outputs.version }}
|
||||||
|
steps:
|
||||||
|
# Installers need a plain X.Y.Z: v6.7.0-pre1 packages as 6.7.0.
|
||||||
|
- id: v
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
[ "$GITHUB_REF_TYPE" = tag ] || { echo "::error::Run this on a tag, not $GITHUB_REF"; exit 1; }
|
||||||
|
version=${TAG#v}
|
||||||
|
version=${version%%-*}
|
||||||
|
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Tag $TAG is not vX.Y.Z[-suffix]"; exit 1; }
|
||||||
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: version
|
||||||
|
uses: ./.github/workflows/desktop.yml
|
||||||
|
with:
|
||||||
|
version: ${{ needs.version.outputs.version }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [version, build]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
pattern: amber-desktop-*
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
# create-release.yml creates the release for the Android APK at the same
|
||||||
|
# time; whichever gets there first creates it and the other attaches.
|
||||||
|
- name: Attach installers to the release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
TAG: ${{ needs.version.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
ls -l dist
|
||||||
|
prerelease=$([[ "$TAG" == *-pre* ]] && echo --prerelease || true)
|
||||||
|
for attempt in 1 2 3 4 5; do
|
||||||
|
gh release view "$TAG" > /dev/null 2>&1 && break
|
||||||
|
gh release create "$TAG" --verify-tag --title "Release refs/tags/$TAG" --notes "" $prerelease && break
|
||||||
|
sleep 20
|
||||||
|
done
|
||||||
|
gh release upload "$TAG" dist/* --clobber
|
||||||
@@ -2,6 +2,7 @@ name: Test/Build Desktop
|
|||||||
|
|
||||||
# Lints, tests and packages the Compose for Desktop signer (:desktop) on every
|
# Lints, tests and packages the Compose for Desktop signer (:desktop) on every
|
||||||
# OS it ships for, and uploads the installers so each change can be tried out.
|
# OS it ships for, and uploads the installers so each change can be tried out.
|
||||||
|
# desktop-release.yml calls it with the release version.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -13,8 +14,16 @@ on:
|
|||||||
- 'gradle.properties'
|
- 'gradle.properties'
|
||||||
- '.github/workflows/desktop.yml'
|
- '.github/workflows/desktop.yml'
|
||||||
push:
|
push:
|
||||||
|
# Branches only: tag pushes are built by desktop-release.yml.
|
||||||
|
branches: ['**']
|
||||||
paths: *desktop-paths
|
paths: *desktop-paths
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Package version (X.Y.Z); defaults to the one in desktop/build.gradle.kts'
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -27,6 +36,10 @@ defaults:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
env:
|
||||||
|
VERSION_ARG: ${{ inputs.version && format('-PdesktopVersion={0}', inputs.version) || '' }}
|
||||||
|
RELEASE_VERSION: ${{ inputs.version }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -67,9 +80,9 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: linux
|
- name: linux-x86_64
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
- name: windows
|
- name: windows-x64
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- name: macos-arm64
|
- name: macos-arm64
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
@@ -98,6 +111,10 @@ jobs:
|
|||||||
- name: Create keystore.properties
|
- name: Create keystore.properties
|
||||||
run: touch keystore.properties
|
run: touch keystore.properties
|
||||||
|
|
||||||
|
# Names the installers: the release version, or the commit for CI builds.
|
||||||
|
- name: Set installer version
|
||||||
|
run: echo "DIST_VERSION=${RELEASE_VERSION:-dev-${GITHUB_SHA::7}}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Install Linux packaging tools
|
- name: Install Linux packaging tools
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: sudo apt-get update -qq && sudo apt-get install -y -qq rpm xvfb
|
run: sudo apt-get update -qq && sudo apt-get install -y -qq rpm xvfb
|
||||||
@@ -108,36 +125,46 @@ jobs:
|
|||||||
# .deb/.rpm on Linux, .msi/.exe on Windows (WiX is fetched by the
|
# .deb/.rpm on Linux, .msi/.exe on Windows (WiX is fetched by the
|
||||||
# Compose plugin), .dmg on macOS (unsigned).
|
# Compose plugin), .dmg on macOS (unsigned).
|
||||||
- name: Package (gradle)
|
- name: Package (gradle)
|
||||||
run: ./gradlew :desktop:packageDistributionForCurrentOs --no-daemon
|
run: ./gradlew :desktop:packageDistributionForCurrentOs $VERSION_ARG --no-daemon
|
||||||
|
|
||||||
# Start the packaged app on a virtual display and make sure it is still
|
# Distro-independent bundles from the standalone app image (packageDeb/Rpm
|
||||||
# alive after a while, i.e. it doesn't crash on startup. HOME points at an
|
# build from a temporary one).
|
||||||
# empty dir so it boots like a first launch.
|
- name: Package AppImage and tar.xz
|
||||||
- name: Smoke test packaged app
|
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
# packageDeb/Rpm build from a temporary app image; build the
|
./gradlew :desktop:createDistributable $VERSION_ARG --no-daemon
|
||||||
# standalone one to launch.
|
desktop/packaging/linux.sh "$DIST_VERSION" dist
|
||||||
./gradlew :desktop:createDistributable --no-daemon
|
|
||||||
AMBER="$PWD/desktop/build/compose/binaries/main/app/Amber/bin/Amber"
|
# Start the AppImage on a virtual display and make sure it is still alive
|
||||||
export HOME="$RUNNER_TEMP/amber-home" AMBER_DISABLE_TRAY=1
|
# after a while, i.e. it doesn't crash on startup. HOME points at an
|
||||||
|
# empty dir so it boots like a first launch.
|
||||||
|
- name: Smoke test AppImage
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
export HOME="$RUNNER_TEMP/amber-home" AMBER_DISABLE_TRAY=1 APPIMAGE_EXTRACT_AND_RUN=1
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
xvfb-run -a "$AMBER" > "$RUNNER_TEMP/amber.log" 2>&1 &
|
xvfb-run -a dist/Amber-*.AppImage > "$RUNNER_TEMP/amber.log" 2>&1 &
|
||||||
sleep 30
|
sleep 30
|
||||||
cat "$RUNNER_TEMP/amber.log"
|
cat "$RUNNER_TEMP/amber.log"
|
||||||
pgrep -f 'binaries/main/app/Amber/bin/Amber' > /dev/null || { echo "::error::Amber exited during startup"; exit 1; }
|
pgrep -f 'usr/lib/amber/bin/Amber' > /dev/null || { echo "::error::Amber exited during startup"; exit 1; }
|
||||||
pkill -f 'binaries/main/app/Amber/bin/Amber' || true
|
pkill -f 'usr/lib/amber/bin/Amber' || true
|
||||||
|
|
||||||
|
# Name every installer Amber-<version>-<platform>.<ext>: the macOS DMGs
|
||||||
|
# of both architectures would otherwise collide in the release.
|
||||||
|
- name: Collect installers
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
for f in desktop/build/compose/binaries/main/*/*.{deb,rpm,msi,exe,dmg}; do
|
||||||
|
[ -e "$f" ] || continue
|
||||||
|
cp "$f" "dist/Amber-$DIST_VERSION-${{ matrix.name }}.${f##*.}"
|
||||||
|
done
|
||||||
|
ls -l dist
|
||||||
|
|
||||||
- name: Upload installers
|
- name: Upload installers
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: amber-desktop-${{ matrix.name }}-${{ github.sha }}
|
name: amber-desktop-${{ matrix.name }}
|
||||||
path: |
|
path: dist/*
|
||||||
desktop/build/compose/binaries/main/deb/*.deb
|
|
||||||
desktop/build/compose/binaries/main/rpm/*.rpm
|
|
||||||
desktop/build/compose/binaries/main/msi/*.msi
|
|
||||||
desktop/build/compose/binaries/main/exe/*.exe
|
|
||||||
desktop/build/compose/binaries/main/dmg/*.dmg
|
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ compose.desktop {
|
|||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe, TargetFormat.Deb, TargetFormat.Rpm)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Exe, TargetFormat.Deb, TargetFormat.Rpm)
|
||||||
packageName = "Amber"
|
packageName = "Amber"
|
||||||
packageVersion = "6.2.3"
|
// Release builds pass the tag's version (-PdesktopVersion=X.Y.Z).
|
||||||
|
packageVersion = providers.gradleProperty("desktopVersion").getOrElse("6.2.3")
|
||||||
description = "Amber - Nostr event signer"
|
description = "Amber - Nostr event signer"
|
||||||
vendor = "greenart7c3"
|
vendor = "greenart7c3"
|
||||||
copyright = "© greenart7c3. Distributed under the MIT license."
|
copyright = "© greenart7c3. Distributed under the MIT license."
|
||||||
|
|||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Builds the distro-independent Linux bundles from the jpackage app image
|
||||||
|
# (./gradlew :desktop:createDistributable):
|
||||||
|
#
|
||||||
|
# Amber-<version>-linux-x86_64.tar.xz unpack anywhere, run Amber/bin/Amber
|
||||||
|
# Amber-<version>-x86_64.AppImage single self-contained executable
|
||||||
|
#
|
||||||
|
# desktop/packaging/linux.sh <version> <out-dir>
|
||||||
|
#
|
||||||
|
# Needs curl (to fetch appimagetool) and xz. FUSE is not needed: appimagetool
|
||||||
|
# runs with APPIMAGE_EXTRACT_AND_RUN.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
version=${1:?usage: linux.sh <version> <out-dir>}
|
||||||
|
out=${2:?usage: linux.sh <version> <out-dir>}
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
desktop=$(cd "$here/.." && pwd)
|
||||||
|
image="$desktop/build/compose/binaries/main/app/Amber"
|
||||||
|
work="$desktop/build/linux-bundles"
|
||||||
|
|
||||||
|
[ -x "$image/bin/Amber" ] || { echo "No app image at $image; run :desktop:createDistributable first" >&2; exit 1; }
|
||||||
|
mkdir -p "$out"
|
||||||
|
out=$(cd "$out" && pwd)
|
||||||
|
rm -rf "$work"
|
||||||
|
mkdir -p "$work"
|
||||||
|
|
||||||
|
tar -C "$(dirname "$image")" -cJf "$out/Amber-$version-linux-x86_64.tar.xz" Amber
|
||||||
|
|
||||||
|
appdir="$work/Amber.AppDir"
|
||||||
|
mkdir -p "$appdir/usr/lib"
|
||||||
|
cp -a "$image" "$appdir/usr/lib/amber"
|
||||||
|
cp "$desktop/src/main/resources/icon.png" "$appdir/amber.png"
|
||||||
|
ln -s amber.png "$appdir/.DirIcon"
|
||||||
|
# StartupWMClass matches the X11 class Main.kt sets, so docks show this icon.
|
||||||
|
cat > "$appdir/amber.desktop" << 'EOF'
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Amber
|
||||||
|
Comment=Nostr event signer
|
||||||
|
Exec=Amber %u
|
||||||
|
Icon=amber
|
||||||
|
Categories=Network;
|
||||||
|
Terminal=false
|
||||||
|
StartupWMClass=Amber
|
||||||
|
MimeType=x-scheme-handler/nostrconnect;
|
||||||
|
EOF
|
||||||
|
cat > "$appdir/AppRun" << 'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
here=$(dirname "$(readlink -f "$0")")
|
||||||
|
exec "$here/usr/lib/amber/bin/Amber" "$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$appdir/AppRun"
|
||||||
|
|
||||||
|
tool="$work/appimagetool"
|
||||||
|
curl -fsSL -o "$tool" https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||||
|
chmod +x "$tool"
|
||||||
|
ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 "$tool" --no-appstream "$appdir" "$out/Amber-$version-x86_64.AppImage"
|
||||||
|
|
||||||
|
ls -l "$out"
|
||||||
@@ -45,7 +45,12 @@ object AutoStart {
|
|||||||
/** Set by the jpackage launcher to the installed binary; absent in dev (gradle) runs. */
|
/** Set by the jpackage launcher to the installed binary; absent in dev (gradle) runs. */
|
||||||
private fun packagedExecutable(): String? = System.getProperty("jpackage.app-path")?.takeIf { it.isNotBlank() }
|
private fun packagedExecutable(): String? = System.getProperty("jpackage.app-path")?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
private fun currentExecutable(): String? = runCatching {
|
/**
|
||||||
|
* Inside an AppImage the binary runs from a per-launch mount
|
||||||
|
* (/tmp/.mount_*), so use the AppImage file itself; its runtime exports
|
||||||
|
* the path as APPIMAGE.
|
||||||
|
*/
|
||||||
|
private fun currentExecutable(): String? = System.getenv("APPIMAGE")?.takeIf { it.isNotBlank() } ?: runCatching {
|
||||||
String(java.nio.file.Files.readAllBytes(java.nio.file.Path.of("/proc/self/cmdline")), Charsets.UTF_8)
|
String(java.nio.file.Files.readAllBytes(java.nio.file.Path.of("/proc/self/cmdline")), Charsets.UTF_8)
|
||||||
.split('\u0000')
|
.split('\u0000')
|
||||||
.firstOrNull { it.isNotBlank() }
|
.firstOrNull { it.isNotBlank() }
|
||||||
|
|||||||
@@ -236,7 +236,8 @@ object UriLaunch {
|
|||||||
/** Desktop-entry Exec quoting: double quotes with backslash escapes. */
|
/** Desktop-entry Exec quoting: double quotes with backslash escapes. */
|
||||||
internal fun quoteForDesktopEntry(value: String): String = if (value.none { it in " \t\"'\\" }) value else "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
|
internal fun quoteForDesktopEntry(value: String): String = if (value.none { it in " \t\"'\\" }) value else "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
|
||||||
|
|
||||||
private fun currentCommandLine(): String? = runCatching {
|
/** The AppImage file rather than its per-launch mount; see AutoStart.currentExecutable. */
|
||||||
|
private fun currentCommandLine(): String? = System.getenv("APPIMAGE")?.takeIf { it.isNotBlank() } ?: runCatching {
|
||||||
String(Files.readAllBytes(Path.of("/proc/self/cmdline")), Charsets.UTF_8)
|
String(Files.readAllBytes(Path.of("/proc/self/cmdline")), Charsets.UTF_8)
|
||||||
.split('\u0000')
|
.split('\u0000')
|
||||||
.firstOrNull { it.isNotBlank() }
|
.firstOrNull { it.isNotBlank() }
|
||||||
|
|||||||
Reference in New Issue
Block a user