Toggle diff (375 lines)
diff --git a/.dir-locals.el b/.dir-locals.el
index 8f07a08eb5..a4fcbfe7ca 100644
(eval . (put 'origin 'scheme-indent-function 0))
(eval . (put 'build-system 'scheme-indent-function 0))
(eval . (put 'bag 'scheme-indent-function 0))
+ (eval . (put 'gexp->derivation 'scheme-indent-function 1))
(eval . (put 'graft 'scheme-indent-function 0))
(eval . (put 'operating-system 'scheme-indent-function 0))
(eval . (put 'file-system 'scheme-indent-function 0))
diff --git a/doc/guix.texi b/doc/guix.texi
index 0930a514c7..7fb8d8e9d2 100644
@@ -6016,6 +6016,11 @@ This produces a SquashFS image containing all the specified binaries and
symlinks, as well as empty mount points for virtual file systems like
+This produces a Debian archive (a package with the @samp{.deb} file
+extension) containing all the specified binaries and symlinks, that can
+be installed on top of any dpkg-based GNU/Linux distribution.
Singularity @emph{requires} you to provide @file{/bin/sh} in the image.
For that reason, @command{guix pack -f squashfs} always implies @code{-S
diff --git a/guix/scripts/pack.scm b/guix/scripts/pack.scm
index 8a108b7a1a..18f003dec0 100644
--- a/guix/scripts/pack.scm
+++ b/guix/scripts/pack.scm
;;; Copyright © 2018 Efraim Flashner <efraim@flashner.co.il>
;;; Copyright © 2020 Tobias Geerinckx-Rice <me@tobias.gr>
;;; Copyright © 2020 Eric Bavier <bavier@posteo.net>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;; This file is part of GNU Guix.
@@ -341,6 +343,10 @@ added to the pack."
#:references-graphs `(("profile" ,profile))))
(define (singularity-environment-file profile)
"Return a shell script that defines the environment variables corresponding
to the search paths of PROFILE."
@@ -367,6 +373,10 @@ to the search paths of PROFILE."
(computed-file "singularity-environment.sh" build))
+;;; SquashFS image format.
(define* (squashfs-image name profile
(profile-name "guix-profile")
@@ -541,6 +551,10 @@ added to the pack."
#:references-graphs `(("profile" ,profile))))
+;;; Docker image format.
(define* (docker-image name profile
(profile-name "guix-profile")
@@ -628,6 +642,165 @@ the image."
#:references-graphs `(("profile" ,profile))))
+;;; Debian archive format.
+;;; TODO: When relocatable option is selected, install to a unique prefix.
+;;; This would enable installation of multiple deb packs with conflicting
+;;; files at the same time.
+;;; TODO: Allow passing a custom control file from the CLI.
+;;; TODO: Allow providing a postinst script.
+(define* (debian-archive name profile
+ (profile-name "guix-profile")
+ (compressor (first %compressors))
+ "Return a Debian archive (.deb) containing a store initialized with the
+closure of PROFILE, a derivation. The archive contains /gnu/store; if
+LOCALSTATEDIR? is true, it also contains /var/guix, including /var/guix/db
+with a properly initialized store database. The supported compressors are
+\"none\", \"gz\" or \"xz\".
+SYMLINKS must be a list of (SOURCE -> TARGET) tuples denoting symlinks to be
+ ;; For simplicity, limit the supported compressors to the superset of
+ ;; compressors able to compress both the control file (gz or xz) and the
+ ;; data tarball (gz, bz2 or xz).
+ (define %valid-compressors '("gzip" "xz" "none"))
+ (let ((compressor-name (compressor-name compressor)))
+ (unless (member compressor-name %valid-compressors)
+ (leave (G_ "~a is not a valid Debian archive compressor. \
+Valid compressors are: ~a~%") compressor-name %valid-compressors)))
+ (warning (G_ "entry point not supported in the '~a' format~%")
+ (computed-file (string-append "data.tar"
+ (compressor-extension compressor))
+ (self-contained-tarball/builder
+ #:profile-name profile-name
+ #:compressor compressor
+ #:localstatedir? localstatedir?
+ #:local-build? #f ;allow offloading
+ #:options (list #:references-graphs `(("profile" ,profile))
+ (with-extensions (list guile-gcrypt)
+ (with-imported-modules `(((guix config) => ,(make-config.scm))
+ ,@(source-module-closure
+ #:select? not-config?))
+ (use-modules (guix build pack)
+ ;; Extract the machine type from the specified target, else from the
+ (and=> (or #$target %host-type) (lambda (triplet)
+ (first (string-split triplet #\-)))))
+ (define (gnu-machine-type->debian-machine-type type)
+ "Translate machine TYPE from the GNU to Debian terminology."
+ ;; Debian has its own jargon, different from the one used in GNU, for
+ ;; machine types (see data/cputable in the sources of dpkg).
+ ("mipsisa32r6" "mipsr6")
+ ("mipsisa32r6el" "mipsr6el")
+ ("mipsisa64r6" "mips64r6")
+ ("mipsisa64r6el" "mips64r6el")
+ ("powerpcle" "powerpcel")
+ ("powerpc64le" "ppc64el")
+ (gnu-machine-type->debian-machine-type machine-type))
+ #$define-manifest->friendly-name
+ (define manifest (profile-manifest #$profile))
+ (define single-entry ;manifest entry
+ (match (manifest-entries manifest)
+ (define package-name (or (and=> single-entry manifest-entry-name)
+ (manifest->friendly-name manifest)))
+ (define package-version
+ (or (and=> single-entry manifest-entry-version)
+ (define debian-format-version "2.0")
+ ;; Generate the debian-binary file.
+ (call-with-output-file "debian-binary"
+ (format port "~a~%" debian-format-version)))
+ (define data-tarball-file-name (strip-store-file-name
+ (copy-file #+data-tarball data-tarball-file-name)
+ (define control-tarball-file-name
+ (string-append "control.tar"
+ #$(compressor-extension compressor)))
+ ;; Write the compressed control tarball. Only the control file is
+ ;; mandatory (see: 'man deb' and 'man deb-control').
+ (call-with-output-file "control"
+Description: Debian archive generated by GNU Guix.
+~%" package-name package-version architecture)))
+ (define tar (string-append #+archiver "/bin/tar"))
+ #:compressor '#+(and=> compressor compressor-command))
+ "-cvf" ,control-tarball-file-name
+ ;; Create the .deb archive using GNU ar.
+ (invoke (string-append #+binutils "/bin/ar") "-rv" #$output
+ control-tarball-file-name data-tarball-file-name)))))
+ (gexp->derivation (string-append name ".deb")
+ #:references-graphs `(("profile" ,profile))))
;;; Compiling C programs.
@@ -960,7 +1133,8 @@ last resort for relocation."
;; Supported pack formats.
`((tarball . ,self-contained-tarball)
(squashfs . ,squashfs-image)
- (docker . ,docker-image)))
+ (docker . ,docker-image)
+ (deb . ,debian-archive)))
;; Print the supported pack formats.
@@ -972,6 +1146,8 @@ last resort for relocation."
squashfs Squashfs image suitable for Singularity"))
docker Tarball ready for 'docker load'"))
+ deb Debian archive compatible, installable via dpkg/apt"))
diff --git a/tests/pack.scm b/tests/pack.scm
index ae6247a1d5..ed461c6887 100644
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;; This file is part of GNU Guix.
#:use-module ((gnu packages base) #:select (glibc-utf8-locales))
#:use-module (gnu packages bootstrap)
#:use-module ((gnu packages compression) #:select (squashfs-tools))
+ #:use-module ((gnu packages debian) #:select (dpkg))
#:use-module ((gnu packages guile) #:select (guile-sqlite3))
#:use-module ((gnu packages gnupg) #:select (guile-gcrypt))
#:use-module (srfi srfi-64))
(define %tar-bootstrap %bootstrap-coreutils&co)
+(define %ar-bootstrap %bootstrap-binutils)
(pk 'guilelink (readlink "bin"))))
+ (built-derivations (list check))))
+ (unless store (test-skip 1))
+ (test-assertm "deb archive with symlinks" store
+ ((guile (set-guile-for-build (default-guile)))
+ (profile (profile-derivation (packages->manifest
+ (list %bootstrap-guile))
+ (deb (debian-archive "deb-pack" profile
+ #:compressor %gzip-compressor
+ #:symlinks '(("/opt/gnu/bin" -> "bin"))
+ #:archiver %tar-bootstrap))
+ (gexp->derivation "check-deb-pack"
+ (with-imported-modules '((guix build utils))
+ (use-modules (guix build utils)
+ (setenv "PATH" (string-join
+ (list (string-append #+%tar-bootstrap "/bin")
+ (string-append #+dpkg "/bin")
+ (string-append #+%ar-bootstrap "/bin"))
+ ;; Validate the output of 'dpkg --info'.
+ (let* ((port (open-pipe* OPEN_READ "dpkg" "--info" #$deb))
+ (info (get-string-all port))
+ (exit-val (status:exit-val (close-pipe port))))
+ (assert (zero? exit-val))
+ (assert (string-contains
+ (string-append "Package: "
+ #+(package-name %bootstrap-guile))))
+ (assert (string-contains
+ (string-append "Version: "
+ #+(package-version %bootstrap-guile)))))
+ ;; Sanity check .deb contents.
+ (invoke "ar" "-xv" #$deb)
+ (assert (file-exists? "debian-binary"))
+ (assert (file-exists? "data.tar.gz"))
+ (assert (file-exists? "control.tar.gz"))
+ ;; Verify there are no hard links in data.tar.gz, as hard
+ ;; links would cause dpkg to fail unpacking the archive.
+ (let ((port (open-pipe* OPEN_READ "tar" "-tvf" "data.tar.gz")))
+ (let loop ((hard-links '()))
+ (match (pk 'line (read-line port))
+ (assert (zero? (status:exit-val (close-pipe port))))
+ (if (string-prefix? "u" line)
+ (loop (cons line hard-links))
+ (loop hard-links)))))))
+ (unless (null? hard-links)
+ (error "hard links found in data.tar.gz" hard-links))
(built-derivations (list check)))))