graft.scm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2014, 2015, 2016, 2018 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2016 Mark H Weaver <mhw@netris.org>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (guix build graft)
  20. #:use-module (guix build utils)
  21. #:use-module (guix build debug-link)
  22. #:use-module (rnrs bytevectors)
  23. #:use-module (ice-9 vlist)
  24. #:use-module (ice-9 match)
  25. #:use-module (ice-9 threads)
  26. #:use-module (ice-9 binary-ports)
  27. #:use-module (srfi srfi-1) ; list library
  28. #:use-module (srfi srfi-26) ; cut and cute
  29. #:export (replace-store-references
  30. rewrite-directory
  31. graft))
  32. ;;; Commentary:
  33. ;;;
  34. ;;; This module supports "grafts". Grafting a directory means rewriting it,
  35. ;;; with references to some specific items replaced by references to other
  36. ;;; store items---the grafts.
  37. ;;;
  38. ;;; This method is used to provide fast security updates as only the leaves of
  39. ;;; the dependency graph need to be grafted, even when the security updates
  40. ;;; affect a core component such as Bash or libc. It is based on the idea of
  41. ;;; 'replace-dependency' implemented by Shea Levy in Nixpkgs.
  42. ;;;
  43. ;;; Code:
  44. (define-syntax-rule (define-inline name val)
  45. (define-syntax name (identifier-syntax val)))
  46. (define-inline hash-length 32)
  47. (define nix-base32-char?
  48. (cute char-set-contains?
  49. ;; ASCII digits and lower case letters except e o t u
  50. (string->char-set "0123456789abcdfghijklmnpqrsvwxyz")
  51. <>))
  52. (define* (replace-store-references input output replacement-table
  53. #:optional (store (%store-directory)))
  54. "Read data from INPUT, replacing store references according to
  55. REPLACEMENT-TABLE, and writing the result to OUTPUT. REPLACEMENT-TABLE is a
  56. vhash that maps strings (original hashes) to bytevectors (replacement strings
  57. comprising the replacement hash, a dash, and a string).
  58. Note: We use string keys to work around the fact that guile-2.0 hashes all
  59. bytevectors to the same value."
  60. (define (lookup-replacement s)
  61. (match (vhash-assoc s replacement-table)
  62. ((origin . replacement)
  63. replacement)
  64. (#f #f)))
  65. (define (optimize-u8-predicate pred)
  66. (cute vector-ref
  67. (list->vector (map pred (iota 256)))
  68. <>))
  69. (define nix-base32-byte?
  70. (optimize-u8-predicate
  71. (compose nix-base32-char?
  72. integer->char)))
  73. (define (dash? byte) (= byte 45))
  74. (define request-size (expt 2 20)) ; 1 MiB
  75. ;; We scan the file for the following 33-byte pattern: 32 bytes of
  76. ;; nix-base32 characters followed by a dash. To accommodate large files,
  77. ;; we do not read the entire file, but instead work on buffers of up to
  78. ;; 'request-size' bytes. To ensure that every 33-byte sequence appears
  79. ;; entirely within exactly one buffer, adjacent buffers must overlap,
  80. ;; i.e. they must share 32 byte positions. We accomplish this by
  81. ;; "ungetting" the last 32 bytes of each buffer before reading the next
  82. ;; buffer, unless we know that we've reached the end-of-file.
  83. (let ((buffer (make-bytevector request-size)))
  84. (let loop ()
  85. ;; Note: We avoid 'get-bytevector-n' to work around
  86. ;; <http://bugs.gnu.org/17466>.
  87. (match (get-bytevector-n! input buffer 0 request-size)
  88. ((? eof-object?) 'done)
  89. (end
  90. ;; We scan the buffer for dashes that might be preceded by a
  91. ;; nix-base32 hash. The key optimization here is that whenever we
  92. ;; find a NON-nix-base32 character at position 'i', we know that it
  93. ;; cannot be part of a hash, so the earliest position where the next
  94. ;; hash could start is i+1 with the following dash at position i+33.
  95. ;;
  96. ;; Since nix-base32 characters comprise only 1/8 of the 256 possible
  97. ;; byte values, and exclude some of the most common letters in
  98. ;; English text (e t o u), in practice we can advance by 33 positions
  99. ;; most of the time.
  100. (let scan-from ((i hash-length) (written 0))
  101. ;; 'i' is the first position where we look for a dash. 'written'
  102. ;; is the number of bytes in the buffer that have already been
  103. ;; written.
  104. (if (< i end)
  105. (let ((byte (bytevector-u8-ref buffer i)))
  106. (cond ((and (dash? byte)
  107. ;; We've found a dash. Note that we do not know
  108. ;; whether the preceeding 32 bytes are nix-base32
  109. ;; characters, but we do not need to know. If
  110. ;; they are not, the following lookup will fail.
  111. (lookup-replacement
  112. (string-tabulate (lambda (j)
  113. (integer->char
  114. (bytevector-u8-ref buffer
  115. (+ j (- i hash-length)))))
  116. hash-length)))
  117. => (lambda (replacement)
  118. ;; We've found a hash that needs to be replaced.
  119. ;; First, write out all bytes preceding the hash
  120. ;; that have not yet been written.
  121. (put-bytevector output buffer written
  122. (- i hash-length written))
  123. ;; Now write the replacement string.
  124. (put-bytevector output replacement)
  125. ;; Since the byte at position 'i' is a dash,
  126. ;; which is not a nix-base32 char, the earliest
  127. ;; position where the next hash might start is
  128. ;; i+1, and the earliest position where the
  129. ;; following dash might start is (+ i 1
  130. ;; hash-length). Also, increase the write
  131. ;; position to account for REPLACEMENT.
  132. (let ((len (bytevector-length replacement)))
  133. (scan-from (+ i 1 len)
  134. (+ i (- len hash-length))))))
  135. ;; If the byte at position 'i' is a nix-base32 char,
  136. ;; then the dash we're looking for might be as early as
  137. ;; the following byte, so we can only advance by 1.
  138. ((nix-base32-byte? byte)
  139. (scan-from (+ i 1) written))
  140. ;; If the byte at position 'i' is NOT a nix-base32
  141. ;; char, then the earliest position where the next hash
  142. ;; might start is i+1, with the following dash at
  143. ;; position (+ i 1 hash-length).
  144. (else
  145. (scan-from (+ i 1 hash-length) written))))
  146. ;; We have finished scanning the buffer. Now we determine how
  147. ;; many bytes have not yet been written, and how many bytes to
  148. ;; "unget". If 'end' is less than 'request-size' then we read
  149. ;; less than we asked for, which indicates that we are at EOF,
  150. ;; so we needn't unget anything. Otherwise, we unget up to
  151. ;; 'hash-length' bytes (32 bytes). However, we must be careful
  152. ;; not to unget bytes that have already been written, because
  153. ;; that would cause them to be written again from the next
  154. ;; buffer. In practice, this case occurs when a replacement is
  155. ;; made near or beyond the end of the buffer. When REPLACEMENT
  156. ;; went beyond END, we consume the extra bytes from INPUT.
  157. (begin
  158. (if (> written end)
  159. (get-bytevector-n! input buffer 0 (- written end))
  160. (let* ((unwritten (- end written))
  161. (unget-size (if (= end request-size)
  162. (min hash-length unwritten)
  163. 0))
  164. (write-size (- unwritten unget-size)))
  165. (put-bytevector output buffer written write-size)
  166. (unget-bytevector input buffer (+ written write-size)
  167. unget-size)))
  168. (loop)))))))))
  169. (define (rename-matching-files directory mapping)
  170. "Apply MAPPING to the names of all the files in DIRECTORY, where MAPPING is
  171. a list of store file name pairs."
  172. (let* ((mapping (map (match-lambda
  173. ((source . target)
  174. (cons (basename source) (basename target))))
  175. mapping))
  176. (matches (find-files directory
  177. (lambda (file stat)
  178. (assoc-ref mapping (basename file)))
  179. #:directories? #t)))
  180. ;; XXX: This is not quite correct: if MAPPING contains "foo", and
  181. ;; DIRECTORY contains "bar/foo/foo", we first rename "bar/foo" and then
  182. ;; "bar/foo/foo" no longer exists so we fail. Oh well, surely that's good
  183. ;; enough!
  184. (for-each (lambda (file)
  185. (let ((target (assoc-ref mapping (basename file))))
  186. (rename-file file
  187. (string-append (dirname file) "/" target))))
  188. matches)))
  189. (define (exit-on-exception proc)
  190. "Return a procedure that wraps PROC so that 'primitive-exit' is called when
  191. an exception is caught."
  192. (lambda (arg)
  193. (catch #t
  194. (lambda ()
  195. (proc arg))
  196. (lambda (key . args)
  197. ;; Since ports are not thread-safe as of Guile 2.0, reopen stderr.
  198. (let ((port (fdopen 2 "w0")))
  199. (print-exception port #f key args)
  200. (primitive-exit 1))))))
  201. ;; We need this as long as we support Guile < 2.0.13.
  202. (define* (mkdir-p* dir #:optional (mode #o755))
  203. "This is a variant of 'mkdir-p' that works around
  204. <http://bugs.gnu.org/24659> by passing MODE explicitly in each 'mkdir' call."
  205. (define absolute?
  206. (string-prefix? "/" dir))
  207. (define not-slash
  208. (char-set-complement (char-set #\/)))
  209. (let loop ((components (string-tokenize dir not-slash))
  210. (root (if absolute?
  211. ""
  212. ".")))
  213. (match components
  214. ((head tail ...)
  215. (let ((path (string-append root "/" head)))
  216. (catch 'system-error
  217. (lambda ()
  218. (mkdir path mode)
  219. (loop tail path))
  220. (lambda args
  221. (if (= EEXIST (system-error-errno args))
  222. (loop tail path)
  223. (apply throw args))))))
  224. (() #t))))
  225. (define* (rewrite-directory directory output mapping
  226. #:optional (store (%store-directory)))
  227. "Copy DIRECTORY to OUTPUT, replacing strings according to MAPPING, a list of
  228. file name pairs."
  229. (define hash-mapping
  230. ;; List of hash/replacement pairs, where the hash is a nix-base32 string
  231. ;; and the replacement is a string that includes the replacement's name,
  232. ;; like "r837zajjc1q8z9hph4b6860a9c05blyy-openssl-1.0.2j".
  233. (let* ((prefix (string-append store "/"))
  234. (start (string-length prefix))
  235. (end (+ start hash-length)))
  236. (define (valid-hash? h)
  237. (every nix-base32-char? (string->list h)))
  238. (define (hash+rest s)
  239. (and (< end (string-length s))
  240. (let ((hash (substring s start end))
  241. (all (substring s start)))
  242. (and (string-prefix? prefix s)
  243. (valid-hash? hash)
  244. (eqv? #\- (string-ref s end))
  245. (list hash all)))))
  246. (map (match-lambda
  247. (((= hash+rest (origin-hash origin-string))
  248. .
  249. (= hash+rest (replacement-hash replacement-string)))
  250. (unless (= (string-length origin-string)
  251. (string-length replacement-string))
  252. (error "replacement length differs from the original length"
  253. origin-string replacement-string))
  254. (cons origin-hash (string->utf8 replacement-string)))
  255. ((origin . replacement)
  256. (error "invalid replacement" origin replacement)))
  257. mapping)))
  258. (define replacement-table
  259. (alist->vhash hash-mapping))
  260. (define prefix-len
  261. (string-length directory))
  262. (define (destination file)
  263. (string-append output (string-drop file prefix-len)))
  264. (define (rewrite-leaf file)
  265. (let ((stat (lstat file))
  266. (dest (destination file)))
  267. (mkdir-p* (dirname dest))
  268. (case (stat:type stat)
  269. ((symlink)
  270. (let ((target (readlink file)))
  271. (symlink (call-with-output-string
  272. (lambda (output)
  273. (replace-store-references (open-input-string target)
  274. output replacement-table
  275. store)))
  276. dest)))
  277. ((regular)
  278. (call-with-input-file file
  279. (lambda (input)
  280. (call-with-output-file dest
  281. (lambda (output)
  282. (replace-store-references input output replacement-table
  283. store)
  284. (chmod output (stat:perms stat)))))))
  285. ((directory)
  286. (mkdir-p* dest))
  287. (else
  288. (error "unsupported file type" stat)))))
  289. ;; Use 'exit-on-exception' to force an exit upon I/O errors, given that
  290. ;; 'n-par-for-each' silently swallows exceptions.
  291. ;; See <http://bugs.gnu.org/23581>.
  292. (n-par-for-each (parallel-job-count)
  293. (exit-on-exception rewrite-leaf)
  294. (find-files directory (const #t)
  295. #:directories? #t))
  296. (rename-matching-files output mapping))
  297. (define %graft-hooks
  298. ;; Default list of hooks run after grafting.
  299. (list graft-debug-links))
  300. (define* (graft old-outputs new-outputs mapping
  301. #:key (log-port (current-output-port))
  302. (hooks %graft-hooks))
  303. "Apply the grafts described by MAPPING on OLD-OUTPUTS, leading to
  304. NEW-OUTPUTS. MAPPING must be a list of file name pairs; OLD-OUTPUTS and
  305. NEW-OUTPUTS are lists of output name/file name pairs."
  306. (for-each (lambda (input output)
  307. (format log-port "grafting '~a' -> '~a'...~%" input output)
  308. (force-output)
  309. (rewrite-directory input output mapping))
  310. (match old-outputs
  311. (((names . files) ...)
  312. files))
  313. (match new-outputs
  314. (((names . files) ...)
  315. files)))
  316. (for-each (lambda (hook)
  317. (hook old-outputs new-outputs mapping
  318. #:log-port log-port))
  319. hooks))
  320. ;;; graft.scm ends here