pack: Add support for AppImage pack format.

* guix/scripts/pack.scm (self-contained-appimage): New procedure.
(%formats, show-formats): Add it.
(guix-pack): Honor it.
* doc/guix.texi: Document AppImage pack.
* tests/pack.scm ("appimage", "appimage + localstatedir"): New tests.

Co-authored-by: Noé Lopez <noelopez@free.fr>
Change-Id: I33ebfec623cff1cfcd6f029d2d3054c23ab1949a
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
This commit is contained in:
Sebastian Dümcke 2024-11-07 17:50:04 +01:00 committed by Ludovic Courtès
parent b143ec8eda
commit ccf72d5074
No known key found for this signature in database
GPG key ID: 090B11993D9AEBB5
3 changed files with 198 additions and 6 deletions

View file

@ -6955,6 +6955,16 @@ directly be used as a file system container image with the
environment}, using commands like @command{singularity shell} or environment}, using commands like @command{singularity shell} or
@command{singularity exec}. @command{singularity exec}.
@cindex AppImage, create an AppImage file with @command{guix pack}
Another format internally based on SquashFS is
@uref{https://appimage.org/, AppImage}. An AppImage file can be created
and executed without any special privileges:
@example
file=$(guix pack -f appimage --entry-point=bin/guile guile)
$file --help
@end example
Several command-line options allow you to customize your pack: Several command-line options allow you to customize your pack:
@table @code @table @code
@ -7071,6 +7081,48 @@ to install Guix-produced @samp{.rpm} packages on a system where
installation or other, non-rpm packs. installation or other, non-rpm packs.
@end quotation @end quotation
@item appimage
@cindex AppImage, create an AppImage file with @command{guix pack}
This produces an @uref{https://appimage.org/, AppImage file} with the
@samp{.AppImage} extension. AppImage is a SquashFS volume prefixed with
a runtime that mounts the SquashFS file system and executes the binary
provided with @option{--entry-point}. This results in a self-contained
archive that bundles the software and all its requirements into a single
file. When the file is made executable it runs the packaged software.
@example
guix pack -f appimage --entry-point=bin/vlc vlc
@end example
The runtime used by AppImages invokes the @command{fusermount3} command
to mount the image quickly. If that command is unavailable, the
AppImage fails to run, but it can still be started by passing the
@option{--appimage-extract-and-run} flag.
@quotation Warning
When building an AppImage, always @emph{pass} the @option{--relocatable}
option (or @option{-R}, or @option{-RR}) to make sure the image can be
used on systems where Guix is not installed. A warning is printed when
this option is not used.
@end quotation
@example
guix pack -f appimage --entry-point=bin/hello --relocatable hello
@end example
@quotation Note
The resulting AppImage does not conform to the complete standard as it
currently does not contain a @file{.DirIcon} file. This does not impact
functionality of the AppImage itself, but possibly that of software used
to manage AppImages.
@end quotation
@quotation Note
As the generated AppImage packages the complete dependency graph, it
will be larger than comparable AppImage files found online, which depend
on host system libraries.
@end quotation
@end table @end table
@cindex relocatable binaries @cindex relocatable binaries
@ -7160,10 +7212,10 @@ execution engines listed above by setting the
@cindex entry point, for Docker and Singularity images @cindex entry point, for Docker and Singularity images
@item --entry-point=@var{command} @item --entry-point=@var{command}
Use @var{command} as the @dfn{entry point} of the resulting pack, if the pack Use @var{command} as the @dfn{entry point} of the resulting pack, if the
format supports it---currently @code{docker} and @code{squashfs} (Singularity) pack format supports it---currently @code{docker}, @code{appimage}, and
support it. @var{command} must be relative to the profile contained in the @code{squashfs} (Singularity) support it. @var{command} must be
pack. relative to the profile contained in the pack.
The entry point specifies the command that tools like @code{docker run} or The entry point specifies the command that tools like @code{docker run} or
@code{singularity run} automatically start by default. For example, you can @code{singularity run} automatically start by default. For example, you can

View file

@ -10,6 +10,8 @@
;;; Copyright © 2022 Alex Griffin <a@ajgrf.com> ;;; Copyright © 2022 Alex Griffin <a@ajgrf.com>
;;; Copyright © 2023 Graham James Addis <graham@addis.org.uk> ;;; Copyright © 2023 Graham James Addis <graham@addis.org.uk>
;;; Copyright © 2023 Oleg Pykhalov <go.wigust@gmail.com> ;;; Copyright © 2023 Oleg Pykhalov <go.wigust@gmail.com>
;;; Copyright © 2024 Sebastian Dümcke <code@sam-d.com>
;;; Copyright © 2024 Noé Lopez <noelopez@free.fr>
;;; ;;;
;;; This file is part of GNU Guix. ;;; This file is part of GNU Guix.
;;; ;;;
@ -56,6 +58,7 @@ (define-module (guix scripts pack)
#:use-module ((gnu packages compression) #:hide (zip)) #:use-module ((gnu packages compression) #:hide (zip))
#:use-module (gnu packages guile) #:use-module (gnu packages guile)
#:use-module (gnu packages base) #:use-module (gnu packages base)
#:autoload (gnu packages appimage) (appimage-type2-runtime)
#:autoload (gnu packages gnupg) (guile-gcrypt) #:autoload (gnu packages gnupg) (guile-gcrypt)
#:autoload (gnu packages guile) (guile2.0-json guile-json) #:autoload (gnu packages guile) (guile2.0-json guile-json)
#:use-module (srfi srfi-1) #:use-module (srfi srfi-1)
@ -64,6 +67,7 @@ (define-module (guix scripts pack)
#:use-module (srfi srfi-35) #:use-module (srfi srfi-35)
#:use-module (srfi srfi-37) #:use-module (srfi srfi-37)
#:use-module (ice-9 match) #:use-module (ice-9 match)
#:use-module (ice-9 optargs)
#:export (symlink-spec-option-parser #:export (symlink-spec-option-parser
self-contained-tarball self-contained-tarball
@ -71,6 +75,7 @@ (define-module (guix scripts pack)
rpm-archive rpm-archive
docker-image docker-image
squashfs-image squashfs-image
self-contained-appimage
%formats %formats
guix-pack)) guix-pack))
@ -974,7 +979,99 @@ (define signature
(gexp->derivation (string-append name ".rpm") build (gexp->derivation (string-append name ".rpm") build
#:target target #:target target
#:references-graphs `(("profile" ,profile)))) #:references-graphs `(("profile" ,profile))))
;;;
;;; AppImage format
;;;
(define* (self-contained-appimage name profile
#:key target
(profile-name "guix-profile")
entry-point
(compressor (lookup-compressor "zstd"))
localstatedir?
(symlinks '())
(archiver tar)
(extra-options '()))
"Return a self-contained AppImage containing a store initialized with the
closure of PROFILE, a derivation. The AppImage contains /gnu/store unless
RELOCATABLE option is used; if LOCALSTATEDIR? is true, it also contains
/var/guix, including /var/guix/db with a properly initialized store database.
SYMLINKS must be a list of (SOURCE -> TARGET) tuples denoting symlinks to be
added to the pack."
(unless entry-point
(leave (G_ "entry-point must be provided in the '~a' format~%")
'appimage))
(let-keywords extra-options #f ((relocatable? #f))
(unless relocatable?
(warning (G_ "AppImages should be built with the --relocatable flag~%"))))
(define runtime-package appimage-type2-runtime)
(define runtime-path "bin/runtime-fuse3")
(define %valid-compressors '("gzip" "zstd"))
(let ((compressor-name (compressor-name compressor)))
(unless (member compressor-name %valid-compressors)
(leave (G_ "~a is not a valid squashfs archive compressor used in
generating the AppImage. Valid compressors are: ~a~%")
compressor-name
%valid-compressors)))
(define builder
(with-extensions (list guile-gcrypt)
(with-imported-modules (source-module-closure
'((guix build store-copy)
(guix build utils))
#:select? not-config?)
#~(begin
(use-modules (guix build utils)
(guix build store-copy)
(rnrs io ports)
(srfi srfi-1)
(srfi srfi-26))
(define (concatenate-files result file1 file2)
"Creates a new file RESULT containing FILE1 followed by FILE2."
(call-with-output-file result
(lambda (output)
(call-with-input-file file1
(lambda (input)
(dump-port input output)))
(call-with-input-file file2
(lambda (input)
(dump-port input output))))))
(let* ((appdir "AppDir")
(squashfs "squashfs")
(profile-items (map store-info-item
(call-with-input-file "profile" read-reference-graph)))
(profile (find (lambda (item)
(string-suffix? "-profile" item))
profile-items)))
(mkdir-p appdir)
;; Copy all store items from the profile to the AppDir.
(populate-store '("profile") appdir)
;; Symlink the provided entry-point to AppDir/AppRun.
(symlink (string-append "." profile "/" #$entry-point)
(string-append appdir "/AppRun"))
;; Create .desktop file as required by the spec.
(make-desktop-entry-file
(string-append appdir "/" #$name ".desktop")
#:name #$name
#:exec #$entry-point)
;; Compress the AppDir.
(invoke #+(file-append squashfs-tools "/bin/mksquashfs") appdir
squashfs "-root-owned" "-noappend"
"-comp" #+(compressor-name compressor))
;; Append runtime and squashFS into file AppImage.
(concatenate-files #$output
#$(file-append runtime-package "/" runtime-path)
squashfs)
;; Add execution permission.
(chmod #$output #o555))))))
(gexp->derivation (string-append name ".AppImage") builder
#:target target
#:references-graphs `(("profile" ,profile))))
;;; ;;;
;;; Compiling C programs. ;;; Compiling C programs.
@ -1311,6 +1408,7 @@ (define %formats
(squashfs . ,squashfs-image) (squashfs . ,squashfs-image)
(docker . ,docker-image) (docker . ,docker-image)
(deb . ,debian-archive) (deb . ,debian-archive)
(appimage . ,self-contained-appimage)
(rpm . ,rpm-archive))) (rpm . ,rpm-archive)))
(define (show-formats) (define (show-formats)
@ -1327,6 +1425,8 @@ (define (show-formats)
deb Debian archive installable via dpkg/apt")) deb Debian archive installable via dpkg/apt"))
(display (G_ " (display (G_ "
rpm RPM archive installable via rpm/yum")) rpm RPM archive installable via rpm/yum"))
(display (G_ "
appimage AppImage self-contained and executable format"))
(newline)) (newline))
(define (required-option symbol) (define (required-option symbol)
@ -1694,6 +1794,8 @@ (define (process-file-arg opts name)
(process-file-arg opts 'preun-file) (process-file-arg opts 'preun-file)
#:postun-file #:postun-file
(process-file-arg opts 'postun-file))) (process-file-arg opts 'postun-file)))
('appimage
(list #:relocatable? relocatable?))
(_ '()))) (_ '())))
(target (assoc-ref opts 'target)) (target (assoc-ref opts 'target))
(bootstrap? (assoc-ref opts 'bootstrap?)) (bootstrap? (assoc-ref opts 'bootstrap?))

View file

@ -3,6 +3,7 @@
;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net> ;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net>
;;; Copyright © 2021, 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com> ;;; Copyright © 2021, 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;; Copyright © 2023 Oleg Pykhalov <go.wigust@gmail.com> ;;; Copyright © 2023 Oleg Pykhalov <go.wigust@gmail.com>
;;; Copyright © 2024 Noé Lopez <noelopez@free.fr>
;;; ;;;
;;; This file is part of GNU Guix. ;;; This file is part of GNU Guix.
;;; ;;;
@ -32,7 +33,8 @@ (define-module (test-pack)
#:use-module (guix utils) #:use-module (guix utils)
#:use-module ((guix build utils) #:select (%store-directory)) #:use-module ((guix build utils) #:select (%store-directory))
#:use-module (gnu packages) #:use-module (gnu packages)
#:use-module ((gnu packages base) #:select (libc-utf8-locales-for-target)) #:use-module ((gnu packages base) #:select (libc-utf8-locales-for-target
hello))
#:use-module (gnu packages bootstrap) #:use-module (gnu packages bootstrap)
#:use-module ((gnu packages package-management) #:select (rpm)) #:use-module ((gnu packages package-management) #:select (rpm))
#:use-module ((gnu packages compression) #:select (squashfs-tools)) #:use-module ((gnu packages compression) #:select (squashfs-tools))
@ -340,6 +342,42 @@ (define bin
(mkdir #$output)))))))) (mkdir #$output))))))))
(built-derivations (list check)))) (built-derivations (list check))))
(unless store (test-skip 1))
(test-assertm "appimage"
(mlet* %store-monad
((guile (set-guile-for-build (default-guile)))
(profile -> (profile
(content (packages->manifest (list %bootstrap-guile hello)))
(hooks '())
(locales? #f)))
(image (self-contained-appimage "hello-appimage" profile
#:entry-point "bin/hello"
#:extra-options
(list #:relocatable? #t)))
(check (gexp->derivation
"check-appimage"
#~(invoke #$image))))
(built-derivations (list check))))
(unless store (test-skip 1))
(test-assertm "appimage + localstatedir"
(mlet* %store-monad
((guile (set-guile-for-build (default-guile)))
(profile -> (profile
(content (packages->manifest (list %bootstrap-guile hello)))
(hooks '())
(locales? #f)))
(image (self-contained-appimage "hello-appimage" profile
#:entry-point "bin/hello"
#:localstatedir? #t
#:extra-options
(list #:relocatable? #t)))
(check (gexp->derivation
"check-appimage"
#~(begin
(invoke #$image)))))
(built-derivations (list check))))
(unless store (test-skip 1)) (unless store (test-skip 1))
(test-assertm "deb archive with symlinks and control files" (test-assertm "deb archive with symlinks and control files"
(mlet* %store-monad (mlet* %store-monad