Skip to content

Heal duplicate config paths and clean startup logs #3507

Heal duplicate config paths and clean startup logs

Heal duplicate config paths and clean startup logs #3507

Workflow file for this run

name: C/C++ CI
on:
workflow_dispatch:
push:
branches: [master]
tags:
- 'v*'
- 'preview-v*'
paths-ignore:
- '**.md'
- 'wiki/**'
- '.github/ISSUE_TEMPLATE/**'
- '.github/FUNDING.yml'
- '.github/CODE_OF_CONDUCT.md'
- '.github/CONTRIBUTING.md'
pull_request:
paths-ignore:
- '**.md'
- 'wiki/**'
# Cancel older in-progress runs for the same branch/PR ref.
# This reduces wasted CI minutes when multiple pushes happen quickly.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# Set least-privilege defaults for all jobs
permissions:
contents: read
jobs:
build-macOS:
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
permissions:
contents: read
actions: write
strategy:
fail-fast: false
matrix:
include:
- name: x86_64
runner: macos-15-intel
arch: intel
artifact_name: amiberry-macOS-x86_64-intermediate
- name: Apple-Silicon
runner: macos-15
arch: apple-silicon
artifact_name: amiberry-macOS-arm64-intermediate
steps:
- name: Check out repository code
uses: actions/checkout@v5
with:
submodules: recursive
- name: Cache Homebrew
if: matrix.arch == 'intel'
uses: actions/cache@v5
with:
path: /usr/local/Homebrew
key: ${{ runner.os }}-brew-${{ hashFiles('Brewfile') }}
restore-keys: |
${{ runner.os }}-brew-
- name: Fix homebrew in Github Runner
if: matrix.arch == 'intel'
run: |
for f in $(find /usr/local/bin -type l -print); do \
(readlink $f | grep -q -s "/Library") && echo Removing "$f" && rm -f "$f"; \
done || exit 0
- name: Pre-install gobject-introspection from source
if: matrix.arch == 'intel'
run: brew install --build-from-source gobject-introspection
- name: Install dependencies (Homebrew)
run: |
brew update
brew bundle
- name: Build for macOS (${{ matrix.name }})
run: |
cmake -B build && cmake --build build -j$(sysctl -n hw.ncpu)
- name: Archive intermediate app bundle
run: |
tar -C build -czf Amiberry.app.tar.gz Amiberry.app
- name: Upload intermediate app bundle
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: Amiberry.app.tar.gz
retention-days: 1
merge-macOS-universal:
needs: [build-macOS]
runs-on: macos-15
timeout-minutes: 30
permissions:
contents: read
actions: write
steps:
- name: Check out repository code
uses: actions/checkout@v5
- name: Download x86_64 app bundle
uses: actions/download-artifact@v7
with:
name: amiberry-macOS-x86_64-intermediate
path: x86_64-app
- name: Download arm64 app bundle
uses: actions/download-artifact@v7
with:
name: amiberry-macOS-arm64-intermediate
path: arm64-app
- name: Extract downloaded app bundles
run: |
tar -xzf x86_64-app/Amiberry.app.tar.gz -C x86_64-app
tar -xzf arm64-app/Amiberry.app.tar.gz -C arm64-app
- name: Create Universal binary with lipo
run: |
# macOS ships bash 3.2 (GPLv2); install bash 5 for associative arrays
if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then
brew install bash
exec "$(brew --prefix)/bin/bash" --noprofile --norc -euo pipefail "$0" "$@"
fi
set -euo pipefail
mkdir -p universal-app/Amiberry.app
# Start with the arm64 app as the base (preserves bundle structure)
cp -R arm64-app/Amiberry.app/ universal-app/Amiberry.app/
# Merge the main executable
lipo -create \
x86_64-app/Amiberry.app/Contents/MacOS/Amiberry \
arm64-app/Amiberry.app/Contents/MacOS/Amiberry \
-output universal-app/Amiberry.app/Contents/MacOS/Amiberry
chmod +x universal-app/Amiberry.app/Contents/MacOS/Amiberry
# Helper: merge a single binary with lipo, skipping if already universal
merge_binary() {
local x86_file="$1" arm_file="$2" output="$3"
if [ ! -f "$x86_file" ] || [ ! -f "$arm_file" ]; then
return
fi
# If a file is already universal (e.g. capsimage builds fat), just copy it
local x86_archs arm_archs
x86_archs=$(lipo -info "$x86_file" 2>/dev/null | sed 's/.*: //')
arm_archs=$(lipo -info "$arm_file" 2>/dev/null | sed 's/.*: //')
if echo "$x86_archs" | grep -q "x86_64" && echo "$x86_archs" | grep -q "arm64"; then
echo " Already universal: $(basename "$output") — copying as-is"
cp "$x86_file" "$output"
elif [ "$x86_archs" = "$arm_archs" ]; then
echo " WARNING: same arch in both — copying x86 version: $(basename "$output")"
cp "$x86_file" "$output"
else
lipo -create "$x86_file" "$arm_file" -output "$output"
fi
}
# Helper: extract library stem from a versioned dylib filename
# For libs with -N in the name (e.g. libglib-2.0), strip only the last version component
# libglib-2.0.0.dylib → libglib-2.0
# libgobject-2.0.0.dylib → libgobject-2.0
# For all other libs, strip the entire trailing version
# libavif.16.4.0.dylib → libavif
# libSDL3.0.6.0.dylib → libSDL3
# libpng16.16.dylib → libpng16
lib_stem() {
local name="${1%.dylib}"
if [[ "$name" =~ -[0-9] ]] && [[ "$name" =~ \.[0-9]+([.][0-9]+)*$ ]]; then
echo "${name%.*}"
else
echo "$name" | sed -E 's/\.[0-9]+(\.[0-9]+)*$//'
fi
}
X86_FW="x86_64-app/Amiberry.app/Contents/Frameworks"
ARM_FW="arm64-app/Amiberry.app/Contents/Frameworks"
UNI_FW="universal-app/Amiberry.app/Contents/Frameworks"
# Build stem → filename maps for both architectures
declare -A x86_stems arm_stems
if [ -d "$X86_FW" ]; then
for f in "$X86_FW"/*.dylib; do
[ -f "$f" ] || continue
name=$(basename "$f")
stem=$(lib_stem "$name")
x86_stems["$stem"]="$name"
done
fi
if [ -d "$ARM_FW" ]; then
for f in "$ARM_FW"/*.dylib; do
[ -f "$f" ] || continue
name=$(basename "$f")
stem=$(lib_stem "$name")
arm_stems["$stem"]="$name"
done
fi
# Parity check: warn about mismatches between architectures
echo "=== Checking dylib parity between x86_64 and arm64 builds ==="
had_mismatch=0
for stem in "${!arm_stems[@]}"; do
arm_name="${arm_stems[$stem]}"
x86_name="${x86_stems[$stem]:-}"
if [ -z "$x86_name" ]; then
echo " WARNING: $arm_name exists only in arm64 build (no x86_64 equivalent)"
had_mismatch=1
elif [ "$arm_name" != "$x86_name" ]; then
echo " WARNING: version mismatch for $stem: x86_64=$x86_name vs arm64=$arm_name"
had_mismatch=1
fi
done
for stem in "${!x86_stems[@]}"; do
if [ -z "${arm_stems[$stem]:-}" ]; then
echo " WARNING: ${x86_stems[$stem]} exists only in x86_64 build (no arm64 equivalent)"
had_mismatch=1
fi
done
if [ "$had_mismatch" = "0" ]; then
echo " All dylibs match between architectures"
fi
# Merge Frameworks dylibs using stem-aware matching
echo "=== Merging Frameworks dylibs ==="
declare -A merged_stems
# Collect all unique stems from both architectures
all_stems=()
for stem in "${!arm_stems[@]}"; do all_stems+=("$stem"); done
for stem in "${!x86_stems[@]}"; do
if [ -z "${arm_stems[$stem]:-}" ]; then
all_stems+=("$stem")
fi
done
for stem in "${all_stems[@]}"; do
[ -n "${merged_stems[$stem]:-}" ] && continue
merged_stems["$stem"]=1
x86_name="${x86_stems[$stem]:-}"
arm_name="${arm_stems[$stem]:-}"
if [ -n "$x86_name" ] && [ -n "$arm_name" ]; then
if [ "$x86_name" = "$arm_name" ]; then
# Same filename — standard merge
echo " Merging: $arm_name"
merge_binary "$X86_FW/$x86_name" "$ARM_FW/$arm_name" "$UNI_FW/$arm_name"
else
# Version mismatch — merge by stem, provide both names
echo " Merging with version mismatch: x86_64=$x86_name + arm64=$arm_name"
merge_binary "$X86_FW/$x86_name" "$ARM_FW/$arm_name" "$UNI_FW/$arm_name"
ln -sf "$arm_name" "$UNI_FW/$x86_name"
echo " → Universal dylib: $arm_name, symlink: $x86_name → $arm_name"
fi
elif [ -n "$arm_name" ]; then
echo " Keeping arm64-only: $arm_name (no x86_64 equivalent)"
elif [ -n "$x86_name" ]; then
echo " Copying x86_64-only: $x86_name (not in arm64 base)"
cp "$X86_FW/$x86_name" "$UNI_FW/$x86_name"
fi
done
# Merge all dylibs in plugins (arm64 base + any x86-only plugins)
UNI_PLUGINS="universal-app/Amiberry.app/Contents/Resources/plugins"
X86_PLUGINS="x86_64-app/Amiberry.app/Contents/Resources/plugins"
ARM_PLUGINS="arm64-app/Amiberry.app/Contents/Resources/plugins"
if [ -d "$UNI_PLUGINS" ]; then
for dylib in "$UNI_PLUGINS"/*.dylib; do
[ -f "$dylib" ] || continue
bname=$(basename "$dylib")
merge_binary "$X86_PLUGINS/$bname" "$ARM_PLUGINS/$bname" "$dylib"
done
fi
if [ -d "$X86_PLUGINS" ]; then
for dylib in "$X86_PLUGINS"/*.dylib; do
[ -f "$dylib" ] || continue
bname=$(basename "$dylib")
if [ ! -f "$UNI_PLUGINS/$bname" ]; then
echo " Copying x86_64-only plugin: $bname"
mkdir -p "$UNI_PLUGINS"
cp "$dylib" "$UNI_PLUGINS/$bname"
fi
done
fi
# Verify the main binary is universal
echo "=== Universal binary info ==="
lipo -info universal-app/Amiberry.app/Contents/MacOS/Amiberry
file universal-app/Amiberry.app/Contents/MacOS/Amiberry
# Verify all Frameworks dylibs are universal
echo "=== Verifying Frameworks dylibs ==="
non_universal=0
for dylib in "$UNI_FW"/*.dylib; do
[ -f "$dylib" ] || continue
# Skip symlinks — they point to an already-checked universal dylib
[ -L "$dylib" ] && continue
archs=$(lipo -info "$dylib" 2>/dev/null | sed 's/.*: //')
name=$(basename "$dylib")
if echo "$archs" | grep -q "x86_64" && echo "$archs" | grep -q "arm64"; then
echo " OK: $name ($archs)"
else
echo " WARNING: $name is NOT universal ($archs)"
non_universal=1
fi
done
if [ "$non_universal" = "1" ]; then
echo "::warning::Some Frameworks dylibs are not universal — the app may crash on one architecture"
fi
echo "=== Verifying dylib dependencies resolve ==="
missing_deps=0
MAIN_BIN="universal-app/Amiberry.app/Contents/MacOS/Amiberry"
for arch in x86_64 arm64; do
while read -r line; do
dep_name=$(echo "$line" | sed -E 's|.*@executable_path/../Frameworks/([^ ]+).*|\1|')
if [ ! -f "$UNI_FW/$dep_name" ]; then
echo " MISSING ($arch): $dep_name"
missing_deps=1
fi
done < <(otool -arch "$arch" -L "$MAIN_BIN" 2>/dev/null | grep '@executable_path/../Frameworks/' || true)
done
if [ "$missing_deps" = "1" ]; then
echo "::error::Some @executable_path dylib references cannot be resolved — the app will crash on load"
exit 1
fi
- name: Install the Apple certificate
env:
APPLE_DEVID_CERT_BASE64: ${{ secrets.APPLE_DEVID_CERT_BASE64 }}
APPLE_DEVID_CERT_P12_PASSWORD: ${{ secrets.APPLE_DEVID_CERT_P12_PASSWORD }}
APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
run: |
set +x # Mask output for security
CERTIFICATE_PATH=$RUNNER_TEMP/apple_devid_cert.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$APPLE_DEVID_CERT_BASE64" | base64 --decode -o $CERTIFICATE_PATH
security create-keychain -p $APPLE_KEYCHAIN_PASSWORD $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p $APPLE_KEYCHAIN_PASSWORD $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P $APPLE_DEVID_CERT_P12_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -s -k $APPLE_KEYCHAIN_PASSWORD $KEYCHAIN_PATH
security list-keychains -d user -s $KEYCHAIN_PATH
- name: Codesign the dylibs
env:
CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
run: |
for file in universal-app/Amiberry.app/Contents/Frameworks/*.dylib; do
[ -f "$file" ] || continue
[ -L "$file" ] && continue
codesign -s "$CODESIGN_IDENTITY" -f -o runtime,hard "$file"
done
for file in universal-app/Amiberry.app/Contents/Resources/plugins/*.dylib; do
[ -f "$file" ] || continue
[ -L "$file" ] && continue
codesign -s "$CODESIGN_IDENTITY" -f -o runtime,hard "$file"
done
- name: Codesign the app
env:
CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
run: |
codesign -s "$CODESIGN_IDENTITY" -f -o runtime,hard --entitlements packaging/macos/Amiberry.entitlements universal-app/Amiberry.app
- name: Verify app entitlements and signature
run: |
codesign -d --entitlements :- universal-app/Amiberry.app
codesign --verify --deep --strict universal-app/Amiberry.app
echo "Signature verification passed"
- name: Create a zip to send to the notary service
run: |
ditto -c -k --keepParent universal-app/Amiberry.app Amiberry-${{ github.sha }}-macOS-universal.zip
- name: Send the file to be notarized by Apple
run: |
set -uo pipefail
ZIP="Amiberry-${{ github.sha }}-macOS-universal.zip"
# Stream notarytool output to both the log and a file, without
# letting `set -e` kill the script before we can echo the rejection.
xcrun notarytool submit "$ZIP" --wait \
--apple-id "midwan@gmail.com" \
--password ${{ secrets.APPLE_NOTARY_APP_PASSWORD }} \
--team-id ${{ secrets.APPLE_TEAM_ID }} 2>&1 | tee notary.log
STATUS=${PIPESTATUS[0]}
SUBMISSION_ID=$(awk '/id:/ {print $2; exit}' notary.log)
if [ "$STATUS" -eq 0 ] && grep -q 'status: Accepted' notary.log; then
echo "Notarization accepted."
else
echo "::error::Notarization failed (notarytool exit=$STATUS) — fetching rejection log"
if [ -n "$SUBMISSION_ID" ]; then
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "midwan@gmail.com" \
--password ${{ secrets.APPLE_NOTARY_APP_PASSWORD }} \
--team-id ${{ secrets.APPLE_TEAM_ID }} || true
fi
exit 1
fi
- name: Staple the notary receipt to the application bundle
run: |
xcrun stapler staple universal-app/Amiberry.app
rm Amiberry-${{ github.sha }}-macOS-universal.zip
- name: Create DMG package
run: |
set -euo pipefail
# Read version from CMakeLists.txt (BSD-compatible, no grep -P)
VERSION=$(sed -n 's/.*VERSION \([0-9]*\.[0-9]*\.[0-9]*\).*/\1/p' CMakeLists.txt | head -1)
PRE_RELEASE=$(sed -n 's/.*VERSION_PRE_RELEASE "\([0-9]*\)".*/\1/p' CMakeLists.txt | head -1)
if [ -n "$PRE_RELEASE" ]; then
DMG_VERSION="${VERSION}~pre${PRE_RELEASE}"
else
DMG_VERSION="${VERSION}"
fi
DMG_NAME="Amiberry-${DMG_VERSION}-macOS-universal"
# Create a staging directory for DMG contents
STAGING="dmg-staging"
rm -rf "$STAGING"
mkdir -p "$STAGING"
cp -R universal-app/Amiberry.app "$STAGING/"
ln -s /Applications "$STAGING/Applications"
# Copy background image
mkdir -p "$STAGING/.background"
cp packaging/dmg/AppDMGBackground.tiff "$STAGING/.background/background.tiff"
# Create a writable DMG, apply Finder layout/background, then compress.
RW_DMG="${DMG_NAME}-rw.dmg"
hdiutil create -volname "Amiberry" \
-srcfolder "$STAGING" \
-ov -format UDRW \
"$RW_DMG"
DEVICE=""
cleanup() {
if [ -n "$DEVICE" ]; then
hdiutil detach "$DEVICE" || true
fi
}
trap cleanup EXIT
DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "$RW_DMG" | awk '/\/Volumes\/Amiberry/ {print $1; exit}')
if [ -z "$DEVICE" ]; then
echo "Failed to attach DMG."
exit 1
fi
osascript packaging/dmg/AppDMGSetup.scpt "Amiberry"
hdiutil detach "$DEVICE"
DEVICE=""
trap - EXIT
hdiutil convert "$RW_DMG" -format UDZO -imagekey zlib-level=9 -ov -o "${DMG_NAME}.dmg"
rm -f "$RW_DMG"
rm -rf "$STAGING"
echo "DMG_NAME=${DMG_NAME}" >> "$GITHUB_ENV"
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: amiberry-macOS-universal
path: Amiberry-*.dmg
retention-days: 7
build-ubuntu:
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container }}
timeout-minutes: 30
permissions:
contents: read
actions: write
strategy:
fail-fast: false
matrix:
include:
- name: 22.04-amd64
runner: ubuntu-latest
container: ubuntu:22.04
artifact_name: amiberry-ubuntu-22.04-amd64
sdl_source: setup-sdl
codename: jammy
extra_cmake_args: -DBUNDLE_SDL=ON
- name: 24.04-amd64
runner: ubuntu-latest
container: ubuntu:24.04
artifact_name: amiberry-ubuntu-24.04-amd64
sdl_source: setup-sdl
codename: noble
extra_cmake_args: -DBUNDLE_SDL=ON
- name: 25.10-amd64
runner: ubuntu-latest
container: ubuntu:25.10
artifact_name: amiberry-ubuntu-25.10-amd64
sdl_source: setup-sdl
codename: plucky
extra_cmake_args:
- name: 25.10-arm64
runner: ubuntu-24.04-arm
container: ubuntu:25.10
artifact_name: amiberry-ubuntu-25.10-arm64
sdl_source: apt
codename: plucky
extra_cmake_args:
steps:
- name: Install prerequisites
run: |
apt-get update
apt-get install -y sudo git gh
- name: Check out repository code
uses: actions/checkout@v5
with:
submodules: recursive
- name: Install ccache
run: sudo apt-get install -y ccache
- name: Cache ccache
uses: actions/cache@v5
with:
path: ~/.ccache
key: ${{ runner.os }}-${{ matrix.name }}-ccache-${{ github.ref }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-${{ matrix.name }}-ccache-${{ github.ref }}-
${{ runner.os }}-${{ matrix.name }}-ccache-
- name: Cache apt packages
uses: actions/cache@v5
with:
path: /var/cache/apt/archives
key: ${{ runner.os }}-${{ matrix.name }}-apt-${{ hashFiles('**/CMakeLists.txt') }}
restore-keys: |
${{ runner.os }}-${{ matrix.name }}-apt-
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build file zip libflac-dev libmpeg2-4-dev libpng-dev libasound2-dev libmpg123-dev libportmidi-dev libserialport-dev libenet-dev libpcap-dev libzstd-dev libcurl4-openssl-dev nlohmann-json3-dev libdbus-1-dev
- name: Install SDL3 and SDL3_image
if: matrix.sdl_source == 'setup-sdl'
id: sdl
uses: libsdl-org/setup-sdl@main
with:
install-linux-dependencies: true
version: 3.2.8
version-sdl-image: 3.2.4
pre-release: false
- name: Install SDL3 and SDL3_image from apt
if: matrix.sdl_source == 'apt'
run: sudo apt-get install -y libsdl3-dev libsdl3-image-dev
- name: make for Ubuntu ${{ matrix.name }} with setup-sdl
if: matrix.sdl_source == 'setup-sdl'
env:
CCACHE_DIR: ~/.ccache
run: |
export PATH="/usr/lib/ccache:$PATH"
cmake -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_PREFIX_PATH="${{ steps.sdl.outputs.prefix }}" -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DDISTRO_CODENAME=${{ matrix.codename }} ${{ matrix.extra_cmake_args }} && cmake --build build -j$(nproc)
cpack -G DEB --config build/CPackConfig.cmake
- name: make for Ubuntu ${{ matrix.name }} with apt SDL3
if: matrix.sdl_source == 'apt'
env:
CCACHE_DIR: ~/.ccache
run: |
export PATH="/usr/lib/ccache:$PATH"
cmake -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DDISTRO_CODENAME=${{ matrix.codename }} && cmake --build build -j$(nproc)
cpack -G DEB --config build/CPackConfig.cmake
- name: Upload artifact
if: always() && github.ref_type != 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: amiberry_*.deb
retention-days: 7
- name: ZIP package for release
if: github.ref_type == 'tag'
run: zip -r ${{ matrix.artifact_name }}.zip amiberry_*.deb
- name: Upload artifact for release
if: always() && github.ref_type == 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: amiberry-*.zip
build-fedora:
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
permissions:
contents: read
actions: write
strategy:
fail-fast: false
matrix:
include:
- name: x86_64
runner: ubuntu-latest
image: midwan/amiberry-fedora-x86_64:latest
artifact_name: amiberry-fedora-x86_64
release_suffix: fedora-x86_64
codename: fc41
- name: aarch64
runner: ubuntu-24.04-arm
image: midwan/amiberry-fedora-arm64:latest
artifact_name: amiberry-fedora-aarch64
release_suffix: fedora-aarch64
codename: fc41
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Run the build process with Docker
run: |
docker run --rm -v ${{ github.workspace }}:/build ${{ matrix.image }} \
bash -c "cmake -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DDISTRO_CODENAME=${{ matrix.codename }} && cmake --build build -j\$(nproc) && cpack -G RPM --config build/CPackConfig.cmake"
- name: Upload artifact
if: always() && github.ref_type != 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: amiberry-*.rpm
retention-days: 7
- name: ZIP package for release
if: github.ref_type == 'tag'
run: zip -r amiberry-${{ github.ref_name }}-${{ matrix.release_suffix }}.zip amiberry-*.rpm
- name: Upload artifact for release
if: always() && github.ref_type == 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: amiberry-*.zip
build-debian-amd64:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: write
strategy:
fail-fast: false
matrix:
include:
- name: bookworm
image: midwan/amiberry-debian-x86_64:bookworm
artifact_name: amiberry-debian-bookworm-amd64
release_name: debian-bookworm-amd64
codename: bookworm
extra_cmake_args: -DBUNDLE_SDL=ON
- name: trixie
image: midwan/amiberry-debian-x86_64:trixie
artifact_name: amiberry-debian-trixie-amd64
release_name: debian-trixie-amd64
codename: trixie
extra_cmake_args:
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Run the build process with Docker
run: |
docker run --rm -v ${{ github.workspace }}:/build ${{ matrix.image }} \
bash -c "cmake -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DDISTRO_CODENAME=${{ matrix.codename }} ${{ matrix.extra_cmake_args }} && cmake --build build -j\$(nproc) && cpack -G DEB --config build/CPackConfig.cmake"
- name: Upload artifact
if: always() && github.ref_type != 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: "amiberry_*.deb"
retention-days: 7
- name: ZIP package for release
if: github.ref_type == 'tag'
run: zip -r "amiberry-${{ github.ref_name }}-${{ matrix.release_name }}.zip" amiberry_*.deb
- name: Upload artifact for release
if: always() && github.ref_type == 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: "amiberry-*.zip"
build-debian-arm64:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: write
strategy:
fail-fast: false
matrix:
include:
- name: bookworm
image: midwan/amiberry-debian-aarch64:bookworm
artifact_name: amiberry-debian-bookworm-arm64
release_name: debian-bookworm-arm64
codename: bookworm
extra_cmake_args: -DBUNDLE_SDL=ON
- name: trixie
image: midwan/amiberry-debian-aarch64:trixie
artifact_name: amiberry-debian-trixie-arm64
release_name: debian-trixie-arm64
codename: trixie
extra_cmake_args:
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Run the build process with Docker
run: |
docker run --rm -v ${{ github.workspace }}:/build ${{ matrix.image }} \
bash -c "cmake -DCMAKE_TOOLCHAIN_FILE=cmake/Toolchain-aarch64-linux-gnu.cmake -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr -DDISTRO_CODENAME=${{ matrix.codename }} ${{ matrix.extra_cmake_args }} && cmake --build build -j\$(nproc) && cpack -G DEB --config build/CPackConfig.cmake"
- name: Upload artifact
if: always() && github.ref_type != 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: "amiberry_*.deb"
retention-days: 7
- name: ZIP package for release
if: github.ref_type == 'tag'
run: zip -r amiberry-${{ github.ref_name }}-${{ matrix.release_name }}.zip amiberry_*.deb
- name: Upload artifact for release
if: always() && github.ref_type == 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: "amiberry-*.zip"
# Windows x64: native build on windows-latest using llvm-mingw (clang).
# Shares the toolchain with build-windows-arm64 so Windows on Arm and x64
# use a single compiler stack. Replaced the previous MSYS2/MinGW-w64 GCC
# build once the llvm-mingw shakeout was stable.
build-windows:
runs-on: windows-latest
# Cold-cache runs (vcpkg + llvm-mingw downloads) finish in ~24 min,
# warm-cache runs in ~10-15 min — see build-windows-arm64 for comparison.
# 60 min gives ~2.5x headroom over the cold-cache observation, plenty for
# SignPath round-trips and any transient slowness.
timeout-minutes: 60
permissions:
contents: read
actions: write
strategy:
fail-fast: false
matrix:
include:
- name: x64
artifact_name: amiberry-windows-x64
release_suffix: windows-x64
env:
LLVM_MINGW_TAG: "20260421"
LLVM_MINGW_FLAVOR: "ucrt-x86_64"
VCPKG_DEFAULT_TRIPLET: x64-mingw-dynamic
VCPKG_DEFAULT_HOST_TRIPLET: x64-mingw-dynamic
steps:
- name: Check out repository code
uses: actions/checkout@v5
with:
submodules: recursive
- name: Cache llvm-mingw toolchain
id: cache-llvm-mingw
uses: actions/cache@v5
with:
path: C:/llvm-mingw
key: llvm-mingw-${{ env.LLVM_MINGW_TAG }}-${{ env.LLVM_MINGW_FLAVOR }}
- name: Install llvm-mingw
if: steps.cache-llvm-mingw.outputs.cache-hit != 'true'
shell: pwsh
run: |
$tag = "${env:LLVM_MINGW_TAG}"
$flavor = "${env:LLVM_MINGW_FLAVOR}"
$base = "llvm-mingw-$tag-$flavor"
$url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$tag/$base.zip"
Write-Host "Downloading $url"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\llvm-mingw.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\llvm-mingw.zip" -DestinationPath "$env:RUNNER_TEMP\llvm-mingw"
New-Item -ItemType Directory -Force -Path "C:/llvm-mingw" | Out-Null
# The archive extracts into a single subdir named $base; flatten.
Move-Item "$env:RUNNER_TEMP\llvm-mingw\$base\*" "C:/llvm-mingw" -Force
- name: Expose llvm-mingw to subsequent steps
shell: pwsh
run: |
"LLVM_MINGW_ROOT=C:/llvm-mingw" | Out-File -FilePath $env:GITHUB_ENV -Append
"C:/llvm-mingw/bin" | Out-File -FilePath $env:GITHUB_PATH -Append
- name: Bootstrap vcpkg
shell: pwsh
run: |
if (-not (Test-Path "C:/vcpkg/.git")) {
git clone --depth 1 https://github.com/microsoft/vcpkg.git C:/vcpkg
}
& C:/vcpkg/bootstrap-vcpkg.bat -disableMetrics
"VCPKG_ROOT=C:/vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Cache vcpkg installed packages
uses: actions/cache@v5
with:
path: out/build/windows-release/vcpkg_installed
# Include LLVM_MINGW_TAG so a toolchain bump invalidates the
# cache: deps built with one CRT/runtime can be ABI-incompatible
# with binaries linked by the new toolchain (the original
# x64-mingw-dynamic + 20250910 cache showed up as
# "undefined symbol: clock_gettime64" link errors).
key: ${{ runner.os }}-x64-vcpkg-${{ env.LLVM_MINGW_TAG }}-${{ hashFiles('vcpkg.json', 'cmake/Toolchain-x86_64-w64-mingw32.cmake') }}
restore-keys: |
${{ runner.os }}-x64-vcpkg-${{ env.LLVM_MINGW_TAG }}-
- name: Configure
shell: bash
run: |
cmake --preset windows-release
- name: Build
shell: bash
run: |
cmake --build out/build/windows-release -j$NUMBER_OF_PROCESSORS
- name: Install (portable layout)
shell: bash
run: |
rm -rf out/install/windows-release
rm -rf out/package/windows-release
cmake --install out/build/windows-release --prefix out/install/windows-release
- name: Package (Portable ZIP)
shell: bash
run: |
PORTABLE_ROOT="out/install/windows-release"
test -f "$PORTABLE_ROOT/Amiberry.exe"
: > "$PORTABLE_ROOT/amiberry.portable"
if [ "${{ github.ref_type }}" = "tag" ]; then
ZIP_NAME="amiberry-${{ github.ref_name }}-${{ matrix.release_suffix }}-portable.zip"
else
ZIP_NAME="amiberry-${{ github.sha }}-${{ matrix.release_suffix }}-portable.zip"
fi
PACKAGE_ROOT="out/package/windows-release/${ZIP_NAME%.zip}"
mkdir -p "$(dirname "$PACKAGE_ROOT")"
cmake -E copy_directory "$PORTABLE_ROOT" "$PACKAGE_ROOT"
ZIP_PATH="$PWD/$ZIP_NAME"
(cd out/package/windows-release && cmake -E tar cf "$ZIP_PATH" --format=zip "${ZIP_NAME%.zip}")
- name: Verify Portable ZIP
shell: pwsh
run: |
$zip = Get-ChildItem "$PWD/amiberry-*-portable.zip" | Select-Object -First 1
if (-not $zip) {
throw "Portable ZIP not found."
}
$verifyRoot = Join-Path $PWD "portable-smoke"
Remove-Item -Recurse -Force $verifyRoot -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $verifyRoot | Out-Null
Expand-Archive -Path $zip.FullName -DestinationPath $verifyRoot
$portableDir = Get-ChildItem $verifyRoot -Directory | Select-Object -First 1
if (-not $portableDir) {
throw "Portable ZIP did not contain a top-level directory."
}
$exePath = Join-Path $portableDir.FullName "Amiberry.exe"
if (-not (Test-Path $exePath)) {
throw "Portable ZIP is missing Amiberry.exe."
}
$alternateWorkingDir = Join-Path $verifyRoot "alternate-cwd"
New-Item -ItemType Directory -Path $alternateWorkingDir | Out-Null
$stdoutFile = Join-Path $verifyRoot "dump-paths.stdout"
$stderrFile = Join-Path $verifyRoot "dump-paths.stderr"
$proc = Start-Process -FilePath $exePath -ArgumentList "--dump-paths" -WorkingDirectory $alternateWorkingDir -PassThru -Wait -NoNewWindow -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile
if ($proc.ExitCode -ne 0) {
Write-Host "STDOUT:"
if (Test-Path $stdoutFile) { Get-Content $stdoutFile }
Write-Host "STDERR:"
if (Test-Path $stderrFile) { Get-Content $stderrFile }
throw "--dump-paths failed with exit code $($proc.ExitCode)."
}
$lines = @{}
Get-Content $stdoutFile | ForEach-Object {
if ($_ -match '^(.*?)=(.*)$') {
$lines[$matches[1]] = $matches[2]
}
}
function Normalize-PortablePath([string]$path) {
if ([string]::IsNullOrWhiteSpace($path)) { return "" }
return $path.Replace('\', '/').TrimEnd('/').ToLowerInvariant()
}
$expectedRoot = Normalize-PortablePath $portableDir.FullName
$expectedMarker = "$expectedRoot/amiberry.portable"
$expectedConfig = "$expectedRoot/configurations"
$expectedSettings = "$expectedRoot/settings"
$expectedData = "$expectedRoot/data"
$expectedPlugins = "$expectedRoot/plugins"
$blockedRoots = @()
if ($env:LOCALAPPDATA) { $blockedRoots += (Normalize-PortablePath $env:LOCALAPPDATA) }
if ($env:USERPROFILE) { $blockedRoots += (Normalize-PortablePath $env:USERPROFILE) }
function Assert-PortablePath([string]$key, [string]$expected) {
$actual = Normalize-PortablePath $lines[$key]
if ($actual -ne $expected) {
throw "$key mismatch: '$($lines[$key])' vs '$expected'."
}
}
function Assert-NotUnderBlockedRoots([string]$key) {
$actual = Normalize-PortablePath $lines[$key]
foreach ($blockedRoot in $blockedRoots) {
if (-not [string]::IsNullOrWhiteSpace($blockedRoot) -and $actual.StartsWith($blockedRoot)) {
throw "$key unexpectedly resolved under host profile path '$($lines[$key])'."
}
}
}
if ($lines["portable_mode"] -ne "1") {
throw "portable_mode expected 1, got '$($lines["portable_mode"])'."
}
Assert-PortablePath "portable_root" $expectedRoot
Assert-PortablePath "portable_marker_file" $expectedMarker
Assert-PortablePath "home_dir" $expectedRoot
Assert-PortablePath "config_path" $expectedConfig
Assert-PortablePath "settings_dir" $expectedSettings
Assert-PortablePath "data_dir" $expectedData
Assert-PortablePath "plugins_dir" $expectedPlugins
Assert-NotUnderBlockedRoots "portable_root"
Assert-NotUnderBlockedRoots "portable_marker_file"
Assert-NotUnderBlockedRoots "home_dir"
Assert-NotUnderBlockedRoots "config_path"
Assert-NotUnderBlockedRoots "settings_dir"
Assert-NotUnderBlockedRoots "data_dir"
Assert-NotUnderBlockedRoots "plugins_dir"
if (Test-Path (Join-Path $portableDir.FullName "Settings")) {
throw "--dump-paths should not create Settings in the portable tree."
}
if (Test-Path (Join-Path $portableDir.FullName "Configurations")) {
throw "--dump-paths should not create Configurations in the portable tree."
}
if (Test-Path (Join-Path $portableDir.FullName "conf")) {
throw "--dump-paths should not create conf in the portable tree."
}
# x86_64 builds include the JIT, so exercise --jit-selftest from the
# portable layout. Mirrors the build-windows-arm64 smoke test.
$jitStdoutFile = Join-Path $verifyRoot "jit-selftest.stdout"
$jitStderrFile = Join-Path $verifyRoot "jit-selftest.stderr"
$jitProc = Start-Process -FilePath $exePath -ArgumentList "--jit-selftest" `
-WorkingDirectory $portableDir.FullName -PassThru -Wait -NoNewWindow `
-RedirectStandardOutput $jitStdoutFile -RedirectStandardError $jitStderrFile
if ($jitProc.ExitCode -ne 0) {
Write-Host "JIT SELFTEST STDOUT:"; if (Test-Path $jitStdoutFile) { Get-Content $jitStdoutFile }
Write-Host "JIT SELFTEST STDERR:"; if (Test-Path $jitStderrFile) { Get-Content $jitStderrFile }
throw "--jit-selftest failed with exit code $($jitProc.ExitCode)."
}
Write-Host "x64 (llvm-mingw) Amiberry.exe ran --jit-selftest successfully."
- name: Package (InnoSetup)
shell: pwsh
run: |
cpack -G INNOSETUP --config out/build/windows-release/CPackConfig.cmake
- name: Detect SignPath configuration
id: signpath-config
shell: pwsh
env:
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }}
SIGNPATH_SIGNING_POLICY_SLUG: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
SIGNPATH_ARTIFACT_CONFIGURATION_SLUG: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
run: |
if ($env:GITHUB_EVENT_NAME -eq "pull_request") {
"enabled=false" >> $env:GITHUB_OUTPUT
Write-Host "SignPath disabled for pull_request events."
exit 0
}
$required = @{
SIGNPATH_API_TOKEN = $env:SIGNPATH_API_TOKEN
SIGNPATH_ORGANIZATION_ID = $env:SIGNPATH_ORGANIZATION_ID
SIGNPATH_PROJECT_SLUG = $env:SIGNPATH_PROJECT_SLUG
SIGNPATH_SIGNING_POLICY_SLUG = $env:SIGNPATH_SIGNING_POLICY_SLUG
SIGNPATH_ARTIFACT_CONFIGURATION_SLUG = $env:SIGNPATH_ARTIFACT_CONFIGURATION_SLUG
}
$missing = @(
$required.GetEnumerator() |
Where-Object { [string]::IsNullOrWhiteSpace($_.Value) } |
ForEach-Object { $_.Key } |
Sort-Object
)
if ($missing.Count -gt 0) {
"enabled=false" >> $env:GITHUB_OUTPUT
Write-Host "SignPath disabled because the following settings are missing: $($missing -join ', ')"
exit 0
}
"enabled=true" >> $env:GITHUB_OUTPUT
Write-Host "SignPath enabled for this run."
- name: Upload unsigned Windows artifacts for SignPath
if: steps.signpath-config.outputs.enabled == 'true'
id: signpath-unsigned
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}-signpath-unsigned
path: |
amiberry-*.zip
amiberry-*.exe
if-no-files-found: error
retention-days: 1
compression-level: 0
- name: Submit SignPath signing request
if: steps.signpath-config.outputs.enabled == 'true'
id: signpath
uses: SignPath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
github-artifact-id: ${{ steps.signpath-unsigned.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 1200
output-artifact-directory: signpath-signed
- name: Replace unsigned artifacts with signed outputs
if: steps.signpath-config.outputs.enabled == 'true'
shell: pwsh
run: |
$downloadDir = Join-Path $PWD "signpath-signed"
if (-not (Test-Path $downloadDir)) {
throw "Signed artifact directory '$downloadDir' was not created."
}
$portableZip = Get-ChildItem $downloadDir -Recurse -File -Filter "amiberry-*-portable.zip" | Select-Object -First 1
if (-not $portableZip) {
throw "Signed portable ZIP was not found in '$downloadDir'."
}
$installer = Get-ChildItem $downloadDir -Recurse -File -Filter "amiberry-*.exe" | Select-Object -First 1
if (-not $installer) {
throw "Signed installer EXE was not found in '$downloadDir'."
}
Copy-Item $portableZip.FullName $PWD -Force
Copy-Item $installer.FullName $PWD -Force
Write-Host "Replaced workspace artifacts with SignPath-signed outputs."
- name: Verify SignPath-signed artifacts
if: steps.signpath-config.outputs.enabled == 'true'
shell: pwsh
run: |
$installer = Get-ChildItem "$PWD/amiberry-*.exe" | Select-Object -First 1
if (-not $installer) {
throw "Signed installer EXE not found in workspace."
}
$installerSignature = Get-AuthenticodeSignature $installer.FullName
if ($installerSignature.Status -eq "NotSigned" -or -not $installerSignature.SignerCertificate) {
throw "Installer EXE is missing an Authenticode signature."
}
Write-Host "Installer signature status: $($installerSignature.Status)"
$portableZip = Get-ChildItem "$PWD/amiberry-*-portable.zip" | Select-Object -First 1
if (-not $portableZip) {
throw "Signed portable ZIP not found in workspace."
}
$verifyRoot = Join-Path $PWD "portable-signed-smoke"
Remove-Item -Recurse -Force $verifyRoot -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $verifyRoot | Out-Null
Expand-Archive -Path $portableZip.FullName -DestinationPath $verifyRoot
$portableExe = Get-ChildItem $verifyRoot -Recurse -File -Filter "Amiberry.exe" | Select-Object -First 1
if (-not $portableExe) {
throw "Portable ZIP is missing Amiberry.exe after SignPath signing."
}
$portableSignature = Get-AuthenticodeSignature $portableExe.FullName
if ($portableSignature.Status -eq "NotSigned" -or -not $portableSignature.SignerCertificate) {
throw "Portable ZIP Amiberry.exe is missing an Authenticode signature."
}
Write-Host "Portable EXE signature status: $($portableSignature.Status)"
- name: Upload artifact
if: github.ref_type != 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: |
amiberry-*.zip
amiberry-*.exe
retention-days: 7
- name: Upload artifact for release
if: github.ref_type == 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: |
amiberry-*.zip
amiberry-*.exe
retention-days: 7
# Windows ARM64: native build on the GitHub-hosted windows-11-arm
# runner using the same llvm-mingw toolchain as build-windows. Kept
# as a separate job because it cross-runs on a different runner image
# (windows-11-arm) and needs to install InnoSetup at runtime, but the
# toolchain, presets, and packaging are otherwise the same. This job
# is mandatory and a failure blocks the workflow (and downstream
# create-release).
build-windows-arm64:
runs-on: windows-11-arm
# Cold-cache runs finish in ~24 min, warm-cache runs in ~8 min. The
# extra time vs. x64 mostly comes from the InnoSetup-via-Chocolatey
# install path (windows-11-arm doesn't preinstall ISCC). 60 min keeps
# us in line with build-windows.
timeout-minutes: 60
permissions:
contents: read
actions: write
strategy:
fail-fast: false
matrix:
include:
- name: arm64
artifact_name: amiberry-windows-arm64
release_suffix: windows-arm64
env:
LLVM_MINGW_TAG: "20260421"
LLVM_MINGW_FLAVOR: "ucrt-aarch64"
VCPKG_DEFAULT_TRIPLET: arm64-mingw-dynamic
VCPKG_DEFAULT_HOST_TRIPLET: arm64-mingw-dynamic
steps:
- name: Check out repository code
uses: actions/checkout@v5
with:
submodules: recursive
- name: Cache llvm-mingw toolchain
id: cache-llvm-mingw
uses: actions/cache@v5
with:
path: C:/llvm-mingw
key: llvm-mingw-${{ env.LLVM_MINGW_TAG }}-${{ env.LLVM_MINGW_FLAVOR }}
- name: Install llvm-mingw
if: steps.cache-llvm-mingw.outputs.cache-hit != 'true'
shell: pwsh
run: |
$tag = "${env:LLVM_MINGW_TAG}"
$flavor = "${env:LLVM_MINGW_FLAVOR}"
$base = "llvm-mingw-$tag-$flavor"
$url = "https://github.com/mstorsjo/llvm-mingw/releases/download/$tag/$base.zip"
Write-Host "Downloading $url"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\llvm-mingw.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\llvm-mingw.zip" -DestinationPath "$env:RUNNER_TEMP\llvm-mingw"
New-Item -ItemType Directory -Force -Path "C:/llvm-mingw" | Out-Null
# The archive extracts into a single subdir named $base; flatten.
Move-Item "$env:RUNNER_TEMP\llvm-mingw\$base\*" "C:/llvm-mingw" -Force
- name: Expose llvm-mingw to subsequent steps
shell: pwsh
run: |
"LLVM_MINGW_ROOT=C:/llvm-mingw" | Out-File -FilePath $env:GITHUB_ENV -Append
"C:/llvm-mingw/bin" | Out-File -FilePath $env:GITHUB_PATH -Append
- name: Bootstrap vcpkg
shell: pwsh
run: |
if (-not (Test-Path "C:/vcpkg/.git")) {
git clone --depth 1 https://github.com/microsoft/vcpkg.git C:/vcpkg
}
& C:/vcpkg/bootstrap-vcpkg.bat -disableMetrics
"VCPKG_ROOT=C:/vcpkg" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Cache vcpkg installed packages
uses: actions/cache@v5
with:
path: out/build/windows-arm64-release/vcpkg_installed
# Include LLVM_MINGW_TAG so a toolchain bump invalidates the
# cache (avoids ABI mismatches between deps built with one
# toolchain and binaries linked with another).
key: ${{ runner.os }}-arm64-vcpkg-${{ env.LLVM_MINGW_TAG }}-${{ hashFiles('vcpkg.json', 'cmake/Toolchain-aarch64-w64-mingw32.cmake') }}
restore-keys: |
${{ runner.os }}-arm64-vcpkg-${{ env.LLVM_MINGW_TAG }}-
- name: Configure
shell: bash
run: |
cmake --preset windows-arm64-release
- name: Build
shell: bash
run: |
cmake --build out/build/windows-arm64-release -j$NUMBER_OF_PROCESSORS
- name: Install (portable layout)
shell: bash
run: |
rm -rf out/install/windows-arm64-release
rm -rf out/package/windows-arm64-release
cmake --install out/build/windows-arm64-release --prefix out/install/windows-arm64-release
- name: Package (Portable ZIP)
shell: bash
run: |
PORTABLE_ROOT="out/install/windows-arm64-release"
test -f "$PORTABLE_ROOT/Amiberry.exe"
: > "$PORTABLE_ROOT/amiberry.portable"
if [ "${{ github.ref_type }}" = "tag" ]; then
ZIP_NAME="amiberry-${{ github.ref_name }}-windows-arm64-portable.zip"
else
ZIP_NAME="amiberry-${{ github.sha }}-windows-arm64-portable.zip"
fi
PACKAGE_ROOT="out/package/windows-arm64-release/${ZIP_NAME%.zip}"
mkdir -p "$(dirname "$PACKAGE_ROOT")"
cmake -E copy_directory "$PORTABLE_ROOT" "$PACKAGE_ROOT"
ZIP_PATH="$PWD/$ZIP_NAME"
(cd out/package/windows-arm64-release && cmake -E tar cf "$ZIP_PATH" --format=zip "${ZIP_NAME%.zip}")
- name: Smoke test (--dump-paths, --jit-selftest)
shell: pwsh
run: |
$zip = Get-ChildItem "$PWD/amiberry-*-windows-arm64-portable.zip" | Select-Object -First 1
if (-not $zip) { throw "Portable ZIP not found." }
$verifyRoot = Join-Path $PWD "portable-smoke"
Remove-Item -Recurse -Force $verifyRoot -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $verifyRoot | Out-Null
Expand-Archive -Path $zip.FullName -DestinationPath $verifyRoot
$portableDir = Get-ChildItem $verifyRoot -Directory | Select-Object -First 1
$exePath = Join-Path $portableDir.FullName "Amiberry.exe"
if (-not (Test-Path $exePath)) { throw "Amiberry.exe missing from portable ZIP." }
$stdoutFile = Join-Path $verifyRoot "dump-paths.stdout"
$stderrFile = Join-Path $verifyRoot "dump-paths.stderr"
$proc = Start-Process -FilePath $exePath -ArgumentList "--dump-paths" `
-WorkingDirectory $portableDir.FullName -PassThru -Wait -NoNewWindow `
-RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile
if ($proc.ExitCode -ne 0) {
Write-Host "STDOUT:"; if (Test-Path $stdoutFile) { Get-Content $stdoutFile }
Write-Host "STDERR:"; if (Test-Path $stderrFile) { Get-Content $stderrFile }
throw "--dump-paths failed with exit code $($proc.ExitCode)."
}
Write-Host "ARM64 Amiberry.exe ran --dump-paths successfully."
$jitStdoutFile = Join-Path $verifyRoot "jit-selftest.stdout"
$jitStderrFile = Join-Path $verifyRoot "jit-selftest.stderr"
$jitProc = Start-Process -FilePath $exePath -ArgumentList "--jit-selftest" `
-WorkingDirectory $portableDir.FullName -PassThru -Wait -NoNewWindow `
-RedirectStandardOutput $jitStdoutFile -RedirectStandardError $jitStderrFile
if ($jitProc.ExitCode -ne 0) {
Write-Host "JIT SELFTEST STDOUT:"; if (Test-Path $jitStdoutFile) { Get-Content $jitStdoutFile }
Write-Host "JIT SELFTEST STDERR:"; if (Test-Path $jitStderrFile) { Get-Content $jitStderrFile }
throw "--jit-selftest failed with exit code $($jitProc.ExitCode)."
}
Write-Host "ARM64 Amiberry.exe ran --jit-selftest successfully."
- name: Ensure InnoSetup is available
shell: pwsh
run: |
# InnoSetup ships as an x86 binary; it runs under WoA x86 emulation
# on windows-11-arm runners. If ISCC is already on PATH (e.g. via
# a future preinstall), use it; otherwise install via Chocolatey.
$iscc = Get-Command iscc.exe -ErrorAction SilentlyContinue
if ($iscc) {
Write-Host "ISCC already available at $($iscc.Source)"
} else {
Write-Host "ISCC not found, installing InnoSetup via Chocolatey..."
# Chocolatey is preinstalled on all GitHub-hosted Windows runners.
choco install innosetup --no-progress --yes | Out-Null
# Chocolatey shims go to C:\ProgramData\chocolatey\bin which is
# already on PATH; verify ISCC is reachable.
$iscc = Get-Command iscc.exe -ErrorAction SilentlyContinue
if (-not $iscc) {
# Fallback: add the standard install dir explicitly.
$programFiles = ${env:ProgramFiles(x86)}
$fallback = Join-Path $programFiles "Inno Setup 6"
if (Test-Path $fallback) {
"$fallback" | Out-File -FilePath $env:GITHUB_PATH -Append
Write-Host "Added $fallback to PATH"
} else {
throw "InnoSetup install succeeded but ISCC not found on PATH or in $fallback."
}
}
}
- name: Package (InnoSetup)
shell: pwsh
run: |
cpack -G INNOSETUP --config out/build/windows-arm64-release/CPackConfig.cmake
- name: Detect SignPath configuration
id: signpath-config
shell: pwsh
env:
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }}
SIGNPATH_SIGNING_POLICY_SLUG: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
SIGNPATH_ARTIFACT_CONFIGURATION_SLUG: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
run: |
if ($env:GITHUB_EVENT_NAME -eq "pull_request") {
"enabled=false" >> $env:GITHUB_OUTPUT
Write-Host "SignPath disabled for pull_request events."
exit 0
}
$required = @{
SIGNPATH_API_TOKEN = $env:SIGNPATH_API_TOKEN
SIGNPATH_ORGANIZATION_ID = $env:SIGNPATH_ORGANIZATION_ID
SIGNPATH_PROJECT_SLUG = $env:SIGNPATH_PROJECT_SLUG
SIGNPATH_SIGNING_POLICY_SLUG = $env:SIGNPATH_SIGNING_POLICY_SLUG
SIGNPATH_ARTIFACT_CONFIGURATION_SLUG = $env:SIGNPATH_ARTIFACT_CONFIGURATION_SLUG
}
$missing = @(
$required.GetEnumerator() |
Where-Object { [string]::IsNullOrWhiteSpace($_.Value) } |
ForEach-Object { $_.Key } |
Sort-Object
)
if ($missing.Count -gt 0) {
"enabled=false" >> $env:GITHUB_OUTPUT
Write-Host "SignPath disabled because the following settings are missing: $($missing -join ', ')"
exit 0
}
"enabled=true" >> $env:GITHUB_OUTPUT
Write-Host "SignPath enabled for this run."
- name: Upload unsigned Windows artifacts for SignPath
if: steps.signpath-config.outputs.enabled == 'true'
id: signpath-unsigned
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}-signpath-unsigned
path: |
amiberry-*.zip
amiberry-*.exe
if-no-files-found: error
retention-days: 1
compression-level: 0
- name: Submit SignPath signing request
if: steps.signpath-config.outputs.enabled == 'true'
id: signpath
uses: SignPath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
github-artifact-id: ${{ steps.signpath-unsigned.outputs.artifact-id }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 1200
output-artifact-directory: signpath-signed
- name: Replace unsigned artifacts with signed outputs
if: steps.signpath-config.outputs.enabled == 'true'
shell: pwsh
run: |
$downloadDir = Join-Path $PWD "signpath-signed"
if (-not (Test-Path $downloadDir)) {
throw "Signed artifact directory '$downloadDir' was not created."
}
$portableZip = Get-ChildItem $downloadDir -Recurse -File -Filter "amiberry-*-portable.zip" | Select-Object -First 1
if (-not $portableZip) {
throw "Signed portable ZIP was not found in '$downloadDir'."
}
$installer = Get-ChildItem $downloadDir -Recurse -File -Filter "amiberry-*.exe" | Select-Object -First 1
if (-not $installer) {
throw "Signed installer EXE was not found in '$downloadDir'."
}
Copy-Item $portableZip.FullName $PWD -Force
Copy-Item $installer.FullName $PWD -Force
Write-Host "Replaced workspace artifacts with SignPath-signed outputs."
- name: Verify SignPath-signed artifacts
if: steps.signpath-config.outputs.enabled == 'true'
shell: pwsh
run: |
$installer = Get-ChildItem "$PWD/amiberry-*.exe" | Select-Object -First 1
if (-not $installer) {
throw "Signed installer EXE not found in workspace."
}
$installerSignature = Get-AuthenticodeSignature $installer.FullName
if ($installerSignature.Status -eq "NotSigned" -or -not $installerSignature.SignerCertificate) {
throw "Installer EXE is missing an Authenticode signature."
}
Write-Host "Installer EXE signature status: $($installerSignature.Status)"
$portableZip = Get-ChildItem "$PWD/amiberry-*-windows-arm64-portable.zip" | Select-Object -First 1
if (-not $portableZip) {
throw "Signed portable ZIP not found in workspace."
}
$verifyRoot = Join-Path $PWD "portable-signed-smoke"
Remove-Item -Recurse -Force $verifyRoot -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $verifyRoot | Out-Null
Expand-Archive -Path $portableZip.FullName -DestinationPath $verifyRoot
$portableExe = Get-ChildItem $verifyRoot -Recurse -File -Filter "Amiberry.exe" | Select-Object -First 1
if (-not $portableExe) {
throw "Portable ZIP is missing Amiberry.exe after SignPath signing."
}
$portableSignature = Get-AuthenticodeSignature $portableExe.FullName
if ($portableSignature.Status -eq "NotSigned" -or -not $portableSignature.SignerCertificate) {
throw "Portable ZIP Amiberry.exe is missing an Authenticode signature."
}
Write-Host "Portable EXE signature status: $($portableSignature.Status)"
- name: Upload artifact
if: github.ref_type != 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: |
amiberry-*.zip
amiberry-*.exe
retention-days: 7
- name: Upload artifact for release
if: github.ref_type == 'tag'
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.artifact_name }}
path: |
amiberry-*.zip
amiberry-*.exe
retention-days: 7
build-libretro:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: unix
artifact_platform: linux
arch: x86_64
artifact: amiberry_libretro.so
- os: ubuntu-latest
platform: unix
artifact_platform: linux
arch: aarch64
artifact: amiberry_libretro.so
- os: windows-latest
platform: win
artifact_platform: win
arch: x86_64
artifact: amiberry_libretro.dll
- os: macos-latest
platform: osx
artifact_platform: macOS
arch: arm64
artifact: amiberry_libretro.dylib
steps:
- name: Check out amiberry
uses: actions/checkout@v5
with:
path: amiberry
submodules: recursive
- name: Install dependencies (Windows)
if: runner.os == 'Windows'
uses: msys2/setup-msys2@v2
with:
# Use the CLANG64 subsystem so the libretro core is also built
# with llvm-mingw / clang, matching build-windows. The
# mingw-w64-clang-x86_64-toolchain package provides clang,
# clang++, lld, libc++, libunwind, libwinpthread.
msystem: CLANG64
update: false
install: mingw-w64-clang-x86_64-toolchain mingw-w64-clang-x86_64-zlib make
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential
if [ "${{ matrix.arch }}" = "aarch64" ]; then
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
ZLIB_PREFIX="${RUNNER_TEMP}/zlib-aarch64"
if sudo apt-get install -y zlib1g-dev-arm64-cross || sudo apt-get install -y libz-dev-arm64-cross; then
echo "ZLIB_CFLAGS=-I/usr/aarch64-linux-gnu/include" >> "$GITHUB_ENV"
echo "ZLIB_LIBS=-L/usr/aarch64-linux-gnu/lib -lz" >> "$GITHUB_ENV"
else
curl -fL https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz | tar xz -C "${RUNNER_TEMP}"
pushd "${RUNNER_TEMP}/zlib-1.3.2"
CC=aarch64-linux-gnu-gcc AR=aarch64-linux-gnu-ar RANLIB=aarch64-linux-gnu-ranlib \
./configure --static --prefix="${ZLIB_PREFIX}"
make -j$(nproc)
make install
popd
echo "ZLIB_CFLAGS=-I${ZLIB_PREFIX}/include" >> "$GITHUB_ENV"
echo "ZLIB_LIBS=${ZLIB_PREFIX}/lib/libz.a" >> "$GITHUB_ENV"
fi
fi
- name: Build Libretro Core (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
cd amiberry/libretro
make platform=${{ matrix.platform }} ARCH=${{ matrix.arch }} -j$(nproc)
- name: Build Libretro Core
if: runner.os != 'Windows'
shell: bash
run: |
cd amiberry/libretro
if [ "${{ matrix.arch }}" = "aarch64" ]; then
make platform=${{ matrix.platform }} ARCH=${{ matrix.arch }} CROSS_COMPILE=aarch64-linux-gnu- CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ ZLIB_CFLAGS="${ZLIB_CFLAGS}" ZLIB_LIBS="${ZLIB_LIBS}" -j$(nproc)
else
make platform=${{ matrix.platform }} ARCH=${{ matrix.arch }} -j$(nproc)
fi
- name: Prepare libretro release artifact
if: github.ref_type == 'tag'
shell: bash
run: |
artifact="${{ matrix.artifact }}"
ext="${artifact##*.}"
release_artifact="amiberry-libretro-${{ matrix.artifact_platform }}-${{ matrix.arch }}.${ext}"
cp "amiberry/libretro/${artifact}" "$release_artifact"
echo "LIBRETRO_RELEASE_ARTIFACT=$release_artifact" >> "$GITHUB_ENV"
- name: Upload artifact
if: github.ref_type != 'tag'
uses: actions/upload-artifact@v6
with:
name: amiberry-libretro-${{ matrix.artifact_platform }}-${{ matrix.arch }}
path: |
amiberry/libretro/${{ matrix.artifact }}
amiberry/libretro/amiberry_libretro.info
retention-days: 7
- name: Upload artifact for release
if: github.ref_type == 'tag'
uses: actions/upload-artifact@v6
with:
name: amiberry-libretro-${{ matrix.artifact_platform }}-${{ matrix.arch }}
path: ${{ env.LIBRETRO_RELEASE_ARTIFACT }}
- name: Upload libretro info artifact for release
if: github.ref_type == 'tag' && matrix.platform == 'unix' && matrix.arch == 'x86_64'
uses: actions/upload-artifact@v6
with:
name: amiberry-libretro-info
path: amiberry/libretro/amiberry_libretro.info
create-release:
needs: [merge-macOS-universal, build-fedora, build-ubuntu, build-debian-amd64, build-debian-arm64, build-windows, build-windows-arm64, build-libretro]
runs-on: ubuntu-latest
timeout-minutes: 15
if: github.ref_type == 'tag' && (startsWith(github.ref_name, 'v') || startsWith(github.ref_name, 'preview-v'))
# Elevated permissions only where needed for creating a release
permissions:
contents: write
actions: read
pull-requests: read
issues: read
steps:
- name: Download Build Artifacts
uses: actions/download-artifact@v7
with:
path: artifacts
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R artifacts
- name: Generate SHA256SUMS
run: |
cd artifacts
sha256sum amiberry-*.zip amiberry-*.exe amiberry-libretro-*.* amiberry_libretro.info Amiberry-*.dmg 2>/dev/null | tee SHA256SUMS
cat SHA256SUMS
- name: Create Release
uses: ncipollo/release-action@v1
with:
prerelease: ${{ startsWith(github.ref_name, 'preview-v') }}
allowUpdates: true
omitBodyDuringUpdate: true
generateReleaseNotes: true
artifacts: |
artifacts/amiberry-*.zip
artifacts/amiberry-libretro-*.*
artifacts/amiberry_libretro.info
artifacts/amiberry-*.exe
artifacts/Amiberry-*.dmg
artifacts/SHA256SUMS
update-repos:
needs: create-release
uses: ./.github/workflows/update-repos.yml
with:
tag_name: ${{ github.ref_name }}
secrets: inherit
upload-ppa:
needs: create-release
# Only upload stable releases (not preview-v* pre-releases)
if: ${{ !startsWith(github.ref_name, 'preview-v') }}
uses: ./.github/workflows/ppa-upload.yml
with:
tag_name: ${{ github.ref_name }}
secrets: inherit