aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2012-2024 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2017, 2020 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
;;; Copyright © 2018, 2019 Clément Lassieur <clement@lassieur.org>
;;; Copyright © 2020 Julien Lepiller <julien@lepiller.eu>
;;; Copyright © 2020, 2021 Mathieu Othacehe <othacehe@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu ci)
  #:use-module (guix build-system channel)
  #:use-module (guix config)
  #:autoload   (guix describe) (package-channels)
  #:use-module (guix memoization)
  #:use-module (guix store)
  #:use-module (guix profiles)
  #:use-module (guix packages)
  #:autoload   (guix transformations) (tunable-package? tuned-package)
  #:use-module (guix channels)
  #:use-module (guix config)
  #:use-module (guix derivations)
  #:use-module (guix monads)
  #:use-module (guix gexp)
  #:use-module (guix ui)
  #:use-module ((guix licenses)
                #:select (gpl3+ license? license-name))
  #:use-module ((guix utils) #:select (%current-system))
  #:use-module ((guix scripts system) #:select (read-operating-system))
  #:use-module ((guix scripts pack)
                #:select (self-contained-tarball))
  #:use-module (gnu bootloader)
  #:use-module (gnu bootloader u-boot)
  #:use-module (gnu compression)
  #:use-module (gnu image)
  #:use-module (gnu packages)
  #:use-module (gnu packages gcc)
  #:use-module (gnu packages gdb)
  #:use-module (gnu packages base)
  #:use-module (gnu packages gawk)
  #:use-module (gnu packages guile)
  #:use-module (gnu packages gettext)
  #:use-module (gnu packages compression)
  #:use-module (gnu packages multiprecision)
  #:use-module (gnu packages make-bootstrap)
  #:use-module (gnu packages package-management)
  #:use-module (guix platform)
  #:use-module (gnu system)
  #:use-module (gnu system image)
  #:use-module (gnu system vm)
  #:use-module (gnu system install)
  #:use-module (gnu system images hurd)
  #:use-module (gnu system images novena)
  #:use-module (gnu system images pine64)
  #:use-module (gnu system images pinebook-pro)
  #:use-module (gnu system images visionfive2)
  #:use-module (gnu tests)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (ice-9 match)
  #:export (derivation->job
            image->job

            %core-packages

            arguments->systems
            cuirass-jobs))

;;; Commentary:
;;;
;;; This file defines build jobs for Cuirass.
;;;
;;; Code:

(define* (derivation->job name drv
                          #:key
                          (max-silent-time 3600)
                          (timeout (* 5 3600)))
  "Return a Cuirass job called NAME and describing DRV.

MAX-SILENT-TIME and TIMEOUT are build options passed to the daemon when
building the derivation."
  `((#:job-name . ,name)
    (#:derivation . ,(derivation-file-name drv))
    (#:inputs . ,(map (compose derivation-file-name
                               derivation-input-derivation)
                      (derivation-inputs drv)))
    (#:outputs . ,(filter-map
                   (lambda (res)
                     (match res
                       ((name . path)
                        `(,name . ,path))))
                   (derivation->output-paths drv)))
    (#:nix-name . ,(derivation-name drv))
    (#:system . ,(derivation-system drv))
    (#:max-silent-time . ,max-silent-time)
    (#:timeout . ,timeout)))

(define* (package-job store job-name package system
                      #:key cross? target (suffix ""))
  "Return a job called JOB-NAME that builds PACKAGE on SYSTEM."
  (let ((job-name (string-append job-name "." system suffix)))
    (parameterize ((%graft? #f))
      (let* ((drv (if cross?
                      (package-cross-derivation store package target system
                                                #:graft? #f)
                      (package-derivation store package system
                                          #:graft? #f)))
             (max-silent-time (or (assoc-ref (package-properties package)
                                             'max-silent-time)
                                  3600))
             (timeout (or (assoc-ref (package-properties package)
                                     'timeout)
                          72000)))
        (derivation->job job-name drv
                         #:max-silent-time max-silent-time
                         #:timeout timeout)))))

(define (package-cross-job store job-name package target system)
  "Return a job called TARGET.JOB-NAME that cross-builds PACKAGE for TARGET on
SYSTEM."
  (let ((name (string-append target "." job-name)))
    (package-job store name package system
                 #:cross? #t
                 #:target target)))

(define %core-packages
  ;; Note: Don't put the '-final' package variants because (1) that's
  ;; implicit, and (2) they cannot be cross-built (due to the explicit input
  ;; chain.)
  (list gcc-10 gcc-11 gcc-12 glibc binutils gdb-minimal
        gmp mpfr mpc coreutils findutils diffutils patch sed grep
        gawk gnu-gettext hello guile-2.2 guile-3.0 zlib gzip xz guix
        %bootstrap-binaries-tarball
        %binutils-bootstrap-tarball
        (%glibc-bootstrap-tarball)
        %gcc-bootstrap-tarball
        %guile-bootstrap-tarball
        %bootstrap-tarballs))

(define (commencement-packages system)
  "Return the list of bootstrap packages from the commencement module for
SYSTEM."
  ;; Only include packages supported on SYSTEM.  For example, the Mes
  ;; bootstrap graph is currently not supported on ARM so it should be
  ;; excluded.
  (filter (lambda (obj)
            (and (package? obj)
                 (supported-package? obj system)))
          (module-map (lambda (sym var)
                        (variable-ref var))
                      (resolve-module '(gnu packages commencement)))))

(define (packages-to-cross-build target)
  "Return the list of packages to cross-build for TARGET."
  ;; Don't cross-build the bootstrap tarballs for MinGW.
  (if (string-contains target "mingw")
      (drop-right %core-packages 6)
      %core-packages))

(define %bare-platform-triplets
  ;; Cross-compilation triplets of platforms that lack a proper user-space and
  ;; for which there's no point in trying to build regular packages.
  '("avr"
    "or1k-elf"
    "xtensa-ath9k-elf"))

(define %unsupported-platform-triplets
  ;; These systems are kept around for nostalgia or for tinkering, but regular
  ;; CI is disabled for them to reduce the load on CI infrastructure.
  '("mips64el-linux-gnu"
    "powerpc-linux-gnu"
    "powerpc64-linux-gnu"))

(define (cross-jobs store system)
  "Return a list of cross-compilation jobs for SYSTEM."
  (define (from-32-to-64? target)
    ;; Return true if SYSTEM is 32-bit and TARGET is 64-bit.  This hack
    ;; prevents known-to-fail cross-builds from i686-linux or armhf-linux to
    ;; mips64el-linux-gnuabi64.
    (and (or (string-prefix? "i686-" system)
             (string-prefix? "i586-" system)
             (string-prefix? "armhf-" system))
         (string-contains target "64")))    ;x86_64, mips64el, aarch64, etc.

  (define (same? target)
    ;; Return true if SYSTEM and TARGET are the same thing.  This is so we
    ;; don't try to cross-compile to 'mips64el-linux-gnu' from
    ;; 'mips64el-linux'.
    (or (and (string-contains target system)
             (not (string=? "x86_64-linux-gnux32" target)))
        (and (string-prefix? "armhf" system)    ;armhf-linux
             (string-prefix? "arm" target))))   ;arm-linux-gnueabihf

  (define (pointless? target)
    ;; Return #t if it makes no sense to cross-build to TARGET from SYSTEM.
    (or (member target %bare-platform-triplets)
        (member target %unsupported-platform-triplets)
        (match system
          ((or "x86_64-linux" "i686-linux")
           (if (string-contains target "mingw")
               (not (string=? "x86_64-linux" system))
               #f))
          (_
           ;; Don't try to cross-compile from non-Intel platforms: this isn't
           ;; very useful and these are often brittle configurations.
           #t))))

  (define (either proc1 proc2 proc3)
    (lambda (x)
      (or (proc1 x) (proc2 x) (proc3 x))))

  (append-map (lambda (target)
                (map (lambda (package)
                       (package-cross-job store (job-name package)
                                          package target system))
                     (packages-to-cross-build target)))
              (remove (either from-32-to-64? same? pointless?)
                      (targets))))

(define* (guix-jobs store systems #:key source commit)
  "Return a list of jobs for Guix itself."
  (define build
    (primitive-load (string-append source "/build-aux/build-self.scm")))

  (map
   (lambda (system)
     (let ((name (string->symbol
                  (string-append "guix." system)))
           (drv (run-with-store store
                  (build source #:version commit #:system system
                         #:pull-version 1
                         #:guile-version "2.2"))))
       (derivation->job name drv)))
   systems))

;; Architectures that are able to build or cross-build Guix System images.
;; This does not mean that other architectures are not supported, only that
;; they are often not fast enough to support Guix System images building.
(define %guix-system-supported-systems
  '("x86_64-linux" "i686-linux"))

(define %guix-system-images
  (list hurd-barebones-qcow2-image
        pine64-barebones-raw-image
        pinebook-pro-barebones-raw-image
        novena-barebones-raw-image
        visionfive2-barebones-raw-image))

(define (hours hours)
  (* 3600 hours))

(define* (image->job store image
                     #:key name system)
  "Return the job for IMAGE on SYSTEM.  If NAME is passed, use it as job name,
otherwise use the IMAGE name."
  (let* ((image-name (or name
                         (symbol->string (image-name image))))
         (name (string-append image-name "." system))
         (drv (run-with-store store
                (mbegin %store-monad
                  (set-guile-for-build (default-guile))
                  (lower-object (system-image image) system)))))
    (parameterize ((%graft? #f))
      (derivation->job name drv))))

(define* (image-jobs store system
                     #:key source commit)
  "Return a list of jobs that build images for SYSTEM."
  (define MiB
    (expt 2 20))

  (parameterize ((current-guix-package
                  (channel-source->package source #:commit commit)))
    (if (member system %guix-system-supported-systems)
        `(,(image->job store
                       (image
                        (inherit mbr-hybrid-disk-image)
                        (operating-system installation-os))
                       #:name "usb-image"
                       #:system system)
          ,(image->job
            store
            (image
             (inherit (image-with-label
                       iso9660-image
                       (string-append "GUIX_" system "_"
                                      (if (> (string-length %guix-version) 7)
                                          (substring %guix-version 0 7)
                                          %guix-version))))
             (operating-system installation-os))
            #:name "iso9660-image"
            #:system system)
          ;; Only cross-compile Guix System images from x86_64-linux for now.
          ,@(if (string=? system "x86_64-linux")
                (map (cut image->job store <>
                          #:system system)
                     %guix-system-images)
                '()))
        '())))

(define* (system-test-jobs store system
                           #:key source commit)
  "Return a list of jobs for the system tests."
  (define (->job test)
    (let ((name (string-append "test." (system-test-name test)
                               "." system))
          (drv (run-with-store store
                 (mbegin %store-monad
                   (set-current-system system)
                   (set-grafting #f)
                   (set-guile-for-build (default-guile))
                   (system-test-value test)))))

      (derivation->job name drv)))

  (if (member system %guix-system-supported-systems)
      ;; Override the value of 'current-guix' used by system tests.  Using a
      ;; channel instance makes tests that rely on 'current-guix' less
      ;; expensive.  It also makes sure we get a valid Guix package when this
      ;; code is not running from a checkout.
      (parameterize ((current-guix-package
                      (channel-source->package source #:commit commit)))
        (map ->job (all-system-tests)))
      '()))

(define (tarball-jobs store system)
  "Return jobs to build the self-contained Guix binary tarball."
  (define (->job name drv)
    (let ((name (string-append name "." system)))
      (parameterize ((%graft? #f))
        (derivation->job name drv))))

  ;; XXX: Add a job for the stable Guix?
  (list
   (->job "binary-tarball"
          (run-with-store store
            (mbegin %store-monad
              (set-guile-for-build (default-guile))
              (>>= (profile-derivation (packages->manifest (list guix)))
                   (lambda (profile)
                     (self-contained-tarball "guix-binary" profile
                                             #:profile-name "current-guix"
                                             #:localstatedir? #t
                                             #:compressor
                                             (lookup-compressor "xz")))))
            #:system system))))

(define job-name
  ;; Return the name of a package's job.
  package-name)

(define base-packages
  (mlambda (system)
    "Return the set of packages considered to be part of the base for SYSTEM."
    (delete-duplicates
     (append-map (match-lambda
                   ((_ package _ ...)
                    (match (package-transitive-inputs package)
                      (((_ inputs _ ...) ...)
                       inputs))))
                 (%final-inputs system)))))

(define package->job
  (lambda* (store package system #:key (suffix ""))
    "Return a job for PACKAGE on SYSTEM, or #f if this combination is not
valid.  Append SUFFIX to the job name."
    (cond ((member package (base-packages system))
           (package-job store (string-append "base." (job-name package))
                        package system #:suffix suffix))
          ((supported-package? package system)
           (let ((drv (package-derivation store package system
                                          #:graft? #f)))
             (and (substitutable-derivation? drv)
                  (package-job store (job-name package)
                               package system #:suffix suffix))))
          (else
           #f))))

(define %x86-64-micro-architectures
  ;; Micro-architectures for which we build tuned variants.
  '("haswell" "skylake" "x86-64-v2" "x86-64-v3" "x86-64-v4"))

(define (tuned-package-jobs store package system)
  "Return a list of jobs for PACKAGE tuned for SYSTEM's micro-architectures."
  (filter-map (lambda (micro-architecture)
                (define suffix
                  (string-append "." micro-architecture))

                (package->job store
                              (tuned-package package micro-architecture)
                              system
                              #:suffix suffix))
              (match system
                ("x86_64-linux" %x86-64-micro-architectures)
                (_ '()))))

(define (all-packages)
  "Return the list of packages to build."
  (define (adjust package result)
    (cond ((package-replacement package)
           ;; XXX: If PACKAGE and its replacement have the same name/version,
           ;; then both Cuirass jobs will have the same name, which
           ;; effectively means that the second one will be ignored.  Thus,
           ;; return the replacement first.
           (cons* (package-replacement package)   ;build both
                  package
                  result))
          ((package-superseded package)
           result)                                ;don't build it
          (else
           (cons package result))))

  (fold-packages adjust
                 (fold adjust '()                 ;include base packages
                       (match (%final-inputs)
                         (((labels packages _ ...) ...)
                          packages)))
                 #:select? (const #t)))           ;include hidden packages

(define (arguments->manifests arguments channels)
  "Return the list of manifests extracted from ARGUMENTS."
  (map (lambda (manifest)
         (any (lambda (checkout)
                (let ((path (in-vicinity checkout manifest)))
                  (and (file-exists? path)
                       path)))
              (map channel-url channels)))
       arguments))

(define (manifests->jobs store manifests systems)
  "Return the list of jobs for the entries in MANIFESTS, a list of file
names, for each one of SYSTEMS."
  (define (load-manifest manifest)
    (save-module-excursion
     (lambda ()
       (set-current-module (make-user-module '((guix profiles) (gnu))))
       (primitive-load manifest))))

  (define (manifest-entry-job-name entry)
    (string-append (manifest-entry-name entry) "-"
                   (manifest-entry-version entry)))

  (define (manifest-entry->job entry system)
    (let* ((obj (manifest-entry-item entry))
           (drv (parameterize ((%graft? #f))
                  (run-with-store store
                    (lower-object obj system)
                    #:system system)))
           (max-silent-time (or (and (package? obj)
                                     (assoc-ref (package-properties obj)
                                                'max-silent-time))
                                3600))
           (timeout (or (and (package? obj)
                             (assoc-ref (package-properties obj) 'timeout))
                        (* 5 3600))))
      (derivation->job (manifest-entry-job-name entry) drv
                       #:max-silent-time max-silent-time
                       #:timeout timeout)))

  (let ((entries (delete-duplicates
                  (append-map (compose manifest-entries load-manifest)
                              manifests)
                  manifest-entry=?)))
    (append-map (lambda (system)
                  (map (cut manifest-entry->job <> system) entries))
                systems)))

(define (arguments->systems arguments)
  "Return the systems list from ARGUMENTS."
  (match (assoc-ref arguments 'systems)
    (#f              %cuirass-supported-systems)
    ((lst ...)       lst)
    ((? string? str) (call-with-input-string str read))))


;;;
;;; Cuirass entry point.
;;;

(define (cuirass-jobs store arguments)
  "Register Cuirass jobs."
  (define subset
    (assoc-ref arguments 'subset))

  (define systems
    (arguments->systems arguments))

  (define channels
    (let ((channels (assq-ref arguments 'channels)))
      (map sexp->channel channels)))

  (define guix
    (find guix-channel? channels))

  (define commit
    (channel-commit guix))

  (define source
    (channel-url guix))

  ;; Turn off grafts.  Grafting is meant to happen on the user's machines.
  (parameterize ((%graft? #f))
    ;; Return one job for each package, except bootstrap packages.
    (append-map
     (lambda (system)
       (format (current-error-port)
               "evaluating for '~a' (heap size: ~a MiB)...~%"
               system
               (round
                (/ (assoc-ref (gc-stats) 'heap-size)
                   (expt 2. 20))))
       (invalidate-derivation-caches!)
       (match subset
         ('all
          ;; Build everything, including replacements.
          (let ((all (all-packages))
                (jobs (lambda (package)
                        (match (package->job store package system)
                          (#f '())
                          (main-job
                           (cons main-job
                                 (if (tunable-package? package)
                                     (tuned-package-jobs store package system)
                                     '())))))))
            (append
             (append-map jobs all)
             (cross-jobs store system))))
         ('core
          ;; Build core packages only.
          (append
           (map (lambda (package)
                  (package-job store (job-name package)
                               package system))
                (append (commencement-packages system) %core-packages))
           (cross-jobs store system)))
         ('guix
          ;; Build Guix modules only.
          (guix-jobs store systems
                     #:source source
                     #:commit commit))
         ('hello
          ;; Build hello package only.
          (let ((hello (specification->package "hello")))
            (list (package-job store (job-name hello)
                               hello system))))
         ('images
          ;; Build Guix System images only.
          (image-jobs store system
                      #:source source
                      #:commit commit))
         ('system-tests
          ;; Build Guix System tests only.
          (system-test-jobs store system
                            #:source source
                            #:commit commit))
         ('tarball
          ;; Build Guix tarball only.
          (tarball-jobs store system))
         (('custom . modules)
          ;; Build custom modules jobs only.
          (append-map
           (lambda (module)
             (let ((proc (module-ref
                          (resolve-interface module)
                          'cuirass-jobs)))
               (proc store arguments)))
           modules))
         (('channels . channels)
          ;; Build only the packages from CHANNELS.
          (let ((all (all-packages)))
            (filter-map
             (lambda (package)
               (any (lambda (channel)
                      (and (member (channel-name channel) channels)
                           (package->job store package system)))
                    (package-channels package)))
             all)))
         (('packages . rest)
          ;; Build selected list of packages only.
          (let ((packages (map specification->package rest)))
            (map (lambda (package)
                   (package-job store (job-name package)
                                package system))
                 packages)))
         (('manifests . rest)
          ;; Build packages in the list of manifests.
          (let ((manifests (arguments->manifests rest channels)))
            (manifests->jobs store manifests systems)))
         (else
          (error "unknown subset" subset))))
     systems)))
tor sblock 120 16))) (define (check-ext2-file-system device) "Return the health of an ext2 file system on DEVICE." (match (status:exit-val (system* "e2fsck" "-v" "-p" "-C" "0" device)) (0 'pass) (1 'errors-corrected) (2 'reboot-required) (_ 'fatal-error))) ;;; ;;; Btrfs file systems. ;;; ;; <https://btrfs.wiki.kernel.org/index.php/On-disk_Format#Superblock>. (define-syntax %btrfs-endianness ;; Endianness of btrfs file systems. (identifier-syntax (endianness little))) (define (btrfs-superblock? sblock) "Return #t when SBLOCK is a btrfs superblock." (bytevector=? (sub-bytevector sblock 64 8) (string->utf8 "_BHRfS_M"))) (define (read-btrfs-superblock device) "Return the raw contents of DEVICE's btrfs superblock as a bytevector, or #f if DEVICE does not contain a btrfs file system." (read-superblock device 65536 4096 btrfs-superblock?)) (define (btrfs-superblock-uuid sblock) "Return the UUID of a btrfs superblock SBLOCK as a 16-byte bytevector." (sub-bytevector sblock 32 16)) (define (btrfs-superblock-volume-name sblock) "Return the volume name of SBLOCK as a string of at most 256 characters, or #f if SBLOCK has no volume name." (null-terminated-latin1->string (sub-bytevector sblock 299 256))) (define (check-btrfs-file-system device) "Return the health of a btrfs file system on DEVICE." (match (status:exit-val (system* "btrfs" "device" "scan")) (0 'pass) (_ 'fatal-error))) ;;; ;;; LUKS encrypted devices. ;;; ;; The LUKS header format is described in "LUKS On-Disk Format Specification": ;; <https://gitlab.com/cryptsetup/cryptsetup/wikis/Specification>. We follow ;; version 1.2.1 of this document. (define-syntax %luks-endianness ;; Endianness of LUKS headers. (identifier-syntax (endianness big))) (define (luks-superblock? sblock) "Return #t when SBLOCK is a luks superblock." (define %luks-magic ;; The 'LUKS_MAGIC' constant. (u8-list->bytevector (append (map char->integer (string->list "LUKS")) (list #xba #xbe)))) (let ((magic (sub-bytevector sblock 0 6)) (version (bytevector-u16-ref sblock 6 %luks-endianness))) (and (bytevector=? magic %luks-magic) (= version 1)))) (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. (read-superblock file 0 592 luks-superblock?)) (define (luks-header-uuid header) "Return the LUKS UUID from HEADER, as a 16-byte bytevector." ;; 40 bytes are reserved for the UUID, but in practice, it contains the 36 ;; bytes of its ASCII representation. (let ((uuid (sub-bytevector header 168 36))) (string->uuid (utf8->string uuid)))) ;;; ;;; Partition lookup. ;;; (define (disk-partitions) "Return the list of device names corresponding to valid disk partitions." (define (last-character str) (string-ref str (- (string-length str) 1))) (define (partition? name major minor) ;; Select device names that end in a digit, like libblkid's 'probe_all' ;; function does. Checking for "/sys/dev/block/MAJOR:MINOR/partition" ;; doesn't work for partitions coming from mapped devices. (and (char-set-contains? char-set:digit (last-character name)) (> major 2))) ;ignore RAM disks and floppy disks (call-with-input-file "/proc/partitions" (lambda (port) ;; Skip the two header lines. (read-line port) (read-line port) ;; Read each subsequent line, and extract the last space-separated ;; field. (let loop ((parts '())) (let ((line (read-line port))) (if (eof-object? line) (reverse parts) (match (string-tokenize line) (((= string->number major) (= string->number minor) blocks name) (if (partition? name major minor) (loop (cons name parts)) (loop parts)))))))))) (define (ENOENT-safe proc) "Wrap the one-argument PROC such that ENOENT errors are caught and lead to a warning and #f as the result." (lambda (device) (catch 'system-error (lambda () (proc device)) (lambda args ;; When running on the hand-made /dev, ;; 'disk-partitions' could return partitions for which ;; we have no /dev node. Handle that gracefully. (let ((errno (system-error-errno args))) (cond ((= ENOENT errno) (format (current-error-port) "warning: device '~a' not found~%" device) #f) ((= ENOMEDIUM errno) ;for removable media #f) (else (apply throw args)))))))) (define (partition-field-reader read field) "Return a procedure that takes a device and returns the value of a FIELD in the partition superblock or #f." (let ((read (ENOENT-safe read))) (lambda (device) (let ((sblock (read device))) (and sblock (field sblock)))))) (define (read-partition-field device partition-field-readers) "Returns the value of a FIELD in the partition superblock of DEVICE or #f. It takes a list of PARTITION-FIELD-READERS and returns the result of the first partition field reader that returned a value." (match (filter-map (cut apply <> (list device)) partition-field-readers) ((field . _) field) (_ #f))) (define %partition-label-readers (list (partition-field-reader read-ext2-superblock ext2-superblock-volume-name) (partition-field-reader read-btrfs-superblock btrfs-superblock-volume-name))) (define %partition-uuid-readers (list (partition-field-reader read-ext2-superblock ext2-superblock-uuid) (partition-field-reader read-btrfs-superblock btrfs-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 (partition-predicate reader =) "Return a predicate that returns true if the FIELD of partition header that was READ is = to the given value." (lambda (expected) (lambda (device) (let ((actual (reader device))) (and actual (= actual expected)))))) (define partition-label-predicate (partition-predicate read-partition-label string=?)) (define partition-uuid-predicate (partition-predicate read-partition-uuid bytevector=?)) (define luks-partition-uuid-predicate (partition-predicate (partition-field-reader read-luks-header luks-header-uuid) bytevector=?)) (define (find-partition predicate) "Return the first partition found that matches PREDICATE, or #f if none were found." (lambda (expected) (find (predicate expected) (map (cut string-append "/dev/" <>) (disk-partitions))))) (define find-partition-by-label (find-partition partition-label-predicate)) (define find-partition-by-uuid (find-partition partition-uuid-predicate)) (define find-partition-by-luks-uuid (find-partition luks-partition-uuid-predicate)) ;;; ;;; 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* (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 max-trials ;; Number of times we retry partition label resolution, 1 second per ;; trial. Note: somebody reported a delay of 16 seconds (!) before their ;; USB key would be detected by the kernel, so we must wait for at least ;; 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))) (or device ;; Some devices take a bit of time to appear, most notably USB ;; storage devices. Thus, wait for the device to appear. (if (> count max-trials) (error "failed to resolve partition" (fmt spec)) (begin (format #t "waiting for partition '~a' to appear...~%" (fmt spec)) (sleep 1) (loop (+ 1 count)))))))) (case canonical-title ((device) ;; Nothing to do. spec) ((label) ;; Resolve the label. (resolve find-partition-by-label spec identity)) ((uuid) (resolve find-partition-by-uuid (if (string? spec) (string->uuid spec) spec) uuid->string)) (else (error "unknown device title" title)))) (define (check-file-system device type) "Run a file system check of TYPE on DEVICE." (define check-procedure (cond ((string-prefix? "ext" type) check-ext2-file-system) ((string-prefix? "btrfs" type) check-btrfs-file-system) (else #f))) (if check-procedure (match (check-procedure device) ('pass #t) ('errors-corrected (format (current-error-port) "File system check corrected errors on ~a; continuing~%" device)) ('reboot-required (format (current-error-port) "File system check corrected errors on ~a; rebooting~%" device) (sleep 3) (reboot)) ('fatal-error (format (current-error-port) "File system check on ~a failed; spawning Bourne-like REPL~%" device) (start-repl %bournish-language))) (format (current-error-port) "No file system check procedure for ~a; skipping~%" device))) (define (mount-flags->bit-mask flags) "Return the number suitable for the 'flags' argument of 'mount' that corresponds to the symbols listed in FLAGS." (let loop ((flags flags)) (match flags (('read-only rest ...) (logior MS_RDONLY (loop rest))) (('bind-mount rest ...) (logior MS_BIND (loop rest))) (('no-suid rest ...) (logior MS_NOSUID (loop rest))) (('no-dev rest ...) (logior MS_NODEV (loop rest))) (('no-exec rest ...) (logior MS_NOEXEC (loop rest))) (() 0)))) (define (regular-file? file-name) "Return #t if FILE-NAME is a regular file." (eq? (stat:type (stat file-name)) 'regular)) (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-nfs source mount-point type flags options) (let* ((idx (string-rindex source #\:)) (host-part (string-take source idx)) ;; Strip [] from around host if present (host (match (string-split host-part (string->char-set "[]")) (("" h "") h) ((h) h))) (aa (match (getaddrinfo host "nfs") ((x . _) x))) (sa (addrinfo:addr aa)) (inet-addr (inet-ntop (sockaddr:fam sa) (sockaddr:addr sa)))) ;; Mounting an NFS file system requires passing the address ;; of the server in the addr= option (mount source mount-point type flags (string-append "addr=" inet-addr (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 may be needed. (if (and (= MS_BIND (logand flags MS_BIND)) (regular-file? 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