Skip to content

Add upstream nightly package automation #2

Add upstream nightly package automation

Add upstream nightly package automation #2

Workflow file for this run

# Packaging CI — verifies the recipes in packaging/{linux,macos,windows}/
# still produce working artifacts on every push to a branch that includes
# packaging/ changes. NOT tag-triggered (that's release.yml later); this
# is the per-PR sanity gate.
#
# Each platform builds on its native GHA runner (no QEMU emulation).
# macOS Universal2 is the special case — built by parallel arm64 + x86_64
# jobs and merged in a follow-up `lipo` job, since standard Homebrew on
# Apple Silicon ships native-arch-only dylibs.
#
# Artifacts uploaded per matrix entry; not assembled into a Release here.
name: Packaging
on:
# Auto-fires only on push to master — i.e. when a PR is merged. Open
# PRs do NOT trigger this workflow; reviewers who want to inspect a
# PR's artifacts before merging can run `workflow_dispatch` against
# the PR branch from the Actions UI. This avoids the double-fire of
# "fires on PR open AND on merge" which is the most expensive shape.
# Path filter ensures doc-only commits don't burn CI either.
push:
branches:
- master
- 'packaging**'
paths:
- 'packaging/**'
- '.github/workflows/packaging.yml'
- 'src/**'
- 'CMakeLists.txt'
- 'cmake/**'
workflow_dispatch:
inputs:
only:
description: >-
Comma-separated list of platform tracks to run.
Default empty = run all. Useful during iteration to focus
on the failing tracks without paying for the green ones.
Valid: appimage, flatpak, macos, windows.
required: false
type: string
default: ''
checkout_ref:
description: >-
Optional git ref to check out for packaging. Defaults to the
workflow ref. Used by upstream nightly automation.
required: false
type: string
default: ''
# Lets release.yml invoke this matrix as a reusable workflow on tag
# pushes — same artifact set, no duplication of the build steps.
workflow_call:
inputs:
only:
required: false
type: string
default: ''
checkout_ref:
required: false
type: string
default: ''
permissions:
contents: read
jobs:
# ------------------------------------------------------------------
# Linux — AppImage on x86_64 + aarch64 native runners
# ------------------------------------------------------------------
appimage:
name: AppImage (${{ matrix.arch }})
if: >-
github.event_name != 'workflow_dispatch' ||
inputs.only == '' ||
contains(inputs.only, 'appimage')
strategy:
fail-fast: false
matrix:
include:
- { arch: x86_64, runs-on: ubuntu-22.04 }
- { arch: aarch64, runs-on: ubuntu-22.04-arm }
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # for git describe in build.sh
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Build AppImage
run: packaging/linux/build.sh appimage
- name: Validate via cross-distro Docker matrix
run: packaging/linux/build.sh validate
- uses: actions/upload-artifact@v7
with:
name: appimage-${{ matrix.arch }}
path: dist/aMule-*-${{ matrix.arch }}.AppImage
if-no-files-found: error
retention-days: 3
# ------------------------------------------------------------------
# Linux — Flatpak on x86_64 + aarch64 native runners
# ------------------------------------------------------------------
flatpak:
name: Flatpak (${{ matrix.arch }})
if: >-
github.event_name != 'workflow_dispatch' ||
inputs.only == '' ||
contains(inputs.only, 'flatpak')
strategy:
fail-fast: false
matrix:
# ubuntu-24.04 (not 22.04 like the AppImage job) so the host
# ships appstream 1.0+ which provides /usr/bin/appstream-compose.
# flatpak-builder shells out to that binary at the end of every
# build to compose AppStream metadata; on 22.04 (appstream 0.15)
# the binary is absent and flatpak-builder fails with
# "bwrap: execvp appstream-compose: No such file or directory".
# Host glibc doesn't matter for Flatpak builds (output runs
# inside the GNOME Platform sandbox), so bumping the runner
# has no compat downside, unlike the AppImage matrix.
include:
- { arch: x86_64, runs-on: ubuntu-24.04 }
- { arch: aarch64, runs-on: ubuntu-24.04-arm }
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Install flatpak + flatpak-builder + GNOME runtime
run: |
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder gettext-base
flatpak remote-add --user --if-not-exists flathub \
https://flathub.org/repo/flathub.flatpakrepo
# Read GNOME_RUNTIME_VERSION from versions.env so this stays
# in sync with whatever the manifest pins.
set -a; . packaging/linux/versions.env; set +a
flatpak install --user --noninteractive flathub \
"org.gnome.Platform//${GNOME_RUNTIME_VERSION}" \
"org.gnome.Sdk//${GNOME_RUNTIME_VERSION}"
- name: Build Flatpak
run: packaging/linux/build.sh flatpak
- uses: actions/upload-artifact@v7
with:
name: flatpak-${{ matrix.arch }}
path: dist/aMule-*-${{ matrix.arch }}.flatpak
if-no-files-found: error
retention-days: 3
# ------------------------------------------------------------------
# macOS — native arm64 + x86_64 builds, then lipo-merge to Universal2
# ------------------------------------------------------------------
macos-build:
name: macOS .app (${{ matrix.arch }})
if: >-
github.event_name != 'workflow_dispatch' ||
inputs.only == '' ||
contains(inputs.only, 'macos')
# Both arches build on macos-15 (Apple Silicon) — the legacy
# macos-13 (Intel) runner is being retired by GitHub and has been
# flaky for months. The x86_64 build uses Rosetta: a parallel Intel
# Homebrew install at /usr/local supplies x86_64 dep bottles, and
# every command that produces x86_64 output is wrapped in
# `arch -x86_64`. CMake honours MACOS_ARCHITECTURES from the env
# (handled in packaging/macos/build.sh) so clang emits the right
# slice. End result is identical to a native Intel build, with the
# benefit of running on a much beefier ARM host.
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
archcmd: ''
brew_prefix: /opt/homebrew
- arch: x86_64
archcmd: 'arch -x86_64'
brew_prefix: /usr/local
runs-on: macos-15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Bootstrap x86_64 Homebrew (Rosetta)
if: matrix.arch == 'x86_64'
run: |
# Apple Silicon runners ship Rosetta preinstalled. Install
# x86_64 Homebrew at /usr/local so we can fetch x86_64 bottles
# for our deps. The native /opt/homebrew install is left alone.
arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- name: Install Homebrew deps
run: |
BREW=${{ matrix.brew_prefix }}/bin/brew
${{ matrix.archcmd }} $BREW update
${{ matrix.archcmd }} $BREW install --quiet \
cmake wxwidgets cryptopp libupnp gd gettext boost dylibbundler libmaxminddb
# Prepend the matrix-selected brew prefix to PATH so every
# `brew --prefix` / `cmake` / `pkg-config` invocation in the
# build script resolves against the right install. gettext
# is keg-only on macOS; add msgfmt to PATH for the NLS probe.
echo "${{ matrix.brew_prefix }}/bin" >> "$GITHUB_PATH"
echo "${{ matrix.brew_prefix }}/opt/gettext/bin" >> "$GITHUB_PATH"
- name: Build native-arch .app
env:
MACOS_ARCHITECTURES: ${{ matrix.arch }}
run: ${{ matrix.archcmd }} packaging/macos/build.sh
- name: Smoke-test amuled --version
run: |
# Mount the produced .dmg at a fixed path. The volume name
# contains spaces (e.g. "aMule 2.3.3-foo"), and parsing it
# out of hdiutil's tabular output is fragile — supply
# -mountpoint instead.
DMG=$(ls dist/aMule-*-macOS.dmg | head -1)
MNT=/tmp/amulemnt
mkdir -p "${MNT}"
hdiutil attach -nobrowse -mountpoint "${MNT}" "${DMG}"
# x86_64 binaries on Apple Silicon run via Rosetta automatically
# (no explicit arch prefix needed for execution), but be
# consistent. amuled --version exits 255 (wxApp::OnInit returns
# false after printing version → wxEntry returns -1). Check the
# banner via grep instead of relying on exit code. Pattern
# accepts the dev placeholder ("aMuleD GIT ...") and any
# tagged-release banner ("aMuleD 3.0.0 ...", etc.).
${{ matrix.archcmd }} "${MNT}/aMule.app/Contents/MacOS/amuled" --version 2>&1 | grep -q '^aMuleD '
# Sanity-check that aMuleGUI.app (the remote-GUI client) is
# also at the dmg root — packaging/macos/build.sh stages it
# next to aMule.app via make_dmg, so its absence would mean
# BUILD_REMOTEGUI didn't actually produce a bundle.
[ -d "${MNT}/aMuleGUI.app" ] || { echo "aMuleGUI.app missing from per-arch dmg" >&2; exit 1; }
hdiutil detach "${MNT}"
- uses: actions/upload-artifact@v7
with:
# Upload the produced .app tree (not the .dmg) so the merge
# job can lipo-merge mach-o files. The final lipo'd .app gets
# re-wrapped into a Universal2 .dmg in the merge job.
name: macos-app-${{ matrix.arch }}
path: build-macos/src/aMule.app
if-no-files-found: error
# Preserve symlinks + permissions inside the .app.
retention-days: 1
- uses: actions/upload-artifact@v7
with:
# aMuleGUI.app is a separate bundle (the remote-GUI client);
# uploaded alongside aMule.app so the merge job can lipo it
# the same way and ship both in the Universal2 .dmg.
name: macos-gui-${{ matrix.arch }}
path: build-macos/src/aMuleGUI.app
if-no-files-found: error
retention-days: 1
macos-universal2:
name: macOS Universal2 .dmg
needs: macos-build
if: >-
github.event_name != 'workflow_dispatch' ||
inputs.only == '' ||
contains(inputs.only, 'macos')
runs-on: macos-15
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.checkout_ref || github.ref }}
- uses: actions/download-artifact@v8
with:
name: macos-app-arm64
path: arm64.app
- uses: actions/download-artifact@v8
with:
name: macos-app-x86_64
path: x86_64.app
- uses: actions/download-artifact@v8
with:
name: macos-gui-arm64
path: arm64-gui.app
- uses: actions/download-artifact@v8
with:
name: macos-gui-x86_64
path: x86_64-gui.app
- name: lipo-merge into Universal2 .app bundles
run: |
# Merges two per-arch .app bundles (BASE = arm64, OTHER =
# x86_64) into a Universal2 OUT bundle. Uses the arm64 .app
# as the base layout; for every Mach-O file inside it, runs
# lipo-create with the matching x86_64 sibling. Side files
# (Info.plist, .icns, locale catalogs) are arch-agnostic
# and inherited from arm64 unchanged.
merge_app() {
local BASE="$1"
local OTHER="$2"
local OUT="$3"
cp -R "${BASE}" "${OUT}"
local rel local_arm local_x86
while IFS= read -r -d '' rel; do
local_arm="${BASE}/${rel}"
local_x86="${OTHER}/${rel}"
if file "${local_arm}" | grep -q "Mach-O" && [ -f "${local_x86}" ]; then
lipo -create -output "${OUT}/${rel}" "${local_arm}" "${local_x86}"
# `lipo -create -output` writes the new file with the
# process umask (default 022 -> rw-r--r--), discarding
# the executable bit on the source. macOS launchd then
# refuses to spawn the bundle with errno 13 (EACCES)
# because access(X_OK) on the main exec fails. Restore
# the executable bit so launchd can spawn the binary.
chmod +x "${OUT}/${rel}"
fi
done < <(cd "${BASE}" && find . -type f -print0 | sed 's|^\./||g')
# Re-apply ad-hoc signatures to every nested Mach-O. clang on
# Apple Silicon emits ad-hoc-signed binaries by default; lipo
# invalidates those signatures on merge, and macOS amfid then
# refuses to load the resulting fat binary. `codesign --force
# --deep --sign -` rewrites valid ad-hoc sigs across the whole
# bundle. Required even before notarization for the binary to
# launch at all on Apple Silicon.
codesign --force --deep --sign - "${OUT}"
}
merge_app arm64.app x86_64.app aMule.app
merge_app arm64-gui.app x86_64-gui.app aMuleGUI.app
- name: Build Universal2 .dmg
run: |
VERSION=$(git describe --tags --always)
mkdir -p dist
STAGE=$(mktemp -d)
cp -R aMule.app "${STAGE}/"
cp -R aMuleGUI.app "${STAGE}/"
ln -s /Applications "${STAGE}/Applications"
hdiutil create -volname "aMule ${VERSION}" \
-srcfolder "${STAGE}" -ov -format UDZO \
"dist/aMule-${VERSION}-macOS-universal2.dmg"
- uses: actions/upload-artifact@v7
with:
name: macos-universal2
path: dist/aMule-*-macOS-universal2.dmg
if-no-files-found: error
# ------------------------------------------------------------------
# Windows — portable .zip for x64 (MINGW64) and ARM64 (CLANGARM64).
# ------------------------------------------------------------------
windows-zip:
name: Windows portable .zip (${{ matrix.arch }})
if: >-
github.event_name != 'workflow_dispatch' ||
inputs.only == '' ||
contains(inputs.only, 'windows')
strategy:
fail-fast: false
matrix:
include:
- arch: x64
msystem: MINGW64
runs-on: windows-2025
pkg_prefix: mingw-w64-x86_64
- arch: arm64
msystem: CLANGARM64
runs-on: windows-11-arm
pkg_prefix: mingw-w64-clang-aarch64
runs-on: ${{ matrix.runs-on }}
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.checkout_ref || github.ref }}
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: true
install: >-
${{ matrix.pkg_prefix }}-boost
${{ matrix.pkg_prefix }}-cmake
${{ matrix.pkg_prefix }}-crypto++
${{ matrix.pkg_prefix }}-gettext
${{ matrix.pkg_prefix }}-libgd
${{ matrix.pkg_prefix }}-libmaxminddb
${{ matrix.pkg_prefix }}-make
${{ matrix.pkg_prefix }}-pkgconf
${{ matrix.pkg_prefix }}-pupnp
${{ matrix.pkg_prefix }}-readline
${{ matrix.pkg_prefix }}-toolchain
${{ matrix.pkg_prefix }}-wxwidgets3.2-msw
${{ matrix.pkg_prefix }}-zlib
git
zip
- name: Configure git safe.directory for MSYS2 git
# MSYS2 git (now installed above) uses a global config separate
# from the Windows git at C:\Program Files\Git\bin\git.exe that
# actions/checkout configures. Without this entry, MSYS2 git
# may trip the "dubious ownership" guard on the workspace dir,
# which would cause `git describe --tags` inside
# packaging/windows/build.sh to fall through the `2>/dev/null
# || echo "snapshot"` fallback and produce
# aMule-snapshot-Windows-*.zip instead of
# aMule-<tag>-Windows-*.zip.
run: git config --global --add safe.directory '*'
- name: Build portable .zip
env:
WINDOWS_MSYSTEM: ${{ matrix.msystem }}
run: packaging/windows/build.sh
- name: Smoke-test amuled.exe --version
run: |
# Two layered quirks here:
# 1. amuled.exe --version exits 255 (wxApp::OnInit returns
# false after banner → wxEntry returns -1). || true
# swallows that.
# 2. Piping `amuled.exe ... | grep -q` on MSYS2 bash exits
# 127: grep -q closes its pipe end at first match,
# amuled.exe gets SIGPIPE on its next stdout flush, MSYS2
# surfaces that as 127. Capture to a file first, then
# grep the file — no pipe race.
./amule-portable-${{ matrix.arch }}/bin/amuled.exe --version \
> /tmp/version.txt 2>&1 || true
cat /tmp/version.txt
# Pattern accepts the dev placeholder ("aMuleD GIT ...") and
# any tagged-release banner ("aMuleD 3.0.0 ...", etc.).
grep -q '^aMuleD ' /tmp/version.txt
- uses: actions/upload-artifact@v7
with:
name: windows-${{ matrix.arch }}
path: dist/aMule-*-Windows-${{ matrix.arch }}.zip
if-no-files-found: error
retention-days: 3