summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaxim Cournoyer <maxim.cournoyer@gmail.com>2023-01-21 15:04:09 -0500
committerMaxim Cournoyer <maxim.cournoyer@gmail.com>2023-02-19 21:13:23 -0500
commit598f4c509bbfec2b983a8ee246cce0a0fe45ec7f (patch)
tree1bf799843929f714428c27eb2325c255a92793f3
parentac1d530d56c1a259630c8873b2281033878a4acb (diff)
pack: Add RPM format.
* guix/rpm.scm: New file. * guix/scripts/pack.scm (rpm-archive): New procedure. (%formats): Register it. (show-formats): Add it. (guix-pack): Register supported extra-options for the rpm format. * tests/pack.scm (rpm-for-tests): New variable. ("rpm archive can be installed/uninstalled"): New test. * tests/rpm.scm: New test. * doc/guix.texi (Invoking guix pack): Document it.
-rw-r--r--Makefile.am2
-rw-r--r--doc/guix.texi46
-rw-r--r--guix/rpm.scm623
-rw-r--r--guix/scripts/pack.scm230
-rw-r--r--tests/pack.scm57
-rw-r--r--tests/rpm.scm86
6 files changed, 1031 insertions, 13 deletions
diff --git a/Makefile.am b/Makefile.am
index 5ce6cc84f4c..8e3815b9c22 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -111,6 +111,7 @@ MODULES = \
111 guix/derivations.scm \ 111 guix/derivations.scm \
112 guix/grafts.scm \ 112 guix/grafts.scm \
113 guix/repl.scm \ 113 guix/repl.scm \
114 guix/rpm.scm \
114 guix/transformations.scm \ 115 guix/transformations.scm \
115 guix/inferior.scm \ 116 guix/inferior.scm \
116 guix/describe.scm \ 117 guix/describe.scm \
@@ -535,6 +536,7 @@ SCM_TESTS = \
535 tests/pypi.scm \ 536 tests/pypi.scm \
536 tests/read-print.scm \ 537 tests/read-print.scm \
537 tests/records.scm \ 538 tests/records.scm \
539 tests/rpm.scm \
538 tests/scripts.scm \ 540 tests/scripts.scm \
539 tests/search-paths.scm \ 541 tests/search-paths.scm \
540 tests/services.scm \ 542 tests/services.scm \
diff --git a/doc/guix.texi b/doc/guix.texi
index 44e2165a82a..05615b95498 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -6896,6 +6896,7 @@ such file or directory'' message.
6896@end quotation 6896@end quotation
6897 6897
6898@item deb 6898@item deb
6899@cindex Debian, build a .deb package with guix pack
6899This produces a Debian archive (a package with the @samp{.deb} file 6900This produces a Debian archive (a package with the @samp{.deb} file
6900extension) containing all the specified binaries and symbolic links, 6901extension) containing all the specified binaries and symbolic links,
6901that can be installed on top of any dpkg-based GNU(/Linux) distribution. 6902that can be installed on top of any dpkg-based GNU(/Linux) distribution.
@@ -6912,7 +6913,8 @@ guix pack -f deb -C xz -S /usr/bin/hello=bin/hello hello
6912Because archives produced with @command{guix pack} contain a collection 6913Because archives produced with @command{guix pack} contain a collection
6913of store items and because each @command{dpkg} package must not have 6914of store items and because each @command{dpkg} package must not have
6914conflicting files, in practice that means you likely won't be able to 6915conflicting files, in practice that means you likely won't be able to
6915install more than one such archive on a given system. 6916install more than one such archive on a given system. You can
6917nonetheless pack as many Guix packages as you want in one such archive.
6916@end quotation 6918@end quotation
6917 6919
6918@quotation Warning 6920@quotation Warning
@@ -6923,6 +6925,48 @@ shared by other software, such as a Guix installation or other, non-deb
6923packs. 6925packs.
6924@end quotation 6926@end quotation
6925 6927
6928@item rpm
6929@cindex RPM, build an RPM archive with guix pack
6930This produces an RPM archive (a package with the @samp{.rpm} file
6931extension) containing all the specified binaries and symbolic links,
6932that can be installed on top of any RPM-based GNU/Linux distribution.
6933The RPM format embeds checksums for every file it contains, which the
6934@command{rpm} command uses to validate the integrity of the archive.
6935
6936Advanced RPM-related options are revealed via the
6937@option{--help-rpm-format} option. These options allow embedding
6938maintainer scripts that can run before or after the installation of the
6939RPM archive, for example.
6940
6941The RPM format supports relocatable packages via the @option{--prefix}
6942option of the @command{rpm} command, which can be handy to install an
6943RPM package to a specific prefix.
6944
6945@example
6946guix pack -f rpm -R -C xz -S /usr/bin/hello=bin/hello hello
6947@end example
6948
6949@example
6950sudo rpm --install --prefix=/opt /gnu/store/...-hello.rpm
6951@end example
6952
6953@quotation Note
6954Contrary to Debian packages, conflicting but @emph{identical} files in
6955RPM packages can be installed simultaneously, which means multiple
6956@command{guix pack}-produced RPM packages can usually be installed side
6957by side without any problem.
6958@end quotation
6959
6960@quotation Warning
6961@command{rpm} assumes ownership of any files contained in the pack,
6962which means it will remove @file{/gnu/store} upon uninstalling a
6963Guix-generated RPM package, unless the RPM package was installed with
6964the @option{--prefix} option of the @command{rpm} command. It is unwise
6965to install Guix-produced @samp{.rpm} packages on a system where
6966@file{/gnu/store} is shared by other software, such as a Guix
6967installation or other, non-rpm packs.
6968@end quotation
6969
6926@end table 6970@end table
6927 6971
6928@cindex relocatable binaries 6972@cindex relocatable binaries
diff --git a/guix/rpm.scm b/guix/rpm.scm
new file mode 100644
index 00000000000..1cb8326a9b2
--- /dev/null
+++ b/guix/rpm.scm
@@ -0,0 +1,623 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (guix rpm)
20 #:autoload (gcrypt hash) (hash-algorithm file-hash md5)
21 #:use-module (guix build utils)
22 #:use-module (ice-9 format)
23 #:use-module (ice-9 match)
24 #:use-module (ice-9 textual-ports)
25 #:use-module (rnrs bytevectors)
26 #:use-module (srfi srfi-1)
27 #:use-module (srfi srfi-9)
28 #:use-module (srfi srfi-26)
29 #:use-module (srfi srfi-71)
30 #:use-module (srfi srfi-171)
31 #:export (generate-lead
32 generate-signature
33 generate-header
34 assemble-rpm-metadata
35
36 ;; XXX: These are internals, but the inline disabling trick
37 ;; doesn't work on them.
38 make-header-entry
39 header-entry?
40 header-entry-tag
41 header-entry-count
42 header-entry-value
43
44 bytevector->hex-string
45
46 fhs-directory?))
47
48;;; Commentary:
49;;;
50;;; This module provides the building blocks required to construct RPM
51;;; archives. It is intended to be importable on the build side, so shouldn't
52;;; depend on (guix diagnostics) or other host-side-only modules.
53;;;
54;;; Code:
55
56(define (gnu-system-triplet->machine-type triplet)
57 "Return the machine component of TRIPLET, a GNU system triplet."
58 (first (string-split triplet #\-)))
59
60(define (gnu-machine-type->rpm-arch type)
61 "Return the canonical RPM architecture string, given machine TYPE."
62 (match type
63 ("arm" "armv7hl")
64 ("powerpc" "ppc")
65 ("powerpc64le" "ppc64le")
66 (machine machine))) ;unchanged
67
68(define (gnu-machine-type->rpm-number type)
69 "Translate machine TYPE to its corresponding RPM integer value."
70 ;; Refer to the rpmrc.in file in the RPM source for the complete
71 ;; translation tables.
72 (match type
73 ((or "i486" "i586" "i686" "x86_64") 1)
74 ((? (cut string-prefix? "powerpc" <>)) 5)
75 ("mips64el" 11)
76 ((? (cut string-prefix? "arm" <>)) 12)
77 ("aarch64" 19)
78 ((? (cut string-prefix? "riscv" <>)) 22)
79 (_ (error "no RPM number known for machine type" type))))
80
81(define (u16-number->u8-list number)
82 "Return a list of byte values made of NUMBER, a 16 bit unsigned integer."
83 (let ((bv (uint-list->bytevector (list number) (endianness big) 2)))
84 (bytevector->u8-list bv)))
85
86(define (u32-number->u8-list number)
87 "Return a list of byte values made of NUMBER, a 32 bit unsigned integer."
88 (let ((bv (uint-list->bytevector (list number) (endianness big) 4)))
89 (bytevector->u8-list bv)))
90
91(define (s32-number->u8-list number)
92 "Return a list of byte values made of NUMBER, a 32 bit signed integer."
93 (let ((bv (sint-list->bytevector (list number) (endianness big) 4)))
94 (bytevector->u8-list bv)))
95
96(define (u8-list->u32-number lst)
97 "Return the 32 bit unsigned integer corresponding to the 4 bytes in LST."
98 (bytevector-u32-ref (u8-list->bytevector lst) 0 (endianness big)))
99
100
101;;;
102;;; Lead section.
103;;;
104
105;; Refer to the docs/manual/format.md file of the RPM source for the details
106;; regarding the binary format of an RPM archive.
107(define* (generate-lead name-version #:key (target %host-type))
108 "Generate a RPM lead u8-list that uses NAME-VERSION, the name and version
109string of the package, and TARGET, a GNU triplet used to derive the target
110machine type."
111 (define machine-type (gnu-system-triplet->machine-type target))
112 (define magic (list #xed #xab #xee #xdb))
113 (define file-format-version (list 3 0)) ;3.0
114 (define type (list 0 0)) ;0 for binary packages
115 (define arch-number (u16-number->u8-list
116 (gnu-machine-type->rpm-number machine-type)))
117 ;; The 66 bytes from 10 to 75 are for the name-version-release string.
118 (define name
119 (let ((padding-bytes (make-list (- 66 (string-length name-version)) 0)))
120 (append (bytevector->u8-list (string->utf8 name-version))
121 padding-bytes)))
122 ;; There is no OS number corresponding to GNU/Hurd (GNU), only Linux, per
123 ;; rpmrc.in.
124 (define os-number (list 0 1))
125
126 ;; For RPM format 3.0, the signature type is 5, which means a "Header-style"
127 ;; signature.
128 (define signature-type (list 0 5))
129
130 (define reserved-bytes (make-list 16 0))
131
132 (append magic file-format-version type arch-number name
133 os-number signature-type reserved-bytes))
134
135
136;;;
137;;; Header section.
138;;;
139
140(define header-magic (list #x8e #xad #xe8))
141(define header-version (list 1))
142(define header-reserved (make-list 4 0)) ;4 reserved bytes
143;;; Every header starts with 8 bytes made by the header magic number, the
144;;; header version and 4 reserved bytes.
145(define header-intro (append header-magic header-version header-reserved))
146
147;;; Header entry data types.
148(define NULL 0)
149(define CHAR 1)
150(define INT8 2)
151(define INT16 3) ;2-bytes aligned
152(define INT32 4) ;4-bytes aligned
153(define INT64 5) ;8-bytes aligned
154(define STRING 6)
155(define BIN 7)
156(define STRING_ARRAY 8)
157(define I18NSTRIN_TYPE 9)
158
159;;; Header entry tags.
160(define-record-type <rpm-tag>
161 (make-rpm-tag number type)
162 rpm-tag?
163 (number rpm-tag-number)
164 (type rpm-tag-type))
165
166;;; The following are internal tags used to identify the data sections.
167(define RPMTAG_HEADERSIGNATURES (make-rpm-tag 62 BIN)) ;signature header
168(define RPMTAG_HEADERIMMUTABLE (make-rpm-tag 63 BIN)) ;main/data header
169(define RPMTAG_HEADERI18NTABLE (make-rpm-tag 100 STRING_ARRAY))
170
171;;; Subset of RPM tags from include/rpm/rpmtag.h.
172(define RPMTAG_NAME (make-rpm-tag 1000 STRING))
173(define RPMTAG_VERSION (make-rpm-tag 1001 STRING))
174(define RPMTAG_RELEASE (make-rpm-tag 1002 STRING))
175(define RPMTAG_SUMMARY (make-rpm-tag 1004 STRING))
176(define RPMTAG_SIZE (make-rpm-tag 1009 INT32))
177(define RPMTAG_LICENSE (make-rpm-tag 1014 STRING))
178(define RPMTAG_OS (make-rpm-tag 1021 STRING))
179(define RPMTAG_ARCH (make-rpm-tag 1022 STRING))
180(define RPMTAG_PREIN (make-rpm-tag 1023 STRING))
181(define RPMTAG_POSTIN (make-rpm-tag 1024 STRING))
182(define RPMTAG_PREUN (make-rpm-tag 1025 STRING))
183(define RPMTAG_POSTUN (make-rpm-tag 1026 STRING))
184(define RPMTAG_FILESIZES (make-rpm-tag 1028 INT32))
185(define RPMTAG_FILEMODES (make-rpm-tag 1030 INT16))
186(define RPMTAG_FILEDIGESTS (make-rpm-tag 1035 STRING_ARRAY))
187(define RPMTAG_FILELINKTOS (make-rpm-tag 1036 STRING_ARRAY))
188(define RPMTAG_FILEUSERNAME (make-rpm-tag 1039 STRING_ARRAY))
189(define RPMTAG_GROUPNAME (make-rpm-tag 1040 STRING_ARRAY))
190(define RPMTAG_PREFIXES (make-rpm-tag 1098 STRING_ARRAY))
191(define RPMTAG_DIRINDEXES (make-rpm-tag 1116 INT32))
192(define RPMTAG_BASENAMES (make-rpm-tag 1117 STRING_ARRAY))
193(define RPMTAG_DIRNAMES (make-rpm-tag 1118 STRING_ARRAY))
194(define RPMTAG_PAYLOADFORMAT (make-rpm-tag 1124 STRING))
195(define RPMTAG_PAYLOADCOMPRESSOR (make-rpm-tag 1125 STRING))
196(define RPMTAG_LONGFILESIZES (make-rpm-tag 5008 INT64))
197(define RPMTAG_LONGSIZE (make-rpm-tag 5009 INT64))
198;;; The algorithm used to compute the digest of each file, e.g. RPM_HASH_MD5.
199(define RPMTAG_FILEDIGESTALGO (make-rpm-tag 5011 INT32))
200;;; RPMTAG_ENCODING specifies the encoding used for strings, e.g. "utf-8".
201(define RPMTAG_ENCODING (make-rpm-tag 5062 STRING))
202;;; Compressed payload digest. Its type is a string array, but currently in
203;;; practice it is equivalent to STRING, since only the first element is used.
204(define RPMTAG_PAYLOADDIGEST (make-rpm-tag 5092 STRING_ARRAY))
205;;; The algorithm used to compute the payload digest, e.g. RPM_HASH_SHA256.
206(define RPMTAG_PAYLOADDIGESTALGO (make-rpm-tag 5093 INT32))
207;;; The following are taken from the rpmHashAlgo_e enum in rpmcrypto.h.
208(define RPM_HASH_MD5 1)
209(define RPM_HASH_SHA256 8)
210
211;;; Other useful internal definitions.
212(define REGION_TAG_COUNT 16) ;number of bytes
213(define INT32_MAX (1- (expt 2 32))) ;4294967295 bytes (unsigned)
214
215(define (rpm-tag->u8-list tag)
216 "Return the u8 list corresponding to RPM-TAG, a <rpm-tag> object."
217 (append (u32-number->u8-list (rpm-tag-number tag))
218 (u32-number->u8-list (rpm-tag-type tag))))
219
220(define-record-type <header-entry>
221 (make-header-entry tag count value)
222 header-entry?
223 (tag header-entry-tag) ;<rpm-tag>
224 (count header-entry-count) ;number (u32)
225 (value header-entry-value)) ;string|number|list|...
226
227(define (entry-type->alignement type)
228 "Return the byte alignment of TYPE, an RPM header entry type."
229 (cond ((= INT16 type) 2)
230 ((= INT32 type) 4)
231 ((= INT64 type) 8)
232 (else 1)))
233
234(define (next-aligned-offset offset alignment)
235 "Return the next position from OFFSET which satisfies ALIGNMENT."
236 (if (= 0 (modulo offset alignment))
237 offset
238 (next-aligned-offset (1+ offset) alignment)))
239
240(define (header-entry->data entry)
241 "Return the data of ENTRY, a <header-entry> object, as a u8 list."
242 (let* ((tag (header-entry-tag entry))
243 (count (header-entry-count entry))
244 (value (header-entry-value entry))
245 (number (rpm-tag-number tag))
246 (type (rpm-tag-type tag)))
247 (cond
248 ((= STRING type)
249 (unless (string? value)
250 (error "expected string value for STRING type, got" value))
251 (unless (= 1 count)
252 (error "count must be 1 for STRING type"))
253 (let ((value (cond ((= (rpm-tag-number RPMTAG_VERSION) number)
254 ;; Hyphens are not allowed in version strings.
255 (string-map (match-lambda
256 (#\- #\+)
257 (c c))
258 value))
259 (else value))))
260 (append (bytevector->u8-list (string->utf8 value))
261 (list 0)))) ;strings must end with null byte
262 ((= STRING_ARRAY type)
263 (unless (list? value)
264 (error "expected a list of strings for STRING_ARRAY type, got" value))
265 (unless (= count (length value))
266 (error "expected count to be equal to" (length value) 'got count))
267 (append-map (lambda (s)
268 (append (bytevector->u8-list (string->utf8 s))
269 (list 0))) ;null byte separated
270 value))
271 ((member type (list INT8 INT16 INT32))
272 (if (= 1 count)
273 (unless (number? value)
274 (error "expected number value for scalar INT type; got" value))
275 (unless (list? value)
276 (error "expected list value for array INT type; got" value)))
277 (if (list? value)
278 (cond ((= INT8 type) value)
279 ((= INT16 type) (append-map u16-number->u8-list value))
280 ((= INT32 type) (append-map u32-number->u8-list value))
281 (else (error "unexpected type" type)))
282 (cond ((= INT8 type) (list value))
283 ((= INT16 type) (u16-number->u8-list value))
284 ((= INT32 type) (u32-number->u8-list value))
285 (else (error "unexpected type" type)))))
286 ((= BIN type)
287 (unless (list? value)
288 (error "expected list value for BIN type; got" value))
289 value)
290 (else (error "unimplemented type" type)))))
291
292(define (make-header-index+data entries)
293 "Return the index and data sections as u8 number lists, via multiple values.
294An index is composed of four u32 (16 bytes total) quantities, in order: tag,
295type, offset and count."
296 (match (fold (match-lambda*
297 ((entry (offset . (index . data)))
298 (let* ((tag (header-entry-tag entry))
299 (tag-number (rpm-tag-number tag))
300 (tag-type (rpm-tag-type tag))
301 (count (header-entry-count entry))
302 (data* (header-entry->data entry))
303 (alignment (entry-type->alignement tag-type))
304 (aligned-offset (next-aligned-offset offset alignment))
305 (padding (make-list (- aligned-offset offset) 0)))
306 (cons (+ aligned-offset (length data*))
307 (cons (append index
308 (u32-number->u8-list tag-number)
309 (u32-number->u8-list tag-type)
310 (u32-number->u8-list aligned-offset)
311 (u32-number->u8-list count))
312 (append data padding data*))))))
313 '(0 . (() . ()))
314 entries)
315 ((offset . (index . data))
316 (values index data))))
317
318;; Prevent inlining of the variables/procedures accessed by unit tests.
319(set! make-header-index+data make-header-index+data)
320(set! RPMTAG_ARCH RPMTAG_ARCH)
321(set! RPMTAG_LICENSE RPMTAG_LICENSE)
322(set! RPMTAG_NAME RPMTAG_NAME)
323(set! RPMTAG_OS RPMTAG_OS)
324(set! RPMTAG_RELEASE RPMTAG_RELEASE)
325(set! RPMTAG_SUMMARY RPMTAG_SUMMARY)
326(set! RPMTAG_VERSION RPMTAG_VERSION)
327
328(define (wrap-in-region-tags header region-tag)
329 "Wrap HEADER, a header provided as u8-list with REGION-TAG."
330 (let* ((type (rpm-tag-type region-tag))
331 (header-intro (take header 16))
332 (header-rest (drop header 16))
333 ;; Increment the existing index value to account for the added region
334 ;; tag index.
335 (index-length (1+ (u8-list->u32-number
336 (drop-right (drop header-intro 8) 4)))) ;bytes 8-11
337 ;; Increment the data length value to account for the added region
338 ;; tag data.
339 (data-length (+ REGION_TAG_COUNT
340 (u8-list->u32-number
341 (take-right header-intro 4))))) ;last 4 bytes of intro
342 (unless (member region-tag (list RPMTAG_HEADERSIGNATURES
343 RPMTAG_HEADERIMMUTABLE))
344 (error "expected RPMTAG_HEADERSIGNATURES or RPMTAG_HEADERIMMUTABLE, got"
345 region-tag))
346 (append (drop-right header-intro 8) ;strip existing index and data lengths
347 (u32-number->u8-list index-length)
348 (u32-number->u8-list data-length)
349 ;; Region tag (16 bytes).
350 (u32-number->u8-list (rpm-tag-number region-tag)) ;number
351 (u32-number->u8-list type) ;type
352 (u32-number->u8-list (- data-length REGION_TAG_COUNT)) ;offset
353 (u32-number->u8-list REGION_TAG_COUNT) ;count
354 ;; Immutable region.
355 header-rest
356 ;; Region tag trailer (16 bytes). Note: the trailer offset value
357 ;; is an enforced convention; it has no practical use.
358 (u32-number->u8-list (rpm-tag-number region-tag)) ;number
359 (u32-number->u8-list type) ;type
360 (s32-number->u8-list (* -1 index-length 16)) ;negative offset
361 (u32-number->u8-list REGION_TAG_COUNT)))) ;count
362
363(define (bytevector->hex-string bv)
364 (format #f "~{~2,'0x~}" (bytevector->u8-list bv)))
365
366(define (files->md5-checksums files)
367 "Return the MD5 checksums (formatted as hexadecimal strings) for FILES."
368 (let ((file-md5 (cut file-hash (hash-algorithm md5) <>)))
369 (map (lambda (f)
370 (or (and=> (false-if-exception (file-md5 f))
371 bytevector->hex-string)
372 ;; Only regular files (e.g., not directories) can have their
373 ;; checksum computed.
374 ""))
375 files)))
376
377(define (strip-leading-dot name)
378 "Remove the leading \".\" from NAME, if present. If a single \".\" is
379encountered, translate it to \"/\"."
380 (match name
381 ("." "/") ;special case
382 ((? (cut string-prefix? "." <>))
383 (string-drop name 1))
384 (x name)))
385
386;;; An extensive list of required and optional FHS directories, per its 3.0
387;;; revision.
388(define %fhs-directories
389 (list "/bin" "/boot" "/dev"
390 "/etc" "/etc/opt" "/etc/X11" "/etc/sgml" "/etc/xml"
391 "/home" "/root" "/lib" "/media" "/mnt"
392 "/opt" "/opt/bin" "/opt/doc" "/opt/include"
393 "/opt/info" "/opt/lib" "/opt/man"
394 "/run" "/sbin" "/srv" "/sys" "/tmp"
395 "/usr" "/usr/bin" "/usr/include" "/usr/libexec"
396 "/usr/share/color" "/usr/share/dict" "/usr/share/doc" "/usr/share/games"
397 "/usr/share/info" "/usr/share/locale" "/usr/share/man" "/usr/share/misc"
398 "/usr/share/nls" "/usr/share/ppd" "/usr/share/sgml"
399 "/usr/share/terminfo" "/usr/share/tmac" "/usr/share/xml"
400 "/usr/share/zoneinfo" "/usr/local" "/usr/local/bin" "/usr/local/etc"
401 "/usr/local/games" "/usr/local/include" "/usr/local/lib"
402 "/usr/local/man" "/usr/local/sbin" "/usr/local/sbin" "/usr/local/share"
403 "/usr/local/src" "/var" "/var/account" "/var/backups"
404 "/var/cache" "/var/cache/fonts" "/var/cache/man" "/var/cache/www"
405 "/var/crash" "/var/cron" "/var/games" "/var/mail" "/var/msgs"
406 "/var/lib" "/var/lib/color" "/var/lib/hwclock" "/var/lib/misc"
407 "/var/local" "/var/lock" "/var/log" "/var/opt" "/var/preserve"
408 "/var/run" "/var/spool" "/var/spool/lpd" "/var/spool/mqueue"
409 "/var/spool/news" "/var/spool/rwho" "/var/spool/uucp"
410 "/var/tmp" "/var/yp"))
411
412(define (fhs-directory? file-name)
413 "Predicate to check if FILE-NAME is a known File Hierarchy Standard (FHS)
414directory."
415 (member (strip-leading-dot file-name) %fhs-directories))
416
417(define (directory->file-entries directory)
418 "Return the file lists triplet header entries for the files found under
419DIRECTORY."
420 (with-directory-excursion directory
421 ;; Skip the initial "." directory, as its name would get concatenated with
422 ;; the "./" dirname and fail to match "." in the payload.
423 (let* ((files (cdr (find-files "." #:directories? #t)))
424 (file-stats (map lstat files))
425 (directories
426 (append (list ".")
427 (filter-map (match-lambda
428 ((index . file)
429 (let ((st (list-ref file-stats index)))
430 (and (eq? 'directory (stat:type st))
431 file))))
432 (list-transduce (tenumerate) rcons files))))
433 ;; Omit any FHS directories found in FILES to avoid the RPM package
434 ;; from owning them. This can occur when symlinks directives such
435 ;; as "/usr/bin/hello -> bin/hello" are used.
436 (package-files package-file-stats
437 (unzip2 (reverse
438 (fold (lambda (file stat res)
439 (if (fhs-directory? file)
440 res
441 (cons (list file stat) res)))
442 '() files file-stats))))
443
444 ;; When provided with the index of a file, the directory index must
445 ;; return the index of the corresponding directory entry.
446 (dirindexes (map (lambda (d)
447 (list-index (cut string=? <> d) directories))
448 (map dirname package-files)))
449 ;; The files owned are those appearing in 'basenames'; own them
450 ;; all.
451 (basenames (map basename package-files))
452 ;; The directory names must end with a trailing "/".
453 (dirnames (map (compose strip-leading-dot (cut string-append <> "/"))
454 directories))
455 ;; Note: All the file-related entries must have the same length as
456 ;; the basenames entry.
457 (symlink-targets (map (lambda (f)
458 (if (symbolic-link? f)
459 (readlink f)
460 "")) ;unused
461 package-files))
462 (file-modes (map stat:mode package-file-stats))
463 (file-sizes (map stat:size package-file-stats))
464 (file-md5s (files->md5-checksums package-files)))
465 (let ((basenames-length (length basenames))
466 (dirindexes-length (length dirindexes)))
467 (unless (= basenames-length dirindexes-length)
468 (error "length mismatch for dirIndexes; expected/actual"
469 basenames-length dirindexes-length))
470 (append
471 (if (> (apply max file-sizes) INT32_MAX)
472 (list (make-header-entry RPMTAG_LONGFILESIZES (length file-sizes)
473 file-sizes)
474 (make-header-entry RPMTAG_LONGSIZE 1
475 (reduce + 0 file-sizes)))
476 (list (make-header-entry RPMTAG_FILESIZES (length file-sizes)
477 file-sizes)
478 (make-header-entry RPMTAG_SIZE 1 (reduce + 0 file-sizes))))
479 (list
480 (make-header-entry RPMTAG_FILEMODES (length file-modes) file-modes)
481 (make-header-entry RPMTAG_FILEDIGESTS (length file-md5s) file-md5s)
482 (make-header-entry RPMTAG_FILEDIGESTALGO 1 RPM_HASH_MD5)
483 (make-header-entry RPMTAG_FILELINKTOS (length symlink-targets)
484 symlink-targets)
485 (make-header-entry RPMTAG_FILEUSERNAME basenames-length
486 (make-list basenames-length "root"))
487 (make-header-entry RPMTAG_GROUPNAME basenames-length
488 (make-list basenames-length "root"))
489 ;; The dirindexes, basenames and dirnames tags form the so-called RPM
490 ;; "path triplet".
491 (make-header-entry RPMTAG_DIRINDEXES dirindexes-length dirindexes)
492 (make-header-entry RPMTAG_BASENAMES basenames-length basenames)
493 (make-header-entry RPMTAG_DIRNAMES (length dirnames) dirnames)))))))
494
495(define (make-header entries)
496 "Return the u8 list of a RPM header containing ENTRIES, a list of
497<rpm-entry> objects."
498 (let* ((entries (sort entries (lambda (x y)
499 (< (rpm-tag-number (header-entry-tag x))
500 (rpm-tag-number (header-entry-tag y))))))
501 (count (length entries))
502 (index data (make-header-index+data entries)))
503 (append header-intro ;8 bytes
504 (u32-number->u8-list count) ;4 bytes
505 (u32-number->u8-list (length data)) ;4 bytes
506 ;; Now starts the header index, which can contain up to 32 entries
507 ;; of 16 bytes each.
508 index data)))
509
510(define* (generate-header name version
511 payload-digest
512 payload-directory
513 payload-compressor
514 #:key
515 relocatable?
516 prein-file postin-file
517 preun-file postun-file
518 (target %host-type)
519 (release "0")
520 (license "N/A")
521 (summary "RPM archive generated by GNU Guix.")
522 (os "Linux")) ;see rpmrc.in
523 "Return the u8 list corresponding to the Header section. PAYLOAD-DIGEST is
524the SHA256 checksum string of the compressed payload. PAYLOAD-DIRECTORY is
525the directory containing the payload files. PAYLOAD-COMPRESSOR is the name of
526the compressor used to compress the CPIO payload, such as \"none\", \"gz\",
527\"xz\" or \"zstd\"."
528 (let* ((rpm-arch (gnu-machine-type->rpm-arch
529 (gnu-system-triplet->machine-type target)))
530 (file->string (cut call-with-input-file <> get-string-all))
531 (prein-script (and=> prein-file file->string))
532 (postin-script (and=> postin-file file->string))
533 (preun-script (and=> preun-file file->string))
534 (postun-script (and=> postun-file file->string)))
535 (wrap-in-region-tags
536 (make-header (append
537 (list (make-header-entry RPMTAG_HEADERI18NTABLE 1 (list "C"))
538 (make-header-entry RPMTAG_NAME 1 name)
539 (make-header-entry RPMTAG_VERSION 1 version)
540 (make-header-entry RPMTAG_RELEASE 1 release)
541 (make-header-entry RPMTAG_SUMMARY 1 summary)
542 (make-header-entry RPMTAG_LICENSE 1 license)
543 (make-header-entry RPMTAG_OS 1 os)
544 (make-header-entry RPMTAG_ARCH 1 rpm-arch))
545 (directory->file-entries payload-directory)
546 (if relocatable?
547 ;; Note: RPMTAG_PREFIXES must not have a trailing
548 ;; slash, unless it's '/'. This allows installing the
549 ;; package via 'rpm -i --prefix=/tmp', for example.
550 (list (make-header-entry RPMTAG_PREFIXES 1 (list "/")))
551 '())
552 (if prein-script
553 (list (make-header-entry RPMTAG_PREIN 1 prein-script))
554 '())
555 (if postin-script
556 (list (make-header-entry RPMTAG_POSTIN 1 postin-script))
557 '())
558 (if preun-script
559 (list (make-header-entry RPMTAG_PREUN 1 preun-script))
560 '())
561 (if postun-script
562 (list (make-header-entry RPMTAG_POSTUN 1 postun-script))
563 '())
564 (if (string=? "none" payload-compressor)
565 '()
566 (list (make-header-entry RPMTAG_PAYLOADCOMPRESSOR 1
567 payload-compressor)))
568 (list (make-header-entry RPMTAG_ENCODING 1 "utf-8")
569 (make-header-entry RPMTAG_PAYLOADFORMAT 1 "cpio")
570 (make-header-entry RPMTAG_PAYLOADDIGEST 1
571 (list payload-digest))
572 (make-header-entry RPMTAG_PAYLOADDIGESTALGO 1
573 RPM_HASH_SHA256))))
574 RPMTAG_HEADERIMMUTABLE)))
575
576
577;;;
578;;; Signature section
579;;;
580
581;;; Header sha256 checksum.
582(define RPMSIGTAG_SHA256 (make-rpm-tag 273 STRING))
583;;; Uncompressed payload size.
584(define RPMSIGTAG_PAYLOADSIZE (make-rpm-tag 1007 INT32))
585;;; Header and compressed payload combined size.
586(define RPMSIGTAG_SIZE (make-rpm-tag 1000 INT32))
587;;; Uncompressed payload size (when size > max u32).
588(define RPMSIGTAG_LONGARCHIVESIZE (make-rpm-tag 271 INT64))
589;;; Header and compressed payload combined size (when size > max u32).
590(define RPMSIGTAG_LONGSIZE (make-rpm-tag 270 INT64))
591;;; Extra space reserved for signatures (typically 32 bytes).
592(define RPMSIGTAG_RESERVEDSPACE (make-rpm-tag 1008 BIN))
593
594(define (generate-signature header-sha256
595 header+compressed-payload-size
596 ;; uncompressed-payload-size
597 )
598 "Return the u8 list representing a signature header containing the
599HEADER-SHA256 (a string) and the PAYLOAD-SIZE, which is the combined size of
600the header and compressed payload."
601 (define size-tag (if (> header+compressed-payload-size INT32_MAX)
602 RPMSIGTAG_LONGSIZE
603 RPMSIGTAG_SIZE))
604 (wrap-in-region-tags
605 (make-header (list (make-header-entry RPMSIGTAG_SHA256 1 header-sha256)
606 (make-header-entry size-tag 1
607 header+compressed-payload-size)
608 ;; (make-header-entry RPMSIGTAG_PAYLOADSIZE 1
609 ;; uncompressed-payload-size)
610 ;; Reserve 32 bytes of extra space in case users would
611 ;; like to add signatures, as done in rpmGenerateSignature.
612 (make-header-entry RPMSIGTAG_RESERVEDSPACE 32
613 (make-list 32 0))))
614 RPMTAG_HEADERSIGNATURES))
615
616(define (assemble-rpm-metadata lead signature header)
617 "Align and append the various u8 list components together, and return the
618result as a bytevector."
619 (let* ((offset (+ (length lead) (length signature)))
620 (header-offset (next-aligned-offset offset 8))
621 (padding (make-list (- header-offset offset) 0)))
622 ;; The Header is 8-bytes aligned.
623 (u8-list->bytevector (append lead signature padding header))))
diff --git a/guix/scripts/pack.scm b/guix/scripts/pack.scm
index 77425e5b0f1..701e41ff1ab 100644
--- a/guix/scripts/pack.scm
+++ b/guix/scripts/pack.scm
@@ -5,7 +5,7 @@
5;;; Copyright © 2018 Chris Marusich <cmmarusich@gmail.com> 5;;; Copyright © 2018 Chris Marusich <cmmarusich@gmail.com>
6;;; Copyright © 2018 Efraim Flashner <efraim@flashner.co.il> 6;;; Copyright © 2018 Efraim Flashner <efraim@flashner.co.il>
7;;; Copyright © 2020 Tobias Geerinckx-Rice <me@tobias.gr> 7;;; Copyright © 2020 Tobias Geerinckx-Rice <me@tobias.gr>
8;;; Copyright © 2020, 2021, 2022 Maxim Cournoyer <maxim.cournoyer@gmail.com> 8;;; Copyright © 2020, 2021, 2022, 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com>
9;;; Copyright © 2020 Eric Bavier <bavier@posteo.net> 9;;; Copyright © 2020 Eric Bavier <bavier@posteo.net>
10;;; Copyright © 2022 Alex Griffin <a@ajgrf.com> 10;;; Copyright © 2022 Alex Griffin <a@ajgrf.com>
11;;; 11;;;
@@ -67,6 +67,7 @@
67 67
68 self-contained-tarball 68 self-contained-tarball
69 debian-archive 69 debian-archive
70 rpm-archive
70 docker-image 71 docker-image
71 squashfs-image 72 squashfs-image
72 73
@@ -856,6 +857,166 @@ Section: misc
856 857
857 858
858;;; 859;;;
860;;; RPM archive format.
861;;;
862(define* (rpm-archive name profile
863 #:key target
864 (profile-name "guix-profile")
865 entry-point
866 (compressor (first %compressors))
867 deduplicate?
868 localstatedir?
869 (symlinks '())
870 archiver
871 (extra-options '()))
872 "Return a RPM archive (.rpm) containing a store initialized with the closure
873of PROFILE, a derivation. The archive contains /gnu/store. SYMLINKS must be
874a list of (SOURCE -> TARGET) tuples denoting symlinks to be added to the pack.
875ARCHIVER and ENTRY-POINT are not used. RELOCATABLE?, PREIN-FILE, POSTIN-FILE,
876PREUN-FILE and POSTUN-FILE can be provided via EXTRA-OPTIONS."
877 (when entry-point
878 (warning (G_ "entry point not supported in the '~a' format~%") 'rpm))
879
880 (define root (populate-profile-root profile
881 #:profile-name profile-name
882 #:target target
883 #:localstatedir? localstatedir?
884 #:deduplicate? deduplicate?
885 #:symlinks symlinks))
886
887 (define payload
888 (let* ((raw-cpio-file-name "payload.cpio")
889 (compressed-cpio-file-name (string-append raw-cpio-file-name
890 (compressor-extension
891 compressor))))
892 (computed-file compressed-cpio-file-name
893 (with-imported-modules (source-module-closure
894 '((guix build utils)
895 (guix cpio)
896 (guix rpm)))
897 #~(begin
898 (use-modules (guix build utils)
899 (guix cpio)
900 (guix rpm)
901 (srfi srfi-1))
902
903 ;; Make sure non-ASCII file names are properly handled.
904 #+(set-utf8-locale profile)
905
906 (define %root (if #$localstatedir? "." #$root))
907
908 (when #$localstatedir?
909 ;; Fix the permission of the Guix database file, which was made
910 ;; read-only when copied to the store in populate-profile-root.
911 (copy-recursively #$root %root)
912 (chmod (string-append %root "/var/guix/db/db.sqlite") #o644))
913
914 (call-with-output-file #$raw-cpio-file-name
915 (lambda (port)
916 (with-directory-excursion %root
917 ;; The first "." entry is discarded.
918 (write-cpio-archive
919 (remove fhs-directory?
920 (cdr (find-files "." #:directories? #t)))
921 port))))
922 (when #+(compressor-command compressor)
923 (apply invoke (append #+(compressor-command compressor)
924 (list #$raw-cpio-file-name))))
925 (copy-file #$compressed-cpio-file-name #$output)))
926 #:local-build? #f))) ;allow offloading
927
928 (define build
929 (with-extensions (list guile-gcrypt)
930 (with-imported-modules `(((guix config) => ,(make-config.scm))
931 ,@(source-module-closure
932 `((gcrypt hash)
933 (guix build utils)
934 (guix profiles)
935 (guix rpm))
936 #:select? not-config?))
937 #~(begin
938 (use-modules (gcrypt hash)
939 (guix build utils)
940 (guix profiles)
941 (guix rpm)
942 (ice-9 binary-ports)
943 (ice-9 match) ;for manifest->friendly-name
944 (ice-9 optargs)
945 (rnrs bytevectors)
946 (srfi srfi-1))
947
948 (define machine-type
949 (and=> (or #$target %host-type)
950 (lambda (triplet)
951 (first (string-split triplet #\-)))))
952
953 #$(procedure-source manifest->friendly-name)
954
955 (define manifest (profile-manifest #$profile))
956
957 (define single-entry ;manifest entry
958 (match (manifest-entries manifest)
959 ((entry)
960 entry)
961 (_ #f)))
962
963 (define name
964 (or (and=> single-entry manifest-entry-name)
965 (manifest->friendly-name manifest)))
966
967 (define version
968 (or (and=> single-entry manifest-entry-version) "0.0.0"))
969
970 (define lead
971 (generate-lead (string-append name "-" version)
972 #:target (or #$target %host-type)))
973
974 (define payload-digest
975 (bytevector->hex-string (file-sha256 #$payload)))
976
977 (let-keywords '#$extra-options #f ((relocatable? #f)
978 (prein-file #f)
979 (postin-file #f)
980 (preun-file #f)
981 (postun-file #f))
982
983 (let ((header (generate-header name version
984 payload-digest
985 #$root
986 #$(compressor-name compressor)
987 #:target (or #$target %host-type)
988 #:relocatable? relocatable?
989 #:prein-file prein-file
990 #:postin-file postin-file
991 #:preun-file preun-file
992 #:postun-file postun-file)))
993
994 (define header-sha256
995 (bytevector->hex-string (sha256 (u8-list->bytevector header))))
996
997 (define payload-size (stat:size (stat #$payload)))
998
999 (define header+compressed-payload-size
1000 (+ (length header) payload-size))
1001
1002 (define signature
1003 (generate-signature header-sha256
1004 header+compressed-payload-size))
1005
1006 ;; Serialize the archive components to a file.
1007 (call-with-input-file #$payload
1008 (lambda (in)
1009 (call-with-output-file #$output
1010 (lambda (out)
1011 (put-bytevector out (assemble-rpm-metadata lead
1012 signature
1013 header))
1014 (sendfile out in payload-size)))))))))))
1015
1016 (gexp->derivation (string-append name ".rpm") build))
1017
1018
1019;;;
859;;; Compiling C programs. 1020;;; Compiling C programs.
860;;; 1021;;;
861 1022
@@ -1187,7 +1348,8 @@ last resort for relocation."
1187 `((tarball . ,self-contained-tarball) 1348 `((tarball . ,self-contained-tarball)
1188 (squashfs . ,squashfs-image) 1349 (squashfs . ,squashfs-image)
1189 (docker . ,docker-image) 1350 (docker . ,docker-image)
1190 (deb . ,debian-archive))) 1351 (deb . ,debian-archive)
1352 (rpm . ,rpm-archive)))
1191 1353
1192(define (show-formats) 1354(define (show-formats)
1193 ;; Print the supported pack formats. 1355 ;; Print the supported pack formats.
@@ -1201,18 +1363,22 @@ last resort for relocation."
1201 docker Tarball ready for 'docker load'")) 1363 docker Tarball ready for 'docker load'"))
1202 (display (G_ " 1364 (display (G_ "
1203 deb Debian archive installable via dpkg/apt")) 1365 deb Debian archive installable via dpkg/apt"))
1366 (display (G_ "
1367 rpm RPM archive installable via rpm/yum"))
1204 (newline)) 1368 (newline))
1205 1369
1370(define (required-option symbol)
1371 "Return an SYMBOL option that requires a value."
1372 (option (list (symbol->string symbol)) #t #f
1373 (lambda (opt name arg result . rest)
1374 (apply values
1375 (alist-cons symbol arg result)
1376 rest))))
1377
1206(define %deb-format-options 1378(define %deb-format-options
1207 (let ((required-option (lambda (symbol) 1379 (list (required-option 'control-file)
1208 (option (list (symbol->string symbol)) #t #f 1380 (required-option 'postinst-file)
1209 (lambda (opt name arg result . rest) 1381 (required-option 'triggers-file)))
1210 (apply values
1211 (alist-cons symbol arg result)
1212 rest))))))
1213 (list (required-option 'control-file)
1214 (required-option 'postinst-file)
1215 (required-option 'triggers-file))))
1216 1382
1217(define (show-deb-format-options) 1383(define (show-deb-format-options)
1218 (display (G_ " 1384 (display (G_ "
@@ -1231,6 +1397,32 @@ last resort for relocation."
1231 (newline) 1397 (newline)
1232 (exit 0)) 1398 (exit 0))
1233 1399
1400(define %rpm-format-options
1401 (list (required-option 'prein-file)
1402 (required-option 'postin-file)
1403 (required-option 'preun-file)
1404 (required-option 'postun-file)))
1405
1406(define (show-rpm-format-options)
1407 (display (G_ "
1408 --help-rpm-format list options specific to the RPM format")))
1409
1410(define (show-rpm-format-options/detailed)
1411 (display (G_ "
1412 --prein-file=FILE
1413 Embed the provided prein script"))
1414 (display (G_ "
1415 --postin-file=FILE
1416 Embed the provided postin script"))
1417 (display (G_ "
1418 --preun-file=FILE
1419 Embed the provided preun script"))
1420 (display (G_ "
1421 --postun-file=FILE
1422 Embed the provided postun script"))
1423 (newline)
1424 (exit 0))
1425
1234(define %options 1426(define %options
1235 ;; Specifications of the command-line options. 1427 ;; Specifications of the command-line options.
1236 (cons* (option '(#\h "help") #f #f 1428 (cons* (option '(#\h "help") #f #f
@@ -1307,7 +1499,12 @@ last resort for relocation."
1307 (lambda args 1499 (lambda args
1308 (show-deb-format-options/detailed))) 1500 (show-deb-format-options/detailed)))
1309 1501
1502 (option '("help-rpm-format") #f #f
1503 (lambda args
1504 (show-rpm-format-options/detailed)))
1505
1310 (append %deb-format-options 1506 (append %deb-format-options
1507 %rpm-format-options
1311 %transformation-options 1508 %transformation-options
1312 %standard-build-options 1509 %standard-build-options
1313 %standard-cross-build-options 1510 %standard-cross-build-options
@@ -1325,6 +1522,7 @@ Create a bundle of PACKAGE.\n"))
1325 (show-transformation-options-help) 1522 (show-transformation-options-help)
1326 (newline) 1523 (newline)
1327 (show-deb-format-options) 1524 (show-deb-format-options)
1525 (show-rpm-format-options)
1328 (newline) 1526 (newline)
1329 (display (G_ " 1527 (display (G_ "
1330 -f, --format=FORMAT build a pack in the given FORMAT")) 1528 -f, --format=FORMAT build a pack in the given FORMAT"))
@@ -1483,6 +1681,16 @@ Create a bundle of PACKAGE.\n"))
1483 (process-file-arg opts 'postinst-file) 1681 (process-file-arg opts 'postinst-file)
1484 #:triggers-file 1682 #:triggers-file
1485 (process-file-arg opts 'triggers-file))) 1683 (process-file-arg opts 'triggers-file)))
1684 ('rpm
1685 (list #:relocatable? relocatable?
1686 #:prein-file
1687 (process-file-arg opts 'prein-file)
1688 #:postin-file
1689 (process-file-arg opts 'postin-file)
1690 #:preun-file
1691 (process-file-arg opts 'preun-file)
1692 #:postun-file
1693 (process-file-arg opts 'postun-file)))
1486 (_ '()))) 1694 (_ '())))
1487 (target (assoc-ref opts 'target)) 1695 (target (assoc-ref opts 'target))
1488 (bootstrap? (assoc-ref opts 'bootstrap?)) 1696 (bootstrap? (assoc-ref opts 'bootstrap?))
diff --git a/tests/pack.scm b/tests/pack.scm
index a02924b7d20..734ae1c69b6 100644
--- a/tests/pack.scm
+++ b/tests/pack.scm
@@ -1,7 +1,7 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org> 2;;; Copyright © 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
3;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net> 3;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net>
4;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com> 4;;; Copyright © 2021, 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com>
5;;; 5;;;
6;;; This file is part of GNU Guix. 6;;; This file is part of GNU Guix.
7;;; 7;;;
@@ -28,13 +28,16 @@
28 #:use-module (guix tests) 28 #:use-module (guix tests)
29 #:use-module (guix gexp) 29 #:use-module (guix gexp)
30 #:use-module (guix modules) 30 #:use-module (guix modules)
31 #:use-module (guix utils)
31 #:use-module (gnu packages) 32 #:use-module (gnu packages)
32 #:use-module ((gnu packages base) #:select (glibc-utf8-locales)) 33 #:use-module ((gnu packages base) #:select (glibc-utf8-locales))
33 #:use-module (gnu packages bootstrap) 34 #:use-module (gnu packages bootstrap)
35 #:use-module ((gnu packages package-management) #:select (rpm))
34 #:use-module ((gnu packages compression) #:select (squashfs-tools)) 36 #:use-module ((gnu packages compression) #:select (squashfs-tools))
35 #:use-module ((gnu packages debian) #:select (dpkg)) 37 #:use-module ((gnu packages debian) #:select (dpkg))
36 #:use-module ((gnu packages guile) #:select (guile-sqlite3)) 38 #:use-module ((gnu packages guile) #:select (guile-sqlite3))
37 #:use-module ((gnu packages gnupg) #:select (guile-gcrypt)) 39 #:use-module ((gnu packages gnupg) #:select (guile-gcrypt))
40 #:use-module ((gnu packages linux) #:select (fakeroot))
38 #:use-module (srfi srfi-64)) 41 #:use-module (srfi srfi-64))
39 42
40(define %store 43(define %store
@@ -59,6 +62,17 @@
59 62
60(define %ar-bootstrap %bootstrap-binutils) 63(define %ar-bootstrap %bootstrap-binutils)
61 64
65;;; This is a variant of the RPM package configured so that its database can
66;;; be created on a writable location readily available inside the build
67;;; container ("/tmp").
68(define rpm-for-tests
69 (package
70 (inherit rpm)
71 (arguments (substitute-keyword-arguments (package-arguments rpm)
72 ((#:configure-flags flags '())
73 #~(cons "--localstatedir=/tmp"
74 (delete "--localstatedir=/var" #$flags)))))))
75
62 76
63(test-begin "pack") 77(test-begin "pack")
64 78
@@ -356,6 +370,47 @@
356 (assert (file-exists? "triggers")) 370 (assert (file-exists? "triggers"))
357 371
358 (mkdir #$output)))))) 372 (mkdir #$output))))))
373 (built-derivations (list check))))
374
375 (unless store (test-skip 1))
376 (test-assertm "rpm archive can be installed/uninstalled" store
377 (mlet* %store-monad
378 ((guile (set-guile-for-build (default-guile)))
379 (profile (profile-derivation (packages->manifest
380 (list %bootstrap-guile))
381 #:hooks '()
382 #:locales? #f))
383 (rpm-pack (rpm-archive "rpm-pack" profile
384 #:compressor %gzip-compressor
385 #:symlinks '(("/bin/guile" -> "bin/guile"))
386 #:extra-options '(#:relocatable? #t)))
387 (check
388 (gexp->derivation "check-rpm-pack"
389 (with-imported-modules (source-module-closure
390 '((guix build utils)))
391 #~(begin
392 (use-modules (guix build utils))
393
394 (define fakeroot #+(file-append fakeroot "/bin/fakeroot"))
395 (define rpm #+(file-append rpm-for-tests "/bin/rpm"))
396 (mkdir-p "/tmp/lib/rpm")
397
398 ;; Install the RPM package. This causes RPM to validate the
399 ;; signatures, header as well as the file digests, which
400 ;; makes it a rather thorough test.
401 (mkdir "test-prefix")
402 (invoke fakeroot rpm "--install"
403 (string-append "--prefix=" (getcwd) "/test-prefix")
404 #$rpm-pack)
405
406 ;; Invoke the installed Guile command.
407 (invoke "./test-prefix/bin/guile" "--version")
408
409 ;; Uninstall the RPM package.
410 (invoke fakeroot rpm "--erase" "guile-bootstrap")
411
412 ;; Required so the above is run.
413 (mkdir #$output))))))
359 (built-derivations (list check))))) 414 (built-derivations (list check)))))
360 415
361(test-end) 416(test-end)
diff --git a/tests/rpm.scm b/tests/rpm.scm
new file mode 100644
index 00000000000..f40b36fe603
--- /dev/null
+++ b/tests/rpm.scm
@@ -0,0 +1,86 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com>
3;;;
4;;; This file is part of GNU Guix.
5;;;
6;;; GNU Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; GNU Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (test-rpm)
20 #:use-module (guix rpm)
21 #:use-module (rnrs bytevectors)
22 #:use-module (srfi srfi-1)
23 #:use-module (srfi srfi-26)
24 #:use-module (srfi srfi-64)
25 #:use-module (srfi srfi-71))
26
27;; For white-box testing.
28(define-syntax-rule (expose-internal name)
29 (define name (@@ (guix rpm) name)))
30
31(expose-internal RPMTAG_ARCH)
32(expose-internal RPMTAG_LICENSE)
33(expose-internal RPMTAG_NAME)
34(expose-internal RPMTAG_OS)
35(expose-internal RPMTAG_RELEASE)
36(expose-internal RPMTAG_SUMMARY)
37(expose-internal RPMTAG_VERSION)
38(expose-internal header-entry-count)
39(expose-internal header-entry-tag)
40(expose-internal header-entry-value)
41(expose-internal header-entry?)
42(expose-internal make-header)
43(expose-internal make-header-entry)
44(expose-internal make-header-index+data)
45
46(test-begin "rpm")
47
48(test-equal "lead must be 96 bytes long"
49 96
50 (length (generate-lead "hello-2.12.1")))
51
52(define header-entries
53 (list (make-header-entry RPMTAG_NAME 1 "hello")
54 (make-header-entry RPMTAG_VERSION 1 "2.12.1")
55 (make-header-entry RPMTAG_RELEASE 1 "0")
56 (make-header-entry RPMTAG_SUMMARY 1
57 "Hello, GNU world: An example GNU package")
58 (make-header-entry RPMTAG_LICENSE 1 "GPL 3 or later")
59 (make-header-entry RPMTAG_OS 1 "Linux")
60 (make-header-entry RPMTAG_ARCH 1 "x86_64")))
61
62(define expected-header-index-length
63 (* 16 (length header-entries))) ;16 bytes per index entry
64
65(define expected-header-data-length
66 (+ (length header-entries) ;to account for null bytes
67 (fold + 0 (map (compose string-length (cut header-entry-value <>))
68 header-entries))))
69
70(let ((index data (make-header-index+data header-entries)))
71 (test-equal "header index"
72 expected-header-index-length
73 (length index))
74
75 ;; This test depends on the fact that only STRING entries are used, and that
76 ;; they are composed of single byte characters and the delimiting null byte.
77 (test-equal "header data"
78 expected-header-data-length
79 (length data)))
80
81(test-equal "complete header section"
82 (+ 16 ;leading magic + count bytes
83 expected-header-index-length expected-header-data-length)
84 (length (make-header header-entries)))
85
86(test-end)