App-Ariza.git | .github/workflows/ | runner-release.yml
# Build the compiled Windows launcher a bundle ships as bin/<exec>.exe,
# and publish it as a pinnable release artefact.
#
# The source is runner/ in this repository. It is built once per runner
# release rather than during `ariza bundle`, because compiling it at
# bundle time would put a C toolchain in the bundling path — on every
# machine, for every cross-build — to produce a file that is identical
# for every app. So: two lanes, MSYS2 UCRT64 for x86_64 and CLANGARM64
# for aarch64, the same toolchains that build the notcurses packs a
# bundle already carries.
#
# Triggers, and what each one is for:
#
# * pull_request touching runner/ — build both lanes and run the
# portable core's tests. Nothing is published.
# * workflow_dispatch — the same, on demand, while iterating on the
# recipe. Still nothing is published: "validate the build" and
# "overwrite the artefacts every ariza release verifies against" are
# one click apart, and a dispatch while resources/RUNNER_VERSION
# still names the live tag would clobber it.
# * push of a `runner-v*` tag — build, test, and publish, after
# checking the pushed tag against resources/RUNNER_VERSION. That
# file is the single source of truth for which tag a release
# produces, exactly as BINARY_TAG is in Notcurses-Native.
#
# After a release: copy the published checksums.txt into
# resources/runner-checksums.txt and commit it. Until that file lists an
# artefact, `ariza bundle` builds Windows bundles without the executable
# and says so; once it does, a failed download or a digest mismatch
# fails the build. App::Ariza::Runner documents that ladder.
name: runner-release
on:
workflow_dispatch:
push:
tags:
- 'runner-v*'
pull_request:
paths:
- 'runner/**'
- 'resources/RUNNER_VERSION'
- '.github/workflows/runner-release.yml'
permissions:
contents: read
jobs:
build:
name: runner-windows-${{ matrix.arch }}
runs-on: ${{ matrix.runs-on }}
# ARM Windows runners emulate the msys2 runtime, so everything that
# is not the compiler itself crawls. This is generous for a project
# of four C files and still fails a wedged runner in a useful
# timeframe rather than eating GitHub's 6h default.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runs-on: windows-2022
msystem: UCRT64
pkg-prefix: mingw-w64-ucrt-x86_64
# ctest runs on one lane. The suite is the portable core,
# which is architecture-independent by construction, and the
# x86_64 lane is the one that is not emulated.
run-tests: true
- arch: aarch64
runs-on: windows-11-arm
msystem: CLANGARM64
pkg-prefix: mingw-w64-clang-aarch64
run-tests: false
defaults:
run:
shell: 'msys2 {0}'
steps:
- uses: actions/checkout@v6
- uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
path-type: inherit
# Deliberately no `update: true`: that runs `pacman -Syuu`,
# which shifts installed package versions on nearly every run
# and invalidates the cache setup-msys2 keys on installed
# package state.
cache: true
install: >-
${{ matrix.pkg-prefix }}-toolchain
${{ matrix.pkg-prefix }}-cmake
${{ matrix.pkg-prefix }}-ninja
- name: Configure + build
run: |
set -euxo pipefail
# Paths stay POSIX: msys2-runtime converts them when a native
# program — cmake, ninja, the compiler — is spawned, and
# feeding those tools Windows-form paths breaks the other
# direction.
cmake -B build -S runner -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DARIZA_RUNNER_WERROR=ON \
-DARIZA_RUNNER_OUTPUT_NAME=ariza-runner-windows-${{ matrix.arch }}
cmake --build build
- name: Test the portable core
if: matrix.run-tests
run: ctest --test-dir build --output-on-failure
# Release gate: the executable a bundle carries must run on a
# machine that has never heard of MSYS2.
#
# -static is asked for in runner/CMakeLists.txt, but a compiler
# flag is a hope and the import table is the fact. An unnoticed
# libgcc_s_seh-1.dll here would work on this runner, work on every
# developer machine with a toolchain, and fail on a clean install
# — the same shape of bug as the vcruntime140.dll one that
# ariza's own PE audit exists to catch.
- name: Assert the runner is self-contained
run: |
set -euo pipefail
exe="build/ariza-runner-windows-${{ matrix.arch }}.exe"
if [[ ! -f "$exe" ]]; then
echo "::error::$exe was not produced"
ls -la build
exit 1
fi
# PE magic, so a truncated or misnamed file is caught here
# rather than by a user.
if [[ "$(head -c 2 "$exe")" != "MZ" ]]; then
echo "::error::$exe is not a PE executable"
exit 1
fi
fail=0
objdump -p "$exe" | awk '/DLL Name:/{print tolower($3)}' \
| sort -u > imports.txt
echo "--- imports ---"
cat imports.txt
while IFS= read -r dll; do
[[ -z "$dll" ]] && continue
case "$dll" in
# Windows' own: the same names App::Ariza::Native's PE
# audit treats as system DLLs, minus the Visual C++
# redistributable family (vcruntime*, msvcp*, concrt*,
# vcomp*) — which that audit refuses for the reason these
# lanes are UCRT64 and CLANGARM64 and not anything MSVC:
# it is not part of Windows and a clean install has none.
kernel32.dll|kernelbase.dll|ntdll.dll|user32.dll|\
advapi32.dll|shell32.dll|shlwapi.dll|ole32.dll|\
oleaut32.dll|rpcrt4.dll|version.dll|userenv.dll|\
ws2_32.dll|bcrypt.dll|crypt32.dll|secur32.dll|\
msvcrt.dll|ucrtbase.dll|api-ms-win-*|ext-ms-*)
echo "ok: $dll" ;;
*)
echo "::error::the runner imports $dll, which is not part of Windows"
fail=1 ;;
esac
done < imports.txt
if (( fail != 0 )); then
echo
echo "❌ This executable is copied into every Windows bundle and"
echo " run on machines with no toolchain. Link it statically"
echo " rather than shipping the DLL beside it."
exit 1
fi
echo "✅ imports nothing but Windows' own DLLs."
ls -la "$exe"
- uses: actions/upload-artifact@v4
with:
name: ariza-runner-windows-${{ matrix.arch }}
path: build/ariza-runner-windows-${{ matrix.arch }}.exe
if-no-files-found: error
publish:
needs: build
runs-on: ubuntu-latest
# Tag-triggered runs only. A dispatch builds and tests both lanes
# and stops there, which is the same contract Notcurses-Native's
# binary releases use and for the same reason: the assets published
# here are what every ariza bundle checksum-verifies against, and
# replacing them under a tag that is already live breaks builds that
# were reproducible a minute earlier.
if: startsWith(github.ref, 'refs/tags/runner-v')
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v4
with:
path: downloaded/
- name: Collect, checksum, and check the tag
id: prep
shell: bash
run: |
set -euxo pipefail
mkdir -p release
find downloaded -type f -name 'ariza-runner-windows-*.exe' \
-exec cp {} release/ \;
# Both lanes, or this is not a release. A half-published tag
# would leave `ariza bundle` failing closed for one
# architecture with a pin file that says it should work.
for arch in x86_64 aarch64; do
if [[ ! -f "release/ariza-runner-windows-$arch.exe" ]]; then
echo "::error::no artefact for $arch"
ls -la release
exit 1
fi
done
cd release
sha256sum * | sort > ../checksums.txt
cd ..
echo "--- checksums.txt ---"
cat checksums.txt
file_tag=$(tr -d '[:space:]' < resources/RUNNER_VERSION)
if [[ -z "$file_tag" ]]; then
echo "❌ resources/RUNNER_VERSION is empty."
exit 1
fi
pushed=${GITHUB_REF#refs/tags/}
if [[ "$pushed" != "$file_tag" ]]; then
echo "❌ Pushed tag '$pushed' doesn't match resources/RUNNER_VERSION '$file_tag'."
echo " That file is what ariza builds its download URL from, so a"
echo " release published under any other tag is one no bundle"
echo " would ever fetch."
exit 1
fi
echo "tag=$file_tag" >> "$GITHUB_OUTPUT"
- name: Create / update GitHub release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.prep.outputs.tag }}
name: ${{ steps.prep.outputs.tag }}
files: |
release/*
checksums.txt
body: |
The compiled launcher ariza stages into a Windows bundle as
`bin/<exec>.exe`.
It reads `bin/<exec>.ariza` beside itself, applies that
file's environment directives, and starts the bundle's own
`raku.exe` on the app's script with the caller's arguments
passed through **byte for byte** — no `cmd.exe` anywhere in
the launch. That is what keeps `^`, `%VAR%`, `!x!` and
quote-heavy arguments intact (a batch trampoline re-parses
`%*` and cannot not damage them), and what keeps a bundle
usable where script-execution policy refuses to run a `.cmd`
or a `.ps1` at all.
Built from `runner/` in this repository:
`ariza-runner-windows-x86_64.exe` in MSYS2 UCRT64 and
`ariza-runner-windows-aarch64.exe` in MSYS2 CLANGARM64,
statically linked and asserted to import nothing but
Windows' own DLLs.
These binaries are **not code-signed**, so SmartScreen will
warn on first run of a bundle that carries one. The `.cmd`
and `.ps1` launchers ship alongside and remain supported.
## Verifying
```
sha256sum -c checksums.txt
```
## After release
Copy `checksums.txt` into `resources/runner-checksums.txt`
and commit it. Until then `ariza bundle` builds Windows
bundles without the executable, loudly; afterwards a failed
download or a digest mismatch fails the build.
draft: false
prerelease: false