aboutsummaryrefslogtreecommitdiff
path: root/gnu/build/linux-initrd.scm
blob: ea7de58553ceac11c11c8df516bef81a9c6dd234 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2013, 2014, 2015, 2018 Ludovic Courtès <ludo@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 build linux-initrd)
  #:use-module ((guix cpio) #:prefix cpio:)
  #:use-module (guix build utils)
  #:use-module (guix build store-copy)
  #:use-module (system base compile)
  #:use-module (rnrs bytevectors)
  #:use-module ((system foreign) #:select (sizeof))
  #:use-module (ice-9 ftw)
  #:export (write-cpio-archive
            build-initrd))

;;; Commentary:
;;;
;;; Tools to create Linux initial RAM disks ("initrds").  Initrds are
;;; essentially gzipped cpio archives, with a '/init' executable that the
;;; kernel runs at boot time.
;;;
;;; Code:

(define* (write-cpio-archive output directory
                             #:key
                             (compress? #t)
                             (gzip "gzip"))
  "Write a cpio archive containing DIRECTORY to file OUTPUT.  When
COMPRESS? is true, compress it using GZIP.  On success, return OUTPUT."

  ;; Note: as per `ramfs-rootfs-initramfs.txt', always add directory entries
  ;; before the files that are inside of it: "The Linux kernel cpio
  ;; extractor won't create files in a directory that doesn't exist, so the
  ;; directory entries must go before the files that go in those
  ;; directories."

  (define files
    ;; Use 'sort' so that (1) the order of files is deterministic, and (2)
    ;; directories appear before the files they contain.
    (sort (file-system-fold (const #t)                 ;enter?
                            (lambda (file stat result) ;leaf
                              (cons file result))
                            (lambda (dir stat result)  ;down
                              (if (string=? dir directory)
                                  result
                                  (cons dir result)))
                            (lambda (file stat result)
                              result)
                            (const #f)                 ;skip
                            (const #f)                 ;error
                            '()
                            directory)
          string<?))

  (call-with-output-file output
    (lambda (port)
      (cpio:write-cpio-archive files port
                               #:file->header cpio:file->cpio-header*)))

  (if compress?
      ;; Gzip insists on adding a '.gz' suffix and does nothing if the input
      ;; file already has that suffix.  Shuffle files around to placate it.
      (let* ((gz-suffix? (string-suffix? ".gz" output))
             (sans-gz    (if gz-suffix?
                             (string-drop-right output 3)
                             output)))
        (when gz-suffix?
          (rename-file output sans-gz))
        ;; Use '--no-name' so that gzip records neither a file name nor a time
        ;; stamp in its output.
        (and (zero? (system* gzip "--best" "--no-name" sans-gz))
             (begin
               (unless gz-suffix?
                 (rename-file (string-append output ".gz") output))
               output)))
      output))

(define (cache-compiled-file-name file)
  "Return the file name of the in-cache .go file for FILE, relative to the
current directory.

This is similar to what 'compiled-file-name' in (system base compile) does."
  (let loop ((file file))
    (let ((target (false-if-exception (readlink file))))
     (if target
         (loop target)
         (format #f ".cache/guile/ccache/~a-~a-~a-~a/~a"
                 (effective-version)
                 (if (eq? (native-endianness) (endianness little))
                     "LE"
                     "BE")
                 (sizeof '*)
                 (effective-version)
                 file)))))

(define (compile-to-cache file)
  "Compile FILE to the cache."
  (let ((compiled-file (cache-compiled-file-name file)))
    (mkdir-p (dirname compiled-file))
    (compile-file file
                  #:opts %auto-compilation-options
                  #:output-file compiled-file)))

(define* (build-initrd output
                       #:key
                       guile init
                       (references-graphs '())
                       (gzip "gzip"))
  "Write an initial RAM disk (initrd) to OUTPUT.  The initrd starts the script
at INIT, running GUILE.  It contains all the items referred to by
REFERENCES-GRAPHS."
  (mkdir "contents")

  ;; Copy the closures of all the items referenced in REFERENCES-GRAPHS.
  (populate-store references-graphs "contents")

  (with-directory-excursion "contents"
    ;; Make '/init'.
    (symlink init "init")

    ;; Compile it.
    (compile-to-cache "init")

    ;; Allow Guile to find out where it is (XXX).  See
    ;; 'guile-relocatable.patch'.
    (mkdir-p "proc/self")
    (symlink (string-append guile "/bin/guile") "proc/self/exe")
    (readlink "proc/self/exe")

    ;; Reset the timestamps of all the files that will make it in the initrd.
    (for-each (lambda (file)
                (unless (eq? 'symlink (stat:type (lstat file)))
                  (utime file 0 0 0 0)))
              (find-files "." ".*"))

    (write-cpio-archive output "." #:gzip gzip))

  ;; Make sure directories are writable so we can delete files.
  (for-each make-file-writable
            (find-files "contents"
                        (lambda (file stat)
                          (eq? 'directory (stat:type stat)))
                        #:directories? #t))
  (delete-file-recursively "contents"))

;;; linux-initrd.scm ends here
t:x:0: wheel:x:999:alice,bob hackers:x:65000:alice,charlie\n") (define %shadow-sample (string-append "\ root:" (crypt "secret" "$6$abc") ":17169:::::: charlie:" (crypt "hey!" "$6$abc") ":17169:::::: nobody:!:0::::::\n")) (test-begin "accounts") (test-equal "write-passwd" %passwd-sample (call-with-output-string (lambda (port) (write-passwd (list (password-entry (name "root") (uid 0) (gid 0) (real-name "Admin") (directory "/root") (shell "/bin/sh")) (password-entry (name "charlie") (uid 1000) (gid 998) (real-name "Charlie") (directory "/home/charlie") (shell "/bin/sh"))) port)))) (test-equal "write-passwd with duplicate entry" %passwd-sample (call-with-output-string (lambda (port) (let ((charlie (password-entry (name "charlie") (uid 1000) (gid 998) (real-name "Charlie") (directory "/home/charlie") (shell "/bin/sh")))) (write-passwd (list (password-entry (name "root") (uid 0) (gid 0) (real-name "Admin") (directory "/root") (shell "/bin/sh")) charlie charlie) port))))) (test-equal "read-passwd + write-passwd" %passwd-sample (call-with-output-string (lambda (port) (write-passwd (call-with-input-string %passwd-sample read-passwd) port)))) (test-equal "write-group" %group-sample (call-with-output-string (lambda (port) (write-group (list (group-entry (name "root") (gid 0)) (group-entry (name "wheel") (gid 999) (members '("alice" "bob"))) (group-entry (name "hackers") (gid 65000) (members '("alice" "charlie")))) port)))) (test-equal "read-group + write-group" %group-sample (call-with-output-string (lambda (port) (write-group (call-with-input-string %group-sample read-group) port)))) (test-equal "write-shadow" %shadow-sample (call-with-output-string (lambda (port) (write-shadow (list (shadow-entry (name "root") (password (crypt "secret" "$6$abc")) (last-change 17169)) (shadow-entry (name "charlie") (password (crypt "hey!" "$6$abc")) (last-change 17169)) (shadow-entry (name "nobody"))) port)))) (test-equal "read-shadow + write-shadow" %shadow-sample (call-with-output-string (lambda (port) (write-shadow (call-with-input-string %shadow-sample read-shadow) port)))) (define allocate-groups (@@ (gnu build accounts) allocate-groups)) (define allocate-passwd (@@ (gnu build accounts) allocate-passwd)) (test-equal "allocate-groups" ;; Allocate GIDs in a stateless fashion. (list (group-entry (name "s") (gid %system-id-max)) (group-entry (name "x") (gid 900)) (group-entry (name "t") (gid 899)) (group-entry (name "a") (gid %id-min) (password "foo") (members '("alice" "bob"))) (group-entry (name "b") (gid (+ %id-min 1)) (members '("charlie")))) (allocate-groups (list (user-group (name "s") (system? #t)) (user-group (name "x") (id 900)) (user-group (name "t") (system? #t)) (user-group (name "a") (password "foo")) (user-group (name "b"))) (alist->vhash `(("a" . "bob") ("a" . "alice") ("b" . "charlie"))))) (test-equal "allocate-groups with requested GIDs" ;; Make sure the requested GID for "b" is honored. (list (group-entry (name "a") (gid (+ 1 %id-min))) (group-entry (name "b") (gid %id-min)) (group-entry (name "c") (gid (+ 2 %id-min)))) (allocate-groups (list (user-group (name "a")) (user-group (name "b") (id %id-min)) (user-group (name "c"))) vlist-null)) (test-equal "allocate-groups with previous state" ;; Make sure bits of state are preserved: password, GID, no reuse of ;; previously-used GIDs. (list (group-entry (name "s") (gid (- %system-id-max 1))) (group-entry (name "t") (gid (- %system-id-max 2))) (group-entry (name "a") (gid 30000) (password #f) (members '("alice" "bob"))) (group-entry (name "b") (gid 30001) (password "bar") (members '("charlie")))) (allocate-groups (list (user-group (name "s") (system? #t)) (user-group (name "t") (system? #t)) (user-group (name "a") (password "foo")) (user-group (name "b"))) (alist->vhash `(("a" . "bob") ("a" . "alice") ("b" . "charlie"))) (list (group-entry (name "a") (gid 30000)) (group-entry (name "b") (gid 30001) (password "bar")) (group-entry (name "removed") (gid %system-id-max))))) (test-equal "allocate-groups with previous state, looping" ;; Check that allocation starts after the highest previously-used GID, and ;; loops back to the lowest GID. (list (group-entry (name "a") (gid (- %id-max 1))) (group-entry (name "b") (gid %id-min)) (group-entry (name "c") (gid (+ 1 %id-min)))) (allocate-groups (list (user-group (name "a")) (user-group (name "b")) (user-group (name "c"))) vlist-null (list (group-entry (name "d") (gid (- %id-max 2)))))) (test-equal "allocate-passwd" ;; Allocate UIDs in a stateless fashion. (list (password-entry (name "alice") (uid %id-min) (gid 1000) (real-name "Alice") (shell "/bin/sh") (directory "/home/alice")) (password-entry (name "bob") (uid (+ 1 %id-min)) (gid 1001) (real-name "Bob") (shell "/bin/gash") (directory "/home/bob")) (password-entry (name "sshd") (uid %system-id-max) (gid 500) (real-name "sshd") (shell "/nologin") (directory "/var/empty")) (password-entry (name "guix") (uid 30000) (gid 499) (real-name "Guix") (shell "/nologin") (directory "/var/empty"))) (allocate-passwd (list (user-account (name "alice") (comment "Alice") (shell "/bin/sh") (group "users")) (user-account (name "bob") (comment "Bob") (shell "/bin/gash") (group "wheel")) (user-account (name "sshd") (system? #t) (comment "sshd") (home-directory "/var/empty") (shell "/nologin") (group "sshd")) (user-account (name "guix") (system? #t) (comment "Guix") (home-directory "/var/empty") (shell "/nologin") (group "guix") (uid 30000))) (list (group-entry (name "users") (gid 1000)) (group-entry (name "wheel") (gid 1001)) (group-entry (name "sshd") (gid 500)) (group-entry (name "guix") (gid 499))))) (test-equal "allocate-passwd with previous state" ;; Make sure bits of state are preserved: UID, no reuse of previously-used ;; UIDs, and shell. (list (password-entry (name "alice") (uid 1234) (gid 1000) (real-name "Alice Smith") (shell "/bin/sh") (directory "/home/alice")) (password-entry (name "charlie") (uid 1236) (gid 1000) (real-name "Charlie") (shell "/bin/sh") (directory "/home/charlie"))) (allocate-passwd (list (user-account (name "alice") (comment "Alice") (shell "/bin/sh") ;honored (group "users")) (user-account (name "charlie") (comment "Charlie") (shell "/bin/sh") (group "users"))) (list (group-entry (name "users") (gid 1000))) (list (password-entry (name "alice") (uid 1234) (gid 9999) (real-name "Alice Smith") (shell "/gnu/.../bin/gash") ;ignored (directory "/home/alice")) (password-entry (name "bob") (uid 1235) (gid 1001) (real-name "Bob") (shell "/bin/sh") (directory "/home/bob"))))) (test-equal "user+group-databases" ;; The whole shebang. (list (list (group-entry (name "a") (gid %id-min) (members '("bob"))) (group-entry (name "b") (gid (+ 1 %id-min)) (members '("alice"))) (group-entry (name "s") (gid %system-id-max))) (list (password-entry (name "alice") (real-name "Alice") (uid %id-min) (gid %id-min) (directory "/a")) (password-entry (name "bob") (real-name "Bob") (uid (+ 1 %id-min)) (gid (+ 1 %id-min)) (directory "/b")) (password-entry (name "nobody") (uid 65534) (gid %system-id-max) (directory "/var/empty"))) (list (shadow-entry (name "alice") (last-change 100) (password (crypt "initial pass" "$6$"))) (shadow-entry (name "bob") (last-change 50) (password (crypt "foo" "$6$"))) (shadow-entry (name "nobody") (last-change 100)))) (call-with-values (lambda () (user+group-databases (list (user-account (name "alice") (comment "Alice") (home-directory "/a") (group "a") (supplementary-groups '("b")) (password (crypt "initial pass" "$6$"))) (user-account (name "bob") (comment "Bob") (home-directory "/b") (group "b") (supplementary-groups '("a"))) (user-account (name "nobody") (group "s") (uid 65534) (home-directory "/var/empty"))) (list (user-group (name "a")) (user-group (name "b")) (user-group (name "s") (system? #t))) #:current-passwd '() #:current-shadow (list (shadow-entry (name "bob") (password (crypt "foo" "$6$")) (last-change 50))) #:current-groups '() #:current-time (lambda (type) (make-time type 0 (* 24 3600 100))))) list)) (test-end "accounts")