From 75ed06a10284634ddf65cdd6722b8f33682efaf4 Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 1 Aug 2026 15:40:00 +0900 Subject: [PATCH 1/3] feat(linux): bundle an AppImage alongside the .deb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The .deb installs only on Debian and Ubuntu, which left every other distribution — Fedora, openSUSE, Arch — with no runnable artifact other than the Nix package. An AppImage covers them from the same build. An .rpm would be the obvious alternative, but dx 0.7 has no RPM-specific dependency settings: it 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. The bundle directory is swept before bundling because CI caches it and non-release runs leave artifacts stamped 0.0.0 behind, which the release upload glob would attach alongside the real ones. The verifier gains an AppImage path: the artifact is self-extracted and its binary checked exactly like the packaged one, and the bundled libraries are checked too. linuxdeploy rewrites RUNPATH to $ORIGIN and copies the host's libraries in, so a devShell build would otherwise pass the header check while shipping a directory full of /nix/store libraries. --- desktop/justfile | 39 +++++++++-- scripts/verify-linux-bundle.sh | 122 +++++++++++++++++++++++---------- 2 files changed, 118 insertions(+), 43 deletions(-) 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 From 834a5c13748249b3b6df4bc97e775d54698c245d Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 1 Aug 2026 15:40:03 +0900 Subject: [PATCH 2/3] ci(linux): publish the AppImage from the release build The Linux legs would build the AppImage but upload only the .deb, so the artifact non-Debian distributions depend on would never reach a release. --- .github/workflows/build.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 From 2d245909dee2c2ba9c500e0b4ea7950befd4cc1c Mon Sep 17 00:00:00 2001 From: Alisue Date: Sat, 1 Aug 2026 15:40:07 +0900 Subject: [PATCH 3/3] docs(readme): document the AppImage install path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Linux section told every reader to install a .deb, which the package manager on Fedora and other RPM distributions cannot use — leaving a source build or Nix as the only documented way in. --- README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) 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