;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2014, 2015, 2016, 2017, 2018 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2016, 2017 David Craven <david@craven.ch>
;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
+;;; Copyright © 2019 Guillaume Le Vaillant <glv@posteo.net>
+;;; Copyright © 2019 Tobias Geerinckx-Rice <me@tobias.gr>
+;;; Copyright © 2019 David C. Trudgian <dave@trudgian.net>
+;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;;
;;; This file is part of GNU Guix.
;;;
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (gnu build file-systems)
+ #:use-module (gnu system uuid)
+ #:use-module (gnu system file-systems)
#:use-module (guix build utils)
#:use-module (guix build bournish)
- #:use-module (guix build syscalls)
+ #:use-module ((guix build syscalls)
+ #:hide (file-system-type))
#:use-module (rnrs io ports)
#:use-module (rnrs bytevectors)
#:use-module (ice-9 match)
#:use-module (ice-9 rdelim)
- #:use-module (ice-9 format)
- #:use-module (ice-9 regex)
#:use-module (system foreign)
#:autoload (system repl repl) (start-repl)
#:use-module (srfi srfi-1)
find-partition-by-luks-uuid
canonicalize-device-spec
- uuid->string
- string->uuid
- string->iso9660-uuid
- string->ext2-uuid
- string->ext3-uuid
- string->ext4-uuid
- string->btrfs-uuid
- iso9660-uuid->string
+ read-partition-label
+ read-partition-uuid
+ read-luks-partition-uuid
bind-mount
(and (magic? block)
block)))))))))
-(define (sub-bytevector bv start size)
- "Return a copy of the SIZE bytes of BV starting from offset START."
- (let ((result (make-bytevector size)))
- (bytevector-copy! bv start result 0 size)
- result))
-
-(define (latin1->string bv terminator)
- "Return a string of BV, a latin1 bytevector, or #f. TERMINATOR is a predicate
-that takes a number and returns #t when a termination character is found."
- (let ((bytes (take-while (negate terminator) (bytevector->u8-list bv))))
- (if (null? bytes)
- #f
- (list->string (map integer->char bytes)))))
-
(define null-terminated-latin1->string
(cut latin1->string <> zero?))
;; <http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-107.pdf>.
-(define-syntax %fat32-endianness
- ;; Endianness of fat file systems.
- (identifier-syntax (endianness little)))
-
(define (fat32-superblock? sblock)
"Return #t when SBLOCK is a fat32 superblock."
(bytevector=? (sub-bytevector sblock 82 8)
"Return the Volume ID of a fat superblock SBLOCK as a 4-byte bytevector."
(sub-bytevector sblock 67 4))
-(define (fat32-uuid->string uuid)
- "Convert fat32 UUID, a 4-byte bytevector, to its string representation."
- (let ((high (bytevector-uint-ref uuid 0 %fat32-endianness 2))
- (low (bytevector-uint-ref uuid 2 %fat32-endianness 2)))
- (format #f "~:@(~x-~x~)" low high)))
-
(define (fat32-superblock-volume-name sblock)
"Return the volume name of SBLOCK as a string of at most 11 characters, or
#f if SBLOCK has no volume name. The volume name is a latin1 string.
Trailing spaces are trimmed."
(string-trim-right (latin1->string (sub-bytevector sblock 71 11) (lambda (c) #f)) #\space))
-(define (check-fat32-file-system device)
+(define (check-fat-file-system device)
"Return the health of a fat file system on DEVICE."
(match (status:exit-val
(system* "fsck.vfat" "-v" "-a" device))
(_ 'fatal-error)))
\f
+;;;
+;;; FAT16 file systems.
+;;;
+
+(define (fat16-superblock? sblock)
+ "Return #t when SBLOCK is a fat16 boot record."
+ (bytevector=? (sub-bytevector sblock 54 8)
+ (string->utf8 "FAT16 ")))
+
+(define (read-fat16-superblock device)
+ "Return the raw contents of DEVICE's fat16 superblock as a bytevector, or
+#f if DEVICE does not contain a fat16 file system."
+ (read-superblock device 0 62 fat16-superblock?))
+
+(define (fat16-superblock-uuid sblock)
+ "Return the Volume ID of a fat superblock SBLOCK as a 4-byte bytevector."
+ (sub-bytevector sblock 39 4))
+
+(define (fat16-superblock-volume-name sblock)
+ "Return the volume name of SBLOCK as a string of at most 11 characters, or
+#f if SBLOCK has no volume name. The volume name is a latin1 string.
+Trailing spaces are trimmed."
+ (string-trim-right (latin1->string (sub-bytevector sblock 43 11)
+ (lambda (c) #f))
+ #\space))
+
+\f
;;;
;;; ISO9660 file systems.
;;;
;; <http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-119.pdf>.
-(define %iso9660-uuid-rx
- ;; Y m d H M S ss
- (make-regexp "^([[:digit:]]{4})-([[:digit:]]{2})-([[:digit:]]{2})-([[:digit:]]{2})-([[:digit:]]{2})-([[:digit:]]{2})-([[:digit:]]{2})$"))
-
-(define (string->iso9660-uuid str)
- "Parse STR as a ISO9660 UUID (which is really a timestamp - see /dev/disk/by-uuid).
-Return its contents as a 16-byte bytevector. Return #f if STR is not a valid
-ISO9660 UUID representation."
- (and=> (regexp-exec %iso9660-uuid-rx str)
- (lambda (match)
- (letrec-syntax ((match-numerals
- (syntax-rules ()
- ((_ index (name rest ...) body)
- (let ((name (match:substring match index)))
- (match-numerals (+ 1 index) (rest ...) body)))
- ((_ index () body)
- body))))
- (match-numerals 1 (year month day hour minute second hundredths)
- (string->utf8 (string-append year month day
- hour minute second hundredths)))))))
-
(define (iso9660-superblock? sblock)
"Return #t when SBLOCK is an iso9660 volume descriptor."
(bytevector=? (sub-bytevector sblock 1 6)
"Return the raw contents of DEVICE's iso9660 primary volume descriptor
as a bytevector, or #f if DEVICE does not contain an iso9660 file system."
;; Start reading at sector 16.
- ;; Since we are not sure that the device contains an ISO9660 filesystem,
+ ;; Since we are not sure that the device contains an ISO9660 file system,
;; we have to find that out first.
(if (read-superblock device (* 2048 16) 2048 iso9660-superblock?)
(read-iso9660-primary-volume-descriptor device (* 2048 16))
- #f)) ; Device does not contain an iso9660 filesystem.
+ #f)) ; Device does not contain an iso9660 file system.
(define (iso9660-superblock-uuid sblock)
"Return the modification time of an iso9660 primary volume descriptor
modification-time)))
(sub-bytevector time 0 16))) ; strips GMT offset.
-(define (iso9660-uuid->string uuid)
- "Given an UUID bytevector, return its timestamp string."
- (define (digits->string bytes)
- (latin1->string bytes (lambda (c) #f)))
- (let* ((year (sub-bytevector uuid 0 4))
- (month (sub-bytevector uuid 4 2))
- (day (sub-bytevector uuid 6 2))
- (hour (sub-bytevector uuid 8 2))
- (minute (sub-bytevector uuid 10 2))
- (second (sub-bytevector uuid 12 2))
- (hundredths (sub-bytevector uuid 14 2))
- (parts (list year month day hour minute second hundredths)))
- (string-append (string-join (map digits->string parts) "-"))))
-
(define (iso9660-superblock-volume-name sblock)
"Return the volume name of SBLOCK as a string. The volume name is an ASCII
string. Trailing spaces are trimmed."
(lambda (c) #f)) #\space))
\f
+;;;
+;;; JFS file systems.
+;;;
+
+;; Taken from <linux-libre>/fs/jfs/jfs_superblock.h.
+
+(define-syntax %jfs-endianness
+ ;; Endianness of JFS file systems.
+ (identifier-syntax (endianness little)))
+
+(define (jfs-superblock? sblock)
+ "Return #t when SBLOCK is a JFS superblock."
+ (bytevector=? (sub-bytevector sblock 0 4)
+ (string->utf8 "JFS1")))
+
+(define (read-jfs-superblock device)
+ "Return the raw contents of DEVICE's JFS superblock as a bytevector, or #f
+if DEVICE does not contain a JFS file system."
+ (read-superblock device 32768 184 jfs-superblock?))
+
+(define (jfs-superblock-uuid sblock)
+ "Return the UUID of JFS superblock SBLOCK as a 16-byte bytevector."
+ (sub-bytevector sblock 136 16))
+
+(define (jfs-superblock-volume-name sblock)
+ "Return the volume name of SBLOCK as a string of at most 16 characters, or
+#f if SBLOCK has no volume name."
+ (null-terminated-latin1->string (sub-bytevector sblock 152 16)))
+
+(define (check-jfs-file-system device)
+ "Return the health of a JFS file system on DEVICE."
+ (match (status:exit-val
+ (system* "jfs_fsck" "-p" "-v" device))
+ (0 'pass)
+ (1 'errors-corrected)
+ (2 'reboot-required)
+ (_ 'fatal-error)))
+
+\f
;;;
;;; LUKS encrypted devices.
;;;
;; <https://gitlab.com/cryptsetup/cryptsetup/wikis/Specification>. We follow
;; version 1.2.1 of this document.
+;; The LUKS2 header format is described in "LUKS2 On-Disk Format Specification":
+;; <https://gitlab.com/cryptsetup/LUKS2-docs/blob/master/luks2_doc_wip.pdf>.
+;; It is a WIP document.
+
(define-syntax %luks-endianness
;; Endianness of LUKS headers.
(identifier-syntax (endianness big)))
(let ((magic (sub-bytevector sblock 0 6))
(version (bytevector-u16-ref sblock 6 %luks-endianness)))
(and (bytevector=? magic %luks-magic)
- (= version 1))))
+ (or (= version 1) (= version 2)))))
(define (read-luks-header file)
"Read a LUKS header from FILE. Return the raw header on success, and #f if
not valid header was found."
- ;; Size in bytes of the LUKS header, including key slots.
+ ;; Size in bytes of the LUKS binary header, which includes key slots in
+ ;; LUKS1. In LUKS2 the binary header is partially backward compatible, so
+ ;; that UUID can be extracted as for LUKS1. Keyslots and other metadata are
+ ;; not part of this header in LUKS2, but are included in the JSON metadata
+ ;; area that follows.
(read-superblock file 0 592 luks-superblock?))
(define (luks-header-uuid header)
(partition-field-reader read-btrfs-superblock
btrfs-superblock-volume-name)
(partition-field-reader read-fat32-superblock
- fat32-superblock-volume-name)))
+ fat32-superblock-volume-name)
+ (partition-field-reader read-fat16-superblock
+ fat16-superblock-volume-name)
+ (partition-field-reader read-jfs-superblock
+ jfs-superblock-volume-name)))
(define %partition-uuid-readers
(list (partition-field-reader read-iso9660-superblock
(partition-field-reader read-btrfs-superblock
btrfs-superblock-uuid)
(partition-field-reader read-fat32-superblock
- fat32-superblock-uuid)))
+ fat32-superblock-uuid)
+ (partition-field-reader read-fat16-superblock
+ fat16-superblock-uuid)
+ (partition-field-reader read-jfs-superblock
+ jfs-superblock-uuid)))
(define read-partition-label
(cut read-partition-field <> %partition-label-readers))
(define read-partition-uuid
(cut read-partition-field <> %partition-uuid-readers))
+(define luks-partition-field-reader
+ (partition-field-reader read-luks-header luks-header-uuid))
+
+(define read-luks-partition-uuid
+ (cut read-partition-field <> (list luks-partition-field-reader)))
+
(define (partition-predicate reader =)
"Return a predicate that returns true if the FIELD of partition header that
was READ is = to the given value."
(partition-predicate read-partition-label string=?))
(define partition-uuid-predicate
- (partition-predicate read-partition-uuid bytevector=?))
+ (partition-predicate read-partition-uuid uuid=?))
(define luks-partition-uuid-predicate
- (partition-predicate
- (partition-field-reader read-luks-header luks-header-uuid)
- bytevector=?))
+ (partition-predicate luks-partition-field-reader uuid=?))
(define (find-partition predicate)
"Return the first partition found that matches PREDICATE, or #f if none
(find-partition luks-partition-uuid-predicate))
\f
-;;;
-;;; UUIDs.
-;;;
-
-(define-syntax %network-byte-order
- (identifier-syntax (endianness big)))
-
-(define (uuid->string uuid)
- "Convert UUID, a 16-byte bytevector, to its string representation, something
-like \"6b700d61-5550-48a1-874c-a3d86998990e\"."
- ;; See <https://tools.ietf.org/html/rfc4122>.
- (let ((time-low (bytevector-uint-ref uuid 0 %network-byte-order 4))
- (time-mid (bytevector-uint-ref uuid 4 %network-byte-order 2))
- (time-hi (bytevector-uint-ref uuid 6 %network-byte-order 2))
- (clock-seq (bytevector-uint-ref uuid 8 %network-byte-order 2))
- (node (bytevector-uint-ref uuid 10 %network-byte-order 6)))
- (format #f "~8,'0x-~4,'0x-~4,'0x-~4,'0x-~12,'0x"
- time-low time-mid time-hi clock-seq node)))
-
-(define %uuid-rx
- ;; The regexp of a UUID.
- (make-regexp "^([[:xdigit:]]{8})-([[:xdigit:]]{4})-([[:xdigit:]]{4})-([[:xdigit:]]{4})-([[:xdigit:]]{12})$"))
-
-(define (string->uuid str)
- "Parse STR as a DCE UUID (see <https://tools.ietf.org/html/rfc4122>) and
-return its contents as a 16-byte bytevector. Return #f if STR is not a valid
-UUID representation."
- (and=> (regexp-exec %uuid-rx str)
- (lambda (match)
- (letrec-syntax ((hex->number
- (syntax-rules ()
- ((_ index)
- (string->number (match:substring match index)
- 16))))
- (put!
- (syntax-rules ()
- ((_ bv index (number len) rest ...)
- (begin
- (bytevector-uint-set! bv index number
- (endianness big) len)
- (put! bv (+ index len) rest ...)))
- ((_ bv index)
- bv))))
- (let ((time-low (hex->number 1))
- (time-mid (hex->number 2))
- (time-hi (hex->number 3))
- (clock-seq (hex->number 4))
- (node (hex->number 5))
- (uuid (make-bytevector 16)))
- (put! uuid 0
- (time-low 4) (time-mid 2) (time-hi 2)
- (clock-seq 2) (node 6)))))))
-
-(define string->ext2-uuid string->uuid)
-(define string->ext3-uuid string->uuid)
-(define string->ext4-uuid string->uuid)
-(define string->btrfs-uuid string->uuid)
-
-\f
-(define* (canonicalize-device-spec spec #:optional (title 'any))
- "Return the device name corresponding to SPEC. TITLE is a symbol, one of
-the following:
-
- • 'device', in which case SPEC is known to designate a device node--e.g.,
- \"/dev/sda1\";
- • 'label', in which case SPEC is known to designate a partition label--e.g.,
- \"my-root-part\";
- • 'uuid', in which case SPEC must be a UUID (a 16-byte bytevector)
- designating a partition;
- • 'any', in which case SPEC can be anything.
-"
+(define (canonicalize-device-spec spec)
+ "Return the device name corresponding to SPEC, which can be a <uuid>, a
+<file-system-label>, or a string (typically a /dev file name)."
(define max-trials
;; Number of times we retry partition label resolution, 1 second per
;; trial. Note: somebody reported a delay of 16 seconds (!) before their
;; this long.
20)
- (define canonical-title
- ;; The realm of canonicalization.
- (if (eq? title 'any)
- (if (string? spec)
- ;; The "--root=SPEC" kernel command-line option always provides a
- ;; string, but the string can represent a device, a UUID, or a
- ;; label. So check for all three.
- (cond ((string-prefix? "/" spec) 'device)
- ((string->uuid spec) 'uuid)
- (else 'label))
- 'uuid)
- title))
-
(define (resolve find-partition spec fmt)
(let loop ((count 0))
(let ((device (find-partition spec)))
(sleep 1)
(loop (+ 1 count))))))))
- (case canonical-title
- ((device)
- ;; Nothing to do.
- spec)
- ((label)
+ (match spec
+ ((? string?)
+ ;; Nothing to do, but wait until SPEC shows up.
+ (resolve identity spec identity))
+ ((? file-system-label?)
;; Resolve the label.
- (resolve find-partition-by-label spec identity))
- ((uuid)
+ (resolve find-partition-by-label
+ (file-system-label->string spec)
+ identity))
+ ((? uuid?)
(resolve find-partition-by-uuid
- (if (string? spec)
- (string->uuid spec)
- spec)
- uuid->string))
- (else
- (error "unknown device title" title))))
+ (uuid-bytevector spec)
+ uuid->string))))
(define (check-file-system device type)
"Run a file system check of TYPE on DEVICE."
(cond
((string-prefix? "ext" type) check-ext2-file-system)
((string-prefix? "btrfs" type) check-btrfs-file-system)
- ((string-suffix? "fat" type) check-fat32-file-system)
+ ((string-suffix? "fat" type) check-fat-file-system)
+ ((string-prefix? "jfs" type) check-jfs-file-system)
+ ((string-prefix? "nfs" type) (const 'pass))
(else #f)))
(if check-procedure
(sleep 3)
(reboot))
('fatal-error
- (format (current-error-port)
- "File system check on ~a failed; spawning Bourne-like REPL~%"
+ (format (current-error-port) "File system check on ~a failed~%"
device)
- (start-repl %bournish-language)))
+
+ ;; Spawn a REPL only if someone would be able to interact with it.
+ (when (isatty? (current-input-port))
+ (format (current-error-port) "Spawning Bourne-like REPL.~%")
+
+ ;; 'current-output-port' is typically connected to /dev/klog (in
+ ;; PID 1), but here we want to make sure we talk directly to the
+ ;; user.
+ (with-output-to-file "/dev/console"
+ (lambda ()
+ (start-repl %bournish-language))))))
(format (current-error-port)
"No file system check procedure for ~a; skipping~%"
device)))
(logior MS_NODEV (loop rest)))
(('no-exec rest ...)
(logior MS_NOEXEC (loop rest)))
+ (('no-atime rest ...)
+ (logior MS_NOATIME (loop rest)))
+ (('strict-atime rest ...)
+ (logior MS_STRICTATIME (loop rest)))
+ (('lazy-time rest ...)
+ (logior MS_LAZYTIME (loop rest)))
(()
0))))
-(define* (mount-file-system spec #:key (root "/root"))
- "Mount the file system described by SPEC under ROOT. SPEC must have the
-form:
-
- (DEVICE TITLE MOUNT-POINT TYPE (FLAGS ...) OPTIONS CHECK?)
-
-DEVICE, MOUNT-POINT, and TYPE must be strings; OPTIONS can be a string or #f;
-FLAGS must be a list of symbols. CHECK? is a Boolean indicating whether to
-run a file system check."
+(define* (mount-file-system fs #:key (root "/root"))
+ "Mount the file system described by FS, a <file-system> object, under ROOT."
(define (mount-nfs source mount-point type flags options)
(let* ((idx (string-rindex source #\:))
(if options
(string-append "," options)
"")))))
- (match spec
- ((source title mount-point type (flags ...) options check?)
- (let ((source (canonicalize-device-spec source title))
- (mount-point (string-append root "/" mount-point))
- (flags (mount-flags->bit-mask flags)))
- (when check?
- (check-file-system source type))
-
- ;; Create the mount point. Most of the time this is a directory, but
- ;; in the case of a bind mount, a regular file or socket may be needed.
- (if (and (= MS_BIND (logand flags MS_BIND))
- (not (file-is-directory? source)))
- (unless (file-exists? mount-point)
- (mkdir-p (dirname mount-point))
- (call-with-output-file mount-point (const #t)))
- (mkdir-p mount-point))
-
- (cond
- ((string-prefix? "nfs" type)
- (mount-nfs source mount-point type flags options))
- (else
- (mount source mount-point type flags options)))
-
- ;; For read-only bind mounts, an extra remount is needed, as per
- ;; <http://lwn.net/Articles/281157/>, which still applies to Linux 4.0.
- (when (and (= MS_BIND (logand flags MS_BIND))
- (= MS_RDONLY (logand flags MS_RDONLY)))
- (let ((flags (logior MS_BIND MS_REMOUNT MS_RDONLY)))
- (mount source mount-point type flags #f)))))))
+ (let ((type (file-system-type fs))
+ (options (file-system-options fs))
+ (source (canonicalize-device-spec (file-system-device fs)))
+ (mount-point (string-append root "/"
+ (file-system-mount-point fs)))
+ (flags (mount-flags->bit-mask (file-system-flags fs))))
+ (when (file-system-check? fs)
+ (check-file-system source type))
+
+ ;; Create the mount point. Most of the time this is a directory, but
+ ;; in the case of a bind mount, a regular file or socket may be needed.
+ (if (and (= MS_BIND (logand flags MS_BIND))
+ (not (file-is-directory? source)))
+ (unless (file-exists? mount-point)
+ (mkdir-p (dirname mount-point))
+ (call-with-output-file mount-point (const #t)))
+ (mkdir-p mount-point))
+
+ (cond
+ ((string-prefix? "nfs" type)
+ (mount-nfs source mount-point type flags options))
+ (else
+ (mount source mount-point type flags options)))
+
+ ;; For read-only bind mounts, an extra remount is needed, as per
+ ;; <http://lwn.net/Articles/281157/>, which still applies to Linux 4.0.
+ (when (and (= MS_BIND (logand flags MS_BIND))
+ (= MS_RDONLY (logand flags MS_RDONLY)))
+ (let ((flags (logior MS_BIND MS_REMOUNT MS_RDONLY)))
+ (mount source mount-point type flags #f)))))
;;; file-systems.scm ends here