|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Embed AppImage update information into a built AppImage and write the matching |
| 4 | +# zsync file next to it. |
| 5 | +# |
| 6 | +# An AppImage type-2 runtime reserves a 1 KiB `.upd_info` ELF section for a |
| 7 | +# single "update information" string. Tauri's bundler leaves it zeroed, so |
| 8 | +# AppImageUpdate, AppImageLauncher, AppManager and AM all report that the image |
| 9 | +# carries no update information and refuse to update it. Filling it in, plus |
| 10 | +# publishing a `.zsync` alongside the AppImage, lets those tools fetch only the |
| 11 | +# blocks that changed instead of the whole ~120 MB image. |
| 12 | +# |
| 13 | +# See https://github.com/AppImage/AppImageSpec/blob/master/draft.md#update-information |
| 14 | +# |
| 15 | +# Usage: |
| 16 | +# REPO=opengeos/GeoLibre TAG=v1.5.0 \ |
| 17 | +# scripts/embed-appimage-update-info.sh path/to/GeoLibre.Desktop_1.5.0_amd64.AppImage |
| 18 | +# |
| 19 | +# # Print the update information string for a file name and exit (no writes): |
| 20 | +# REPO=opengeos/GeoLibre TAG=v1.5.0 \ |
| 21 | +# scripts/embed-appimage-update-info.sh --print GeoLibre.Desktop_1.5.0_amd64.AppImage |
| 22 | +# |
| 23 | +# REPO (owner/name) and TAG (the release tag, e.g. v1.5.0) are both required. |
| 24 | +# The AppImage is patched in place, so run this before uploading it. |
| 25 | +set -euo pipefail |
| 26 | + |
| 27 | +: "${REPO:?Set REPO to the GitHub repository, e.g. opengeos/GeoLibre}" |
| 28 | +: "${TAG:?Set TAG to the release tag, e.g. v1.5.0}" |
| 29 | + |
| 30 | +print_only=false |
| 31 | +if [[ "${1:-}" == "--print" ]]; then |
| 32 | + print_only=true |
| 33 | + shift |
| 34 | +fi |
| 35 | + |
| 36 | +appimage="${1:?Pass the path to the .AppImage}" |
| 37 | + |
| 38 | +[[ "$REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { |
| 39 | + echo "REPO is not an owner/name pair: $REPO" >&2 |
| 40 | + exit 1 |
| 41 | +} |
| 42 | +owner="${REPO%%/*}" |
| 43 | +name="${REPO##*/}" |
| 44 | + |
| 45 | +# The tag carries the version the bundler stamped into the file name. |
| 46 | +version="${TAG#v}" |
| 47 | +base="$(basename "$appimage")" |
| 48 | + |
| 49 | +# The update information has to match *every future* release, not just this one, |
| 50 | +# so the version in the file name becomes a glob. Rewriting it here (rather than |
| 51 | +# hard-coding the pattern) keeps the two in step, and the guard below turns a |
| 52 | +# future rename of the bundle into a loud failure instead of update information |
| 53 | +# that silently matches nothing. |
| 54 | +pattern="${base/_${version}_/_*_}" |
| 55 | +[[ "$pattern" != "$base" ]] || { |
| 56 | + echo "AppImage name '$base' does not contain _${version}_; cannot derive a zsync pattern" >&2 |
| 57 | + exit 1 |
| 58 | +} |
| 59 | + |
| 60 | +# `latest` makes the installed AppImage track the newest release rather than the |
| 61 | +# one it shipped in. |
| 62 | +update_information="gh-releases-zsync|${owner}|${name}|latest|${pattern}.zsync" |
| 63 | + |
| 64 | +if [[ "$print_only" == true ]]; then |
| 65 | + printf '%s\n' "$update_information" |
| 66 | + exit 0 |
| 67 | +fi |
| 68 | + |
| 69 | +[[ -f "$appimage" ]] || { |
| 70 | + echo "No such AppImage: $appimage" >&2 |
| 71 | + exit 1 |
| 72 | +} |
| 73 | + |
| 74 | +# Columns of `objdump -h`: Idx Name Size VMA LMA "File off" Algn. The two hex |
| 75 | +# fields are converted in bash, not awk: `strtonum` is a gawk extension and the |
| 76 | +# Ubuntu runners' default awk is mawk. |
| 77 | +read -r size_hex offset_hex < <( |
| 78 | + objdump -h "$appimage" | awk '$2 == ".upd_info" { print $3, $6; exit }' |
| 79 | +) |
| 80 | +[[ "${size_hex:-}" =~ ^[0-9a-fA-F]+$ && "${offset_hex:-}" =~ ^[0-9a-fA-F]+$ ]] || { |
| 81 | + echo "No .upd_info section in $appimage; is it an AppImage type-2 runtime?" >&2 |
| 82 | + exit 1 |
| 83 | +} |
| 84 | +size=$((16#$size_hex)) |
| 85 | +offset=$((16#$offset_hex)) |
| 86 | +(( size > 0 )) || { |
| 87 | + echo "The .upd_info section in $appimage is empty" >&2 |
| 88 | + exit 1 |
| 89 | +} |
| 90 | +# Leave room for the terminating NUL. |
| 91 | +(( ${#update_information} < size )) || { |
| 92 | + echo "Update information (${#update_information} bytes) does not fit in the ${size}-byte .upd_info section" >&2 |
| 93 | + exit 1 |
| 94 | +} |
| 95 | + |
| 96 | +# Overwrite the whole section, NUL-padded, rather than only the prefix: the |
| 97 | +# reader stops at the first NUL, so any leftover bytes would corrupt the string. |
| 98 | +# `dd` (not objcopy) because objcopy would rewrite the ELF and drop the squashfs |
| 99 | +# image appended after it. |
| 100 | +{ |
| 101 | + printf '%s' "$update_information" |
| 102 | + head -c "$((size - ${#update_information}))" /dev/zero |
| 103 | +} | dd of="$appimage" bs=1 seek="$offset" count="$size" conv=notrunc status=none |
| 104 | + |
| 105 | +# Read it back so a silent short write cannot ship as a working AppImage. |
| 106 | +embedded="$(dd if="$appimage" bs=1 skip="$offset" count="$size" status=none | tr -d '\0')" |
| 107 | +[[ "$embedded" == "$update_information" ]] || { |
| 108 | + echo "Verification failed: .upd_info holds '$embedded', expected '$update_information'" >&2 |
| 109 | + exit 1 |
| 110 | +} |
| 111 | +echo "Embedded update information: $update_information" |
| 112 | + |
| 113 | +# zsync must be generated from the *patched* image, otherwise its checksums |
| 114 | +# describe a file that no longer exists. The URL is absolute so the client never |
| 115 | +# has to resolve it against the redirect GitHub serves release assets through. |
| 116 | +command -v zsyncmake >/dev/null || { |
| 117 | + echo "zsyncmake not found; install the 'zsync' package" >&2 |
| 118 | + exit 1 |
| 119 | +} |
| 120 | +zsyncmake \ |
| 121 | + -u "https://github.com/${REPO}/releases/download/${TAG}/${base}" \ |
| 122 | + -o "${appimage}.zsync" \ |
| 123 | + "$appimage" |
| 124 | +echo "Wrote ${appimage}.zsync" |
0 commit comments