Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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_<version>_<arch>-setup.exe); see the justfile bundle recipes.
- target: macos
os: macos-latest
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<version>_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_<version>_x86_64.AppImage
./arto_<version>_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

Expand Down
39 changes: 32 additions & 7 deletions desktop/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
122 changes: 86 additions & 36 deletions scripts/verify-linux-bundle.sh
Original file line number Diff line number Diff line change
@@ -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 <path to .deb> <executable name>}"
executable="${2:?usage: verify-linux-bundle.sh <path to .deb> <executable name>}"
usage="usage: verify-linux-bundle.sh <path to .deb or .AppImage> <executable name>"
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

Expand All @@ -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
Comment on lines +77 to +88

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