union.scm 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2012, 2013, 2014, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2014 Mark H Weaver <mhw@netris.org>
  4. ;;; Copyright © 2017 Huang Ying <huang.ying.caritas@gmail.com>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (guix build union)
  21. #:use-module (ice-9 match)
  22. #:use-module (ice-9 format)
  23. #:use-module (srfi srfi-1)
  24. #:use-module (srfi srfi-26)
  25. #:use-module (rnrs bytevectors)
  26. #:use-module (rnrs io ports)
  27. #:export (union-build
  28. resolve-collision/default
  29. relative-file-name
  30. symlink-relative))
  31. ;;; Commentary:
  32. ;;;
  33. ;;; Build a directory that is the union of a set of directories, using
  34. ;;; symbolic links.
  35. ;;;
  36. ;;; Code:
  37. ;; This code can be used with the bootstrap Guile, which is Guile 2.0, so
  38. ;; provide a compatibility layer.
  39. (cond-expand
  40. ((and guile-2 (not guile-2.2))
  41. (define (setvbuf port mode . rest)
  42. (apply (@ (guile) setvbuf) port
  43. (match mode
  44. ('line _IOLBF)
  45. ('block _IOFBF)
  46. ('none _IONBF))
  47. rest)))
  48. (else #f))
  49. (define (files-in-directory dirname)
  50. (let ((dir (opendir dirname)))
  51. (let loop ((files '()))
  52. (match (readdir dir)
  53. ((or "." "..")
  54. (loop files))
  55. ((? eof-object?)
  56. (closedir dir)
  57. (sort files string<?))
  58. (file
  59. (loop (cons file files)))))))
  60. (define (file-is-directory? file)
  61. (match (stat file #f)
  62. (#f #f) ;maybe a dangling symlink
  63. (st (eq? 'directory (stat:type st)))))
  64. (define (file=? file1 file2)
  65. "Return #t if FILE1 and FILE2 are regular files and their contents are
  66. identical, #f otherwise."
  67. (let ((st1 (stat file1 #f))
  68. (st2 (stat file2 #f)))
  69. ;; When deduplication is enabled, identical files share the same inode.
  70. (and st1 st2
  71. (or (= (stat:ino st1) (stat:ino st2))
  72. (and (eq? (stat:type st1) 'regular)
  73. (eq? (stat:type st2) 'regular)
  74. (= (stat:size st1) (stat:size st2))
  75. (call-with-input-file file1
  76. (lambda (port1)
  77. (call-with-input-file file2
  78. (lambda (port2)
  79. (define len 8192)
  80. (define buf1 (make-bytevector len))
  81. (define buf2 (make-bytevector len))
  82. (let loop ()
  83. (let ((n1 (get-bytevector-n! port1 buf1 0 len))
  84. (n2 (get-bytevector-n! port2 buf2 0 len)))
  85. (and (equal? n1 n2)
  86. (or (eof-object? n1)
  87. (loop))))))))))))))
  88. (define %harmless-collisions
  89. ;; This is a list of files that are known to collide, but for which emitting
  90. ;; a warning doesn't make sense. For example, "icon-theme.cache" is
  91. ;; regenerated by a profile hook which shadows the file provided by
  92. ;; individual packages, and "gschemas.compiled" is made available to
  93. ;; applications via 'glib-or-gtk-build-system'; "etc/ld.so.cache" is created
  94. ;; for most packages.
  95. '("icon-theme.cache" "gschemas.compiled" "ld.so.cache"))
  96. (define (resolve+warn-if-harmful resolve files)
  97. "Same as (resolve files), but print a warning if the resolved file is not
  98. considered harmless. Also warn if the resolver doesn't pick any file."
  99. (let ((file (resolve files)))
  100. (cond
  101. ((not file)
  102. (format (current-error-port)
  103. "~%warning: collision encountered:~%~{ ~a~%~}"
  104. files)
  105. (format (current-error-port) "warning: not choosing any file~%"))
  106. (((negate member) (basename file) %harmless-collisions)
  107. (format (current-error-port)
  108. "~%warning: collision encountered:~%~{ ~a~%~}"
  109. files)
  110. (format (current-error-port) "warning: choosing ~a~%" file)))
  111. file))
  112. (define (resolve-collision/default files)
  113. (resolve+warn-if-harmful first files))
  114. (define* (union-build output inputs
  115. #:key (log-port (current-error-port))
  116. (create-all-directories? #f)
  117. (symlink symlink)
  118. (resolve-collision resolve-collision/default))
  119. "Build in the OUTPUT directory a symlink tree that is the union of all the
  120. INPUTS, using SYMLINK to create symlinks. As a special case, if
  121. CREATE-ALL-DIRECTORIES?, creates the subdirectories in the output directory to
  122. make sure the caller can modify them later.
  123. When two or more regular files collide, call RESOLVE-COLLISION with the list
  124. of colliding files and use the one that it returns; or, if RESOLVE-COLLISION
  125. returns #f, skip the faulty file altogether."
  126. (define (symlink* input output)
  127. (format log-port "`~a' ~~> `~a'~%" input output)
  128. (symlink input output))
  129. (define (resolve-collisions output dirs files)
  130. (cond ((null? dirs)
  131. ;; The inputs are all files.
  132. (match (resolve-collision files)
  133. (#f #f)
  134. ((? string? file)
  135. (symlink* file output))))
  136. (else
  137. ;; The inputs are a mixture of files and directories
  138. (error "union-build: collision between file and directories"
  139. `((files ,files) (dirs ,dirs))))))
  140. (define (union output inputs)
  141. (match inputs
  142. ((input)
  143. ;; There's only one input, so just make a link unless
  144. ;; create-all-directories?.
  145. (if (and create-all-directories? (file-is-directory? input))
  146. (union-of-directories output inputs)
  147. (symlink* input output)))
  148. (_
  149. (call-with-values (lambda () (partition file-is-directory? inputs))
  150. (match-lambda*
  151. ((dirs ())
  152. ;; All inputs are directories.
  153. (union-of-directories output dirs))
  154. ((() (file (? (cut file=? <> file)) ...))
  155. ;; There are no directories, and all files have the same contents,
  156. ;; so there's no conflict.
  157. (symlink* file output))
  158. ((dirs files)
  159. (resolve-collisions output dirs files)))))))
  160. (define (union-of-directories output dirs)
  161. ;; Create a new directory where we will merge the input directories.
  162. (mkdir output)
  163. ;; Build a hash table mapping each file to a list of input
  164. ;; directories containing that file.
  165. (let ((table (make-hash-table)))
  166. (define (add-to-table! file dir)
  167. (hash-set! table file (cons dir (hash-ref table file '()))))
  168. ;; Populate the table.
  169. (for-each (lambda (dir)
  170. (for-each (cut add-to-table! <> dir)
  171. (files-in-directory dir)))
  172. dirs)
  173. ;; Now iterate over the table and recursively
  174. ;; perform a union for each entry.
  175. (hash-for-each (lambda (file dirs-with-file)
  176. (union (string-append output "/" file)
  177. (map (cut string-append <> "/" file)
  178. (reverse dirs-with-file))))
  179. table)))
  180. (setvbuf (current-output-port) 'line)
  181. (setvbuf (current-error-port) 'line)
  182. (when (file-port? log-port)
  183. (setvbuf log-port 'line))
  184. (union-of-directories output (delete-duplicates inputs)))
  185. ;;;
  186. ;;; Relative symlinks.
  187. ;;;
  188. (define %not-slash
  189. (char-set-complement (char-set #\/)))
  190. (define (relative-file-name reference file)
  191. "Given REFERENCE and FILE, both of which are absolute file names, return the
  192. file name of FILE relative to REFERENCE.
  193. (relative-file-name \"/gnu/store/foo\" \"/gnu/store/bin/bar\")
  194. => \"../bin/bar\"
  195. Note that this is from a purely lexical standpoint; conversely, \"..\" is
  196. *not* resolved lexically on POSIX in the presence of symlinks."
  197. (if (and (string-prefix? "/" file) (string-prefix? "/" reference))
  198. (let loop ((reference (string-tokenize reference %not-slash))
  199. (file (string-tokenize file %not-slash)))
  200. (define (finish)
  201. (string-join (append (make-list (length reference) "..") file)
  202. "/"))
  203. (match reference
  204. (()
  205. (finish))
  206. ((head . tail)
  207. (match file
  208. (()
  209. (finish))
  210. ((head* . tail*)
  211. (if (string=? head head*)
  212. (loop tail tail*)
  213. (finish)))))))
  214. file))
  215. (define (symlink-relative old new)
  216. "Assuming both OLD and NEW are absolute file names, make NEW a symlink to
  217. OLD, but using a relative file name."
  218. (symlink (relative-file-name (dirname new) old)
  219. new))
  220. ;;; union.scm ends here