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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Make AppImage the signed Linux update payload when that format is enabled,
emit its SHA-256 sidecar, and add install-kind-aware update selection so
AppImage, portable, and package-manager installations cannot cross streams.
- Graduate the Linux package-manager lifecycle gates from a single install to
two-version deb/rpm transactions: corrupt upgrades preserve the working
install, normal upgrades replace both metadata and payload, Debian rejects
Expand Down
6 changes: 3 additions & 3 deletions docs/release-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ RIVET_MINIMUM_UPDATABLE_VERSION=1.0.0 # optional; defaults to 0.0.0
RIVET_UPDATE_ROLLOUT=100 # optional; 0..100
```

Run `raco rivet release`. The result is a signed MSI on Windows, a signed/notarized DMG on macOS, or an Ed25519-signed self-contained `.tar.gz` on Linux, plus a versioned portable zip, channel manifest, CycloneDX SBOM, and `THIRD_PARTY_NOTICES.txt`. The channel manifest describes the portable zip; platform installers remain direct-download artifacts. `release --development` exercises the same flow without production platform signing, but the update manifest still requires its independent Ed25519 key.
Run `raco rivet release`. The result is a signed MSI on Windows, a signed/notarized DMG on macOS, or an Ed25519-signed self-contained `.tar.gz` on Linux, plus a versioned portable zip, channel manifest, CycloneDX SBOM, and `THIRD_PARTY_NOTICES.txt`. The channel manifest describes the portable zip on Windows/macOS and on Linux without AppImage; when AppImage packaging is enabled, it describes the exact AppImage instead. Other platform installers remain direct-download artifacts. `release --development` exercises the same flow without production platform signing, but the update manifest still requires its independent Ed25519 key.

On Linux the release additionally builds native system installers: a `.deb` (dpkg-deb, unprivileged, installs under `/opt/<name>` with a desktop entry), an `.rpm` (rpmbuild BUILDROOT, distro-independent), and an `.AppImage` (bundled GTK4 dependency closure, so the same file runs on older distributions). Select the set with the `linux-formats` project setting; the signed tar.gz is always produced as the self-contained installer, while the portable zip is the update-channel payload. Package-manager installs upgrade through the package manager; AppImage installs upgrade by replacing the AppImage. AppImage packaging requires a `linux-icon` PNG in rivet.rktd — the format mandates a top-level icon and Rivet fails closed rather than shipping a placeholder.
On Linux the release additionally builds native system installers: a `.deb` (dpkg-deb, unprivileged, installs under `/opt/<name>` with a desktop entry), an `.rpm` (rpmbuild BUILDROOT, distro-independent), and an `.AppImage` (bundled GTK4 dependency closure, so the same file runs on older distributions). Select the set with the `linux-formats` project setting. The signed tar.gz and portable zip are always retained as release assets. When AppImage is enabled, the signed manifest carries its exact URL, size, SHA-256, and `appimage` installer kind, and release also writes a `.AppImage.sha256` sidecar; no product should infer an unsigned sibling URL. Package-manager installs upgrade through the package manager; AppImage installs use Rivet's verified atomic replacement. AppImage packaging requires a `linux-icon` PNG in rivet.rktd — the format mandates a top-level icon and Rivet fails closed rather than shipping a placeholder.

Applications that deliberately ship without an online update channel can run `raco rivet release --without-updates`. The command still builds, packages, platform-signs, verifies, and emits the installer, SBOM, and third-party notices; it skips only the channel manifest and does not read any `RIVET_UPDATE_*` credentials. Combine it with `--development` to exercise the unsigned release flow before publisher credentials exist. Omitting `--without-updates` keeps the fail-closed behavior above: all three update settings are required, and a partial configuration is an error.

Expand Down Expand Up @@ -84,7 +84,7 @@ Embed only `update-public.der` (or its bytes) in the native host. Key rotation i

## Application API

Require `rivet/distribution`. `fetch-update-manifest` verifies before parsing, `select-update` applies channel/version/rollout/platform policy, and `download-update` enforces limits and hashes. `prepare-platform-installation` turns a verified portable ZIP or Linux AppImage into Rivet's first-party atomic replacement; pass its result to `execute-platform-installation!`. The application supplies restart and health callbacks because it owns its process model and readiness signal. On Windows and macOS the default staged-payload verifier requires Authenticode or a deep strict code signature; development builds may inject an explicit verifier.
Require `rivet/distribution`. `fetch-update-manifest` verifies before parsing, `select-update` applies channel/version/rollout/platform policy, and `download-update` enforces limits and hashes. On Linux, pass `(current-install-kind)` as `#:install-kind`: AppImage and portable installs only select their matching payload, while deb/rpm installations return `package-manager` and do not consume the AppImage feed. `prepare-platform-installation` turns a verified portable ZIP or Linux AppImage into Rivet's first-party atomic replacement; pass its result to `execute-platform-installation!`. The application supplies restart and health callbacks because it owns its process model and readiness signal. On Windows and macOS the default staged-payload verifier requires Authenticode or a deep strict code signature; development builds may inject an explicit verifier.

Pass `#:journal-path` to atomically persist every destructive phase. On the next start, reconstruct the same installation and call `recover-platform-installation!`; it accepts only the exact same signed candidate and absolute download/target/backup paths. An interruption before health restores the prior payload. An interruption during the idempotent commit repeats backup cleanup instead of rolling a healthy application back.

Expand Down
13 changes: 13 additions & 0 deletions rivet-cli/appimage.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
racket/set
racket/string
racket/system
"../rivet/distribution/crypto.rkt"
"linux-native-package.rkt"
"project.rkt")

Expand Down Expand Up @@ -65,6 +66,16 @@
(project-version project)
(appimage-architecture))))

(define (write-appimage-checksum! output)
(define sidecar (string->path (string-append (path->string output) ".sha256")))
(call-with-output-file sidecar
#:exists 'truncate/replace
(lambda (out)
(fprintf out "~a ~a~n"
(sha256-file/hex output)
(path->string (file-name-from-path output)))))
sidecar)

;; Libraries that must remain the host system's own: the dynamic loader and
;; libc family, and the GL/Vulkan driver stack (the display driver owns it).
;; libstdc++/libgcc are bundled on purpose — old distributions ship older
Expand Down Expand Up @@ -303,10 +314,12 @@
(when (file-exists? output) (delete-file output))
(putenv "APPIMAGE_EXTRACT_AND_RUN" "1")
(run! 'create-appimage! tool (path->string appdir) (path->string output))
(write-appimage-checksum! output)
output)

(module+ test-support
(provide stage-appdir!
write-appimage-apprun!
write-appimage-checksum!
appimage-installer-path
run/capture))
8 changes: 4 additions & 4 deletions rivet-cli/installer.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,10 @@
#:exists 'truncate/replace
(lambda (out)
(displayln (bytes->base64-string signature) out))))
;; Native system installer formats ride along with the signed tar.gz
;; payload: deb and rpm integrate with the distribution package manager,
;; AppImage carries its own GTK4 dependency closure. The tar.gz remains
;; the update-channel artifact for every format choice.
;; Native system installer formats ride along with the signed tar.gz:
;; deb/rpm integrate with the distribution package manager, while AppImage
;; carries its own GTK4 dependency closure and becomes the signed in-place
;; update payload when enabled.
(for ([format (in-list (project-linux-formats project))])
(case format
[("deb") (create-deb! project package)]
Expand Down
2 changes: 1 addition & 1 deletion rivet-cli/project.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@
(andmap (lambda (item)
(member item '("deb" "rpm" "appimage")))
v)))
"subset of (\"deb\" \"rpm\" \"appimage\") selecting the native Linux installer formats; the signed tar.gz update payload is always produced")
"subset of (\"deb\" \"rpm\" \"appimage\") selecting native Linux installer formats; AppImage becomes the signed update payload when enabled")
(optional 'linux-binary-name
(lambda (v)
(and (string? v)
Expand Down
41 changes: 32 additions & 9 deletions rivet-cli/release.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
racket/string
"../rivet/distribution/crypto.rkt"
"../rivet/distribution/manifest.rkt"
"appimage.rkt"
"compliance.rkt"
"installer.rkt"
"package.rkt"
Expand Down Expand Up @@ -80,19 +81,40 @@
(path->string (file-name-from-path zip-path)))))
zip-path)

(define (portable-update-artifact update-config portable-zip)
(define (update-artifact-for-file update-config payload platform architecture installer)
(update-artifact
(release-platform)
(release-architecture)
platform
architecture
(string-append
(string-trim (update-environment-base-url update-config) "/")
"/"
(path->string (file-name-from-path portable-zip)))
(sha256-file/hex portable-zip)
(file-size portable-zip)
'zip
(path->string (file-name-from-path payload)))
(sha256-file/hex payload)
(file-size payload)
installer
'()))

(define (portable-update-artifact update-config portable-zip)
(update-artifact-for-file update-config portable-zip
(release-platform) (release-architecture) 'zip))

(define (release-update-artifact update-config project portable-zip
#:platform [platform (release-platform)]
#:architecture [architecture (release-architecture)])
;; AppImage is the Linux self-replacing format. When a release produces one,
;; make it the signed update payload rather than asking products to derive an
;; unsigned sibling URL from the portable ZIP or tarball name.
(define appimage? (and (eq? platform 'linux)
(member "appimage" (project-linux-formats project))))
(define payload (if appimage? (appimage-installer-path project) portable-zip))
(unless (file-exists? payload)
(raise-arguments-error 'release-project!
"selected update payload was not produced"
"installer" (if appimage? 'appimage 'zip)
"payload" payload))
(update-artifact-for-file update-config payload platform architecture
(if appimage? 'appimage 'zip)))

(define (release-project! project
#:production? [production? #t]
#:updates? [updates? #t])
Expand Down Expand Up @@ -121,7 +143,7 @@
update-config
(let* ([previous (getenv "RIVET_PREVIOUS_VERSION")]
[artifact
(portable-update-artifact update-config portable-zip)]
(release-update-artifact update-config project portable-zip)]
[manifest
(update-manifest
(project-identifier project)
Expand Down Expand Up @@ -152,4 +174,5 @@
(values installer manifest-path sbom notices portable-zip))

(module+ test-support
(provide portable-update-artifact))
(provide portable-update-artifact
release-update-artifact))
32 changes: 32 additions & 0 deletions rivet/distribution/platform-adapter.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
racket/file
racket/list
racket/path
racket/string
racket/system
"manifest.rkt"
"updater.rkt")

(provide (struct-out platform-installation)
current-update-platform
current-install-kind
platform-installer-policy
verify-platform-payload!
prepare-platform-installation
Expand All @@ -32,6 +34,33 @@
"unsupported operating system"
"system-type" (system-type 'os))]))

(define (detect-install-kind platform executable appimage)
(cond
[(not (eq? platform 'linux)) 'portable]
[(and appimage (not (string=? (string-trim appimage) ""))) 'appimage]
[else
(define raw
(string-replace (if (path? executable)
(path->string executable)
executable)
"\\" "/"))
(define normalized
(if (string-prefix? raw "/")
raw
(string-replace
(path->string (simplify-path (path->complete-path executable) #f))
"\\" "/")))
;; Rivet's deb and rpm both install under /opt. /usr also covers products
;; following the conventional rpm layout. Update ownership matters here,
;; not which package database owns the executable.
(if (or (string-prefix? normalized "/opt/")
(string-prefix? normalized "/usr/"))
'package-manager
'portable)]))

(define (current-install-kind [executable (find-system-path 'run-file)])
(detect-install-kind (current-update-platform) executable (getenv "APPIMAGE")))

;; `portable` means Rivet can replace the application payload itself.
;; `package-manager` means installation must remain under the OS transaction
;; owner; silently unpacking one of these artifacts would bypass elevation,
Expand Down Expand Up @@ -275,3 +304,6 @@
(platform-installation-plan installation)
journal-path
#:commit (platform-installation-commit installation)))

(module+ test-support
(provide detect-install-kind))
19 changes: 17 additions & 2 deletions rivet/distribution/updater.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,21 @@
#:key-id key-id))
(lambda () (close-input-port in))))

(define (select-update config manifest)
(define install-kinds '(portable appimage package-manager))

(define (artifact-matches-install-kind? artifact install-kind)
(or (not install-kind)
(case install-kind
[(portable) (eq? (update-artifact-installer artifact) 'zip)]
[(appimage) (eq? (update-artifact-installer artifact) 'appimage)]
[(package-manager)
(and (memq (update-artifact-installer artifact) '(deb rpm)) #t)])))

(define (select-update config manifest #:install-kind [install-kind #f])
(unless (or (not install-kind) (memq install-kind install-kinds))
(raise-argument-error 'select-update
"(or/c #f 'portable 'appimage 'package-manager)"
install-kind))
(cond
[(not (string=? (updater-config-application-id config)
(update-manifest-application-id manifest)))
Expand All @@ -91,7 +105,8 @@
#:when (and (eq? (update-artifact-platform item)
(updater-config-platform config))
(eq? (update-artifact-architecture item)
(updater-config-architecture config))))
(updater-config-architecture config))
(artifact-matches-install-kind? item install-kind)))
item))
(and artifact (update-candidate manifest artifact))]))

Expand Down
27 changes: 27 additions & 0 deletions tests/distribution.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@
"2026-09-28T00:00:00Z" "1.0.0" "1.1.0"
#t 100 (list sample-artifact)))

(define linux-config
(updater-config "dev.rivet.test" "1.1.0" 'stable 'linux 'x64
#f #f 0 (* 1024 1024)))
(define linux-zip
(update-artifact 'linux 'x64 "https://updates.example/app.zip"
(make-string 64 #\b) 4 'zip '()))
(define linux-appimage
(update-artifact 'linux 'x64 "https://updates.example/app.AppImage"
(make-string 64 #\c) 4 'appimage '()))
(define linux-manifest
(struct-copy update-manifest sample-manifest
[artifacts (list linux-zip linux-appimage)]))
(check-eq? (update-candidate-artifact
(select-update linux-config linux-manifest
#:install-kind 'portable))
linux-zip)
(check-eq? (update-candidate-artifact
(select-update linux-config linux-manifest
#:install-kind 'appimage))
linux-appimage)
(check-false (select-update linux-config linux-manifest
#:install-kind 'package-manager))
(check-exn exn:fail:contract?
(lambda ()
(select-update linux-config linux-manifest
#:install-kind 'unknown)))

(when private-key
(define public-key
(datum->pk-key (pk-key->datum private-key 'SubjectPublicKeyInfo)
Expand Down
10 changes: 10 additions & 0 deletions tests/linux-native-installer.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
racket/file
racket/path
racket/string
"../rivet/distribution/crypto.rkt"
"../rivet-cli/appimage.rkt"
(only-in (submod "../rivet-cli/appimage.rkt" test-support)
write-appimage-checksum!)
"../rivet-cli/deb.rkt"
"../rivet-cli/linux-native-package.rkt"
"../rivet-cli/project.rkt"
Expand Down Expand Up @@ -56,6 +59,13 @@
(check-true (regexp-match? #rx"(?m:\\.rpm$)" (path->string (rpm-installer-path project))))
(check-true (regexp-match? #rx"(?m:\\.AppImage$)"
(path->string (appimage-installer-path project))))
(define checksum-payload (build-path temp-root "Demo_App.AppImage"))
(write-bytes/text checksum-payload "appimage-bytes")
(define checksum-sidecar (write-appimage-checksum! checksum-payload))
(check-equal?
(string-trim (file->string checksum-sidecar))
(format "~a Demo_App.AppImage"
(sha256-file/hex checksum-payload)))

;; ------------------------------------------------------------- deb
(define deb-root (stage-deb-root! project package-dir
Expand Down
15 changes: 14 additions & 1 deletion tests/platform-update-adapter.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@
racket/file
racket/path
racket/port
"../rivet/distribution.rkt")
"../rivet/distribution.rkt"
(submod "../rivet/distribution/platform-adapter.rkt" test-support))

(define platform (current-update-platform))

(check-equal? (detect-install-kind 'linux "/tmp/App.AppImage"
"/tmp/App.AppImage")
'appimage)
(check-equal? (detect-install-kind 'linux "/opt/Example/RivetHost" #f)
'package-manager)
(check-equal? (detect-install-kind 'linux "/usr/bin/example" #f)
'package-manager)
(check-equal? (detect-install-kind 'linux "/home/user/example" #f)
'portable)
(check-equal? (detect-install-kind 'windows "C:\\Example\\RivetHost.exe" #f)
'portable)

(define (candidate installer [version "2.0.0"])
(define artifact
(update-artifact platform 'x64 "https://updates.example/application.zip"
Expand Down
Loading
Loading