diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bbcaf17..fff9317 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,7 +119,9 @@ jobs: fail-fast: false matrix: include: - # macOS produces a .dmg, Linux a .deb, Windows an NSIS installer + # macOS produces a .dmg, Linux a .deb plus an .AppImage (the .deb + # installs only on Debian/Ubuntu, so the AppImage is what every other + # distro runs), Windows an NSIS installer # (Arto__-setup.exe); see the justfile bundle recipes. - target: macos os: macos-latest @@ -130,13 +132,17 @@ jobs: timeout: 60 - target: ubuntu os: ubuntu-latest - artifacts: ./desktop/target/dx/arto/bundle/**/*.deb + artifacts: | + ./desktop/target/dx/arto/bundle/**/*.deb + ./desktop/target/dx/arto/bundle/**/*.AppImage # A cold cargo cache pushes the full release build past 30m on the # Linux runners (same class of build the nix-check job budgets 60m for). timeout: 60 - target: ubuntu-arm64 os: ubuntu-24.04-arm - artifacts: ./desktop/target/dx/arto/bundle/**/*.deb + artifacts: | + ./desktop/target/dx/arto/bundle/**/*.deb + ./desktop/target/dx/arto/bundle/**/*.AppImage timeout: 60 - target: windows os: windows-latest @@ -391,12 +397,12 @@ jobs: with: tag: ${{ github.event.release.tag_name }} allowUpdates: true - # Lenient so a missing Linux .deb or Windows installer (tolerated build - # failures) does not block the release. The macOS .dmg is still + # Lenient so a missing Linux artifact or Windows installer (tolerated + # build failures) does not block the release. The macOS .dmg is still # effectively required: the release only runs after the mandatory macOS # build, and the downstream dispatch job fails if the aarch64 DMG is absent. artifactErrorsFailBuild: false - artifacts: ./artifacts/**/*.dmg,./artifacts/**/*.deb,./artifacts/**/*-setup.exe + artifacts: ./artifacts/**/*.dmg,./artifacts/**/*.deb,./artifacts/**/*.AppImage,./artifacts/**/*-setup.exe removeArtifacts: true makeLatest: true bodyFile: release_body.md diff --git a/README.md b/README.md index efcb062..1797bbd 100644 --- a/README.md +++ b/README.md @@ -116,16 +116,28 @@ xattr -dr com.apple.quarantine /Applications/Arto.app ### Linux -Download the `.deb` matching your architecture from the [releases] page and -install it with `apt`, which pulls in the GTK/WebKit libraries it declares: +On Debian and Ubuntu, download the `.deb` matching your architecture from the +[releases] page and install it with `apt`, which pulls in the GTK/WebKit +libraries it declares: ``` sudo apt install ./arto__amd64.deb ``` -The package is built on Ubuntu 24.04, so it requires glibc 2.39 or newer -(Ubuntu 24.04+, Debian 13+) and WebKitGTK 4.1. On older distributions, build -from source or use the Nix package below. +On every other distribution — Fedora, openSUSE, Arch — download the +`.AppImage` instead, make it executable and run it: + +``` +chmod +x arto__x86_64.AppImage +./arto__x86_64.AppImage +``` + +The AppImage needs WebKitGTK 4.1 present on the system; on Fedora that is +`sudo dnf install webkit2gtk4.1`. + +Both artifacts are built on Ubuntu 24.04, so they require glibc 2.39 or newer +(Ubuntu 24.04+, Debian 13+, Fedora 40+). On older distributions, build from +source or use the Nix package below. ### Nix diff --git a/desktop/justfile b/desktop/justfile index 9b6973c..5b0222b 100644 --- a/desktop/justfile +++ b/desktop/justfile @@ -140,10 +140,27 @@ _package-dmg: rm -rf "$staging" echo "DMG with embedded Quick Look extension created at $dmg" +# Two Linux artifacts are produced from the same build: a .deb for +# Debian/Ubuntu, and an AppImage for everything else. The AppImage is not a +# convenience — it is the only artifact a non-Debian distro (Fedora, openSUSE, +# Arch) can run. An .rpm is deliberately not built: dx 0.7 has no RPM-specific +# dependency settings and feeds `[bundle.deb] depends` straight into the RPM +# `Requires`, so the package would demand Debian package names that no RPM +# distro can resolve and would fail at install time. [linux] build: - @rm -rf target/dx/arto/release/linux/app/assets - dx bundle --release --linux --package-types deb + #!/usr/bin/env bash + set -euo pipefail + rm -rf target/dx/arto/release/linux/app/assets + # Drop every previously built Linux artifact first. CI caches + # `desktop/target` (which contains `bundle/`), and the build job also runs on + # non-release pushes where the version stays 0.0.0; without this sweep a + # restored `arto_0.0.0_*` package would be attached to a later release + # alongside the real one, since the upload glob matches on extension. + if [[ -d target/dx/arto/bundle ]]; then + find target/dx/arto/bundle -type f \( -name '*.deb' -o -name '*.AppImage' \) -delete + fi + dx bundle --release --linux --package-types deb --package-types appimage # Release artifacts are built with the platform's native toolchain, but a # bundle built inside the Nix devShell links against /nix/store paths and is @@ -160,16 +177,24 @@ verify-bundle: verify-bundle: #!/usr/bin/env bash set -euo pipefail - # Every .deb under the bundle dir is checked, not just the newest: CI caches - # `desktop/target`, so a stale package from an earlier run can be sitting - # there, and the release upload glob would pick it up too. + # Every artifact under the bundle dir is checked, not just the newest: CI + # caches `desktop/target`, so a stale package from an earlier run can be + # sitting there, and the release upload glob would pick it up too. mapfile -t debs < <(find target/dx/arto/bundle -type f -name '*.deb') + mapfile -t appimages < <(find target/dx/arto/bundle -type f -name '*.AppImage') + # Both formats must be present. A missing one means the release would ship + # only half the Linux story — no .deb, or (worse, since it is the only + # artifact non-Debian distros can run) no AppImage. if [[ ${#debs[@]} -eq 0 ]]; then echo "Error: no .deb found under target/dx/arto/bundle" >&2 exit 1 fi - for deb in "${debs[@]}"; do - ../scripts/verify-linux-bundle.sh "$deb" arto + if [[ ${#appimages[@]} -eq 0 ]]; then + echo "Error: no .AppImage found under target/dx/arto/bundle" >&2 + exit 1 + fi + for artifact in "${debs[@]}" "${appimages[@]}"; do + ../scripts/verify-linux-bundle.sh "$artifact" arto done [macos] diff --git a/scripts/verify-linux-bundle.sh b/scripts/verify-linux-bundle.sh index 4fd78a1..eaf9704 100755 --- a/scripts/verify-linux-bundle.sh +++ b/scripts/verify-linux-bundle.sh @@ -1,32 +1,59 @@ #!/usr/bin/env bash -# Reject a Debian package that would not run outside the build environment. +# Reject a Linux artifact that would not run outside the build environment. # -# A package built inside the Nix devShell records an ELF interpreter and a +# An artifact built inside the Nix devShell records an ELF interpreter and a # RUNPATH under /nix/store. Such a binary cannot even be exec'd on a machine -# without that store path, so the whole package is dead on arrival. +# without that store path, so the whole artifact is dead on arrival. # # The packaged binary is inspected rather than the one in the build tree, # because the package is what gets published. # # 1. The ELF interpreter, NEEDED entries and RUNPATH/RPATH must be free of # /nix/store, and no /nix/store string may survive anywhere in the binary. -# 2. The package must declare its shared-library dependencies, so installing -# it pulls them in instead of leaving the user to hunt them down. -# 3. The binary actually starts, which is the only way to confirm that the +# An AppImage carries copies of the host's libraries, so those are checked +# for the same references too. +# 2. A .deb must declare its shared-library dependencies, so installing it +# pulls them in instead of leaving the user to hunt them down. An AppImage +# has no dependency metadata to declare — it carries the libraries +# linuxdeploy copied in — so this check applies to the .deb only. +# 3. The artifact actually starts, which is the only way to confirm that the # interpreter and every NEEDED library really resolve. `--version` exits # immediately and needs no display. set -euo pipefail -deb="${1:?usage: verify-linux-bundle.sh }" -executable="${2:?usage: verify-linux-bundle.sh }" +usage="usage: verify-linux-bundle.sh " +artifact="${1:?$usage}" +executable="${2:?$usage}" + +case "$artifact" in + *.deb) kind=deb ;; + *.AppImage) kind=appimage ;; + *) + echo "Error: unsupported artifact '$artifact' ($usage)" >&2 + exit 1 + ;; +esac workdir="$(mktemp -d)" trap 'rm -rf "$workdir"' EXIT -dpkg-deb --fsys-tarfile "$deb" | tar -x -C "$workdir" -bin="$workdir/usr/bin/$executable" +if [[ "$kind" == "deb" ]]; then + dpkg-deb --fsys-tarfile "$artifact" | tar -x -C "$workdir" + root="$workdir" +else + # `--appimage-extract` unpacks into ./squashfs-root of the *current* + # directory, so it has to run inside the scratch dir with an absolute path to + # the artifact. Extraction is used rather than a FUSE mount because CI + # runners and containers frequently have no /dev/fuse. + artifact="$(cd "$(dirname "$artifact")" && pwd)/$(basename "$artifact")" + chmod +x "$artifact" + (cd "$workdir" && "$artifact" --appimage-extract >/dev/null) + root="$workdir/squashfs-root" +fi + +bin="$root/usr/bin/$executable" if [[ ! -x "$bin" ]]; then - echo "Error: $deb does not contain usr/bin/$executable" >&2 + echo "Error: $artifact does not contain usr/bin/$executable" >&2 exit 1 fi @@ -47,35 +74,58 @@ elif refs="$(grep -F /nix/store <<<"$embedded" | sort -u)"; then status=1 fi -depends="$(dpkg-deb -f "$deb" Depends)" -if [[ -z "$depends" ]]; then - echo "Error: $deb declares no Depends, so installing it does not pull in" >&2 - echo " the shared libraries the binary needs." >&2 - status=1 -elif command -v apt-cache >/dev/null; then - # Each declared name must exist in the distro index. A typo would otherwise - # surface only as an unmet dependency on a user's machine. Alternatives - # ("a | b") are satisfied by any one of their members, which is how the - # Ubuntu/Debian package renamings are expressed. - while IFS= read -r entry; do - resolved="" - while IFS= read -r alternative; do - if apt-cache show "$alternative" >/dev/null 2>&1; then - resolved=1 - break - fi - done < <(tr '|' '\n' <<<"$entry" | perl -lpe 's/\(.*\)//; s/^\s+|\s+$//g') +if [[ "$kind" == "appimage" && -d "$root/usr/lib" ]]; then + # The header check above is not sufficient for an AppImage: linuxdeploy + # rewrites RUNPATH to $ORIGIN/../lib and copies every library it resolved on + # the build host into the AppDir. A devShell build would therefore have its + # /nix/store RUNPATH laundered out of the headers while the bundled libraries + # are the store's own. Inspect what was actually shipped alongside it. + if refs="$(grep -rlF /nix/store "$root/usr/lib")"; then + echo "Error: bundled libraries come from the build environment:" >&2 + echo "$refs" >&2 + status=1 + fi +fi + +if [[ "$kind" == "deb" ]]; then + depends="$(dpkg-deb -f "$artifact" Depends)" + if [[ -z "$depends" ]]; then + echo "Error: $artifact declares no Depends, so installing it does not pull in" >&2 + echo " the shared libraries the binary needs." >&2 + status=1 + elif command -v apt-cache >/dev/null; then + # Each declared name must exist in the distro index. A typo would otherwise + # surface only as an unmet dependency on a user's machine. Alternatives + # ("a | b") are satisfied by any one of their members, which is how the + # Ubuntu/Debian package renamings are expressed. + while IFS= read -r entry; do + resolved="" + while IFS= read -r alternative; do + if apt-cache show "$alternative" >/dev/null 2>&1; then + resolved=1 + break + fi + done < <(tr '|' '\n' <<<"$entry" | perl -lpe 's/\(.*\)//; s/^\s+|\s+$//g') - if [[ -z "$resolved" ]]; then - echo "Error: dependency '$entry' matches no package in the distro index" >&2 - status=1 - fi - done < <(tr ',' '\n' <<<"$depends" | perl -lpe 's/^\s+|\s+$//g' | grep -v '^$') + if [[ -z "$resolved" ]]; then + echo "Error: dependency '$entry' matches no package in the distro index" >&2 + status=1 + fi + done < <(tr ',' '\n' <<<"$depends" | perl -lpe 's/^\s+|\s+$//g' | grep -v '^$') + fi fi if [[ "$status" -ne 0 ]]; then exit "$status" fi -"$bin" --version -echo "Package verified: no /nix/store references, dependencies declared, executable launches" +if [[ "$kind" == "deb" ]]; then + "$bin" --version + echo "Package verified: no /nix/store references, dependencies declared, executable launches" +else + # The artifact itself is launched, not the extracted binary: that exercises + # the AppImage runtime the user actually runs. Extract-and-run for the same + # no-FUSE reason as above. + APPIMAGE_EXTRACT_AND_RUN=1 "$artifact" --version + echo "AppImage verified: no /nix/store references, artifact launches" +fi