utils: Add `copy-recursively'; use it.
[jackhill/guix/guix.git] / guix / build / utils.scm
CommitLineData
c36db98c
LC
1;;; Guix --- Nix package management from Guile. -*- coding: utf-8 -*-
2;;; Copyright (C) 2012 Ludovic Courtès <ludo@gnu.org>
3;;;
4;;; This file is part of Guix.
5;;;
6;;; Guix is free software; you can redistribute it and/or modify it
7;;; under the terms of the GNU General Public License as published by
8;;; the Free Software Foundation; either version 3 of the License, or (at
9;;; your option) any later version.
10;;;
11;;; Guix is distributed in the hope that it will be useful, but
12;;; WITHOUT ANY WARRANTY; without even the implied warranty of
13;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14;;; GNU General Public License for more details.
15;;;
16;;; You should have received a copy of the GNU General Public License
17;;; along with Guix. If not, see <http://www.gnu.org/licenses/>.
18
19(define-module (guix build utils)
20 #:use-module (srfi srfi-1)
b0e0d0e9 21 #:use-module (srfi srfi-11)
c0746cc9 22 #:use-module (ice-9 ftw)
b0e0d0e9
LC
23 #:use-module (ice-9 match)
24 #:use-module (ice-9 regex)
25 #:use-module (ice-9 rdelim)
ebe2f31f
LC
26 #:use-module (rnrs bytevectors)
27 #:use-module (rnrs io ports)
c36db98c 28 #:export (directory-exists?
b0e0d0e9 29 with-directory-excursion
7da95264 30 mkdir-p
c0746cc9 31 copy-recursively
b0e0d0e9 32 set-path-environment-variable
ebe2f31f
LC
33 search-path-as-string->list
34 list->search-path-as-string
b0e0d0e9
LC
35 alist-cons-before
36 alist-cons-after
37 alist-replace
dcd72906 38 with-atomic-file-replacement
10c87717 39 substitute
ebe2f31f
LC
40 substitute*
41 dump-port
91133c2d
LC
42 patch-shebang
43 fold-port-matches
44 remove-store-references))
b0e0d0e9
LC
45
46\f
47;;;
48;;; Directories.
49;;;
c36db98c
LC
50
51(define (directory-exists? dir)
52 "Return #t if DIR exists and is a directory."
9f55cf8d
LC
53 (let ((s (stat dir #f)))
54 (and s
55 (eq? 'directory (stat:type s)))))
c36db98c 56
b0e0d0e9
LC
57(define-syntax-rule (with-directory-excursion dir body ...)
58 "Run BODY with DIR as the process's current directory."
59 (let ((init (getcwd)))
60 (dynamic-wind
61 (lambda ()
62 (chdir dir))
63 (lambda ()
64 body ...)
65 (lambda ()
66 (chdir init)))))
67
7da95264
LC
68(define (mkdir-p dir)
69 "Create directory DIR and all its ancestors."
70 (define absolute?
71 (string-prefix? "/" dir))
72
73 (define not-slash
74 (char-set-complement (char-set #\/)))
75
76 (let loop ((components (string-tokenize dir not-slash))
77 (root (if absolute?
78 ""
79 ".")))
80 (match components
81 ((head tail ...)
82 (let ((path (string-append root "/" head)))
83 (catch 'system-error
84 (lambda ()
85 (mkdir path)
86 (loop tail path))
87 (lambda args
88 (if (= EEXIST (system-error-errno args))
89 (loop tail path)
90 (apply throw args))))))
91 (() #t))))
92
c0746cc9
LC
93(define* (copy-recursively source destination
94 #:optional (log (current-output-port)))
95 "Copy SOURCE directory to DESTINATION."
96 (define strip-source
97 (let ((len (string-length source)))
98 (lambda (file)
99 (substring file len))))
100
101 (file-system-fold (const #t) ; enter?
102 (lambda (file stat result) ; leaf
103 (let ((dest (string-append destination
104 (strip-source file))))
105 (format log "`~a' -> `~a'~%" file dest)
106 (copy-file file dest)))
107 (lambda (dir stat result) ; down
108 (mkdir-p (string-append destination
109 (strip-source dir))))
110 (lambda (dir stat result) ; up
111 result)
112 (const #t) ; skip
113 (lambda (file stat errno result)
114 (format (current-error-port) "i/o error: ~a: ~a~%"
115 file (strerror errno))
116 #f)
117 #t
118 source))
119
b0e0d0e9
LC
120\f
121;;;
122;;; Search paths.
123;;;
124
c36db98c
LC
125(define (search-path-as-list sub-directories input-dirs)
126 "Return the list of directories among SUB-DIRECTORIES that exist in
127INPUT-DIRS. Example:
128
129 (search-path-as-list '(\"share/emacs/site-lisp\" \"share/emacs/24.1\")
130 (list \"/package1\" \"/package2\" \"/package3\"))
131 => (\"/package1/share/emacs/site-lisp\"
132 \"/package3/share/emacs/site-lisp\")
133
134"
135 (append-map (lambda (input)
136 (filter-map (lambda (dir)
137 (let ((dir (string-append input "/"
138 dir)))
139 (and (directory-exists? dir)
140 dir)))
141 sub-directories))
142 input-dirs))
143
144(define (list->search-path-as-string lst separator)
145 (string-join lst separator))
146
ebe2f31f
LC
147(define* (search-path-as-string->list path #:optional (separator #\:))
148 (string-tokenize path (char-set-complement (char-set separator))))
149
c36db98c
LC
150(define* (set-path-environment-variable env-var sub-directories input-dirs
151 #:key (separator ":"))
152 "Look for each of SUB-DIRECTORIES in INPUT-DIRS. Set ENV-VAR to a
153SEPARATOR-separated path accordingly. Example:
154
155 (set-path-environment-variable \"PKG_CONFIG\"
156 '(\"lib/pkgconfig\")
157 (list package1 package2))
158"
9d9ef458
LC
159 (let* ((path (search-path-as-list sub-directories input-dirs))
160 (value (list->search-path-as-string path separator)))
161 (setenv env-var value)
162 (format #t "environment variable `~a' set to `~a'~%"
163 env-var value)))
b0e0d0e9
LC
164
165\f
166;;;
167;;; Phases.
168;;;
169;;; In (guix build gnu-build-system), there are separate phases (configure,
170;;; build, test, install). They are represented as a list of name/procedure
171;;; pairs. The following procedures make it easy to change the list of
172;;; phases.
173;;;
174
175(define* (alist-cons-before reference key value alist
176 #:optional (key=? equal?))
177 "Insert the KEY/VALUE pair before the first occurrence of a pair whose key
178is REFERENCE in ALIST. Use KEY=? to compare keys."
179 (let-values (((before after)
180 (break (match-lambda
181 ((k . _)
182 (key=? k reference)))
183 alist)))
184 (append before (alist-cons key value after))))
185
186(define* (alist-cons-after reference key value alist
187 #:optional (key=? equal?))
188 "Insert the KEY/VALUE pair after the first occurrence of a pair whose key
189is REFERENCE in ALIST. Use KEY=? to compare keys."
190 (let-values (((before after)
191 (break (match-lambda
192 ((k . _)
193 (key=? k reference)))
194 alist)))
195 (match after
196 ((reference after ...)
197 (append before (cons* reference `(,key . ,value) after)))
198 (()
199 (append before `((,key . ,value)))))))
200
201(define* (alist-replace key value alist #:optional (key=? equal?))
202 "Replace the first pair in ALIST whose car is KEY with the KEY/VALUE pair.
203An error is raised when no such pair exists."
204 (let-values (((before after)
205 (break (match-lambda
206 ((k . _)
207 (key=? k key)))
208 alist)))
209 (match after
210 ((_ after ...)
211 (append before (alist-cons key value after))))))
212
213\f
214;;;
215;;; Text substitution (aka. sed).
216;;;
217
dcd72906
LC
218(define (with-atomic-file-replacement file proc)
219 "Call PROC with two arguments: an input port for FILE, and an output
220port for the file that is going to replace FILE. Upon success, FILE is
221atomically replaced by what has been written to the output port, and
222PROC's result is returned."
223 (let* ((template (string-append file ".XXXXXX"))
d9dbab18
LC
224 (out (mkstemp! template))
225 (mode (stat:mode (stat file))))
b0e0d0e9
LC
226 (with-throw-handler #t
227 (lambda ()
228 (call-with-input-file file
229 (lambda (in)
dcd72906
LC
230 (let ((result (proc in out)))
231 (close out)
232 (chmod template mode)
233 (rename-file template file)
234 result))))
b0e0d0e9
LC
235 (lambda (key . args)
236 (false-if-exception (delete-file template))))))
237
dcd72906
LC
238(define (substitute file pattern+procs)
239 "PATTERN+PROCS is a list of regexp/two-argument procedure. For each line
240of FILE, and for each PATTERN that it matches, call the corresponding PROC
241as (PROC LINE MATCHES); PROC must return the line that will be written as a
242substitution of the original line."
243 (let ((rx+proc (map (match-lambda
244 (((? regexp? pattern) . proc)
245 (cons pattern proc))
246 ((pattern . proc)
247 (cons (make-regexp pattern regexp/extended)
248 proc)))
249 pattern+procs)))
250 (with-atomic-file-replacement file
251 (lambda (in out)
252 (let loop ((line (read-line in 'concat)))
253 (if (eof-object? line)
254 #t
255 (let ((line (fold (lambda (r+p line)
256 (match r+p
257 ((regexp . proc)
258 (match (list-matches regexp line)
259 ((and m+ (_ _ ...))
260 (proc line m+))
261 (_ line)))))
262 line
263 rx+proc)))
264 (display line out)
265 (loop (read-line in 'concat)))))))))
266
10c87717
LC
267
268(define-syntax let-matches
269 ;; Helper macro for `substitute*'.
270 (syntax-rules (_)
271 ((let-matches index match (_ vars ...) body ...)
272 (let-matches (+ 1 index) match (vars ...)
273 body ...))
274 ((let-matches index match (var vars ...) body ...)
275 (let ((var (match:substring match index)))
276 (let-matches (+ 1 index) match (vars ...)
277 body ...)))
278 ((let-matches index match () body ...)
279 (begin body ...))))
280
8773648e
LC
281(define-syntax substitute*
282 (syntax-rules ()
283 "Substitute REGEXP in FILE by the string returned by BODY. BODY is
10c87717
LC
284evaluated with each MATCH-VAR bound to the corresponding positional regexp
285sub-expression. For example:
286
4fa697e9
LC
287 (substitute* file
288 ((\"hello\")
289 \"good morning\\n\")
290 ((\"foo([a-z]+)bar(.*)$\" all letters end)
291 (string-append \"baz\" letter end)))
292
293Here, anytime a line of FILE contains \"hello\", it is replaced by \"good
294morning\". Anytime a line of FILE matches the second regexp, ALL is bound to
295the complete match, LETTERS is bound to the first sub-expression, and END is
296bound to the last one.
297
298When one of the MATCH-VAR is `_', no variable is bound to the corresponding
299match substring."
8773648e
LC
300 ((substitute* (file ...) clause ...)
301 (begin
302 (substitute* file clause ...)
303 ...))
304 ((substitute* file ((regexp match-var ...) body ...) ...)
305 (substitute file
306 (list (cons regexp
8197c978
LC
307 (lambda (l m+)
308 ;; Iterate over matches M+ and return the
309 ;; modified line based on L.
310 (let loop ((m* m+) ; matches
311 (o 0) ; offset in L
312 (r '())) ; result
313 (match m*
314 (()
315 (let ((r (cons (substring l o) r)))
316 (string-concatenate-reverse r)))
317 ((m . rest)
318 (let-matches 0 m (match-var ...)
319 (loop rest
320 (match:end m)
321 (cons*
322 (begin body ...)
323 (substring l o (match:start m))
324 r))))))))
8773648e 325 ...)))))
10c87717 326
ebe2f31f
LC
327\f
328;;;
329;;; Patching shebangs---e.g., /bin/sh -> /nix/store/xyz...-bash/bin/sh.
330;;;
331
332(define (dump-port in out)
333 "Read as much data as possible from IN and write it to OUT."
334 (define buffer-size 4096)
335 (define buffer
336 (make-bytevector buffer-size))
337
338 (let loop ((bytes (get-bytevector-n! in buffer 0 buffer-size)))
339 (or (eof-object? bytes)
340 (begin
341 (put-bytevector out buffer 0 bytes)
342 (loop (get-bytevector-n! in buffer 0 buffer-size))))))
343
344(define patch-shebang
525a59d6
LC
345 (let ((shebang-rx (make-regexp "^[[:blank:]]*([[:graph:]]+)(.*)$")))
346 (lambda* (file
347 #:optional (path (search-path-as-string->list (getenv "PATH"))))
348 "Replace the #! interpreter file name in FILE by a valid one found in
349PATH, when FILE actually starts with a shebang. Return #t when FILE was
350patched, #f otherwise."
ebe2f31f
LC
351 (define (patch p interpreter rest-of-line)
352 (let* ((template (string-append file ".XXXXXX"))
353 (out (mkstemp! template))
354 (mode (stat:mode (stat file))))
355 (with-throw-handler #t
356 (lambda ()
357 (format out "#!~a~a~%"
358 interpreter rest-of-line)
359 (dump-port p out)
360 (close out)
361 (chmod template mode)
362 (rename-file template file)
363 #t)
364 (lambda (key . args)
365 (format (current-error-port)
366 "patch-shebang: ~a: error: ~a ~s~%"
367 file key args)
368 (false-if-exception (delete-file template))
369 #f))))
370
371 (with-fluids ((%default-port-encoding #f)) ; ASCII
372 (call-with-input-file file
373 (lambda (p)
374 (and (eq? #\# (read-char p))
375 (eq? #\! (read-char p))
376 (let ((line (false-if-exception (read-line p))))
377 (and=> (and line (regexp-exec shebang-rx line))
378 (lambda (m)
525a59d6
LC
379 (let* ((cmd (match:substring m 1))
380 (bin (search-path path
381 (basename cmd))))
ebe2f31f 382 (if bin
525a59d6
LC
383 (if (string=? bin cmd)
384 #f ; nothing to do
385 (begin
386 (format (current-error-port)
387 "patch-shebang: ~a: changing `~a' to `~a'~%"
388 file cmd bin)
389 (patch p bin (match:substring m 2))))
ebe2f31f
LC
390 (begin
391 (format (current-error-port)
392 "patch-shebang: ~a: warning: no binary for interpreter `~a' found in $PATH~%"
525a59d6 393 file (basename cmd))
ebe2f31f 394 #f)))))))))))))
10c87717 395
91133c2d
LC
396(define* (fold-port-matches proc init pattern port
397 #:optional (unmatched (lambda (_ r) r)))
398 "Read from PORT character-by-character; for each match against
399PATTERN, call (PROC MATCH RESULT), where RESULT is seeded with INIT.
400PATTERN is a list of SRFI-14 char-sets. Call (UNMATCHED CHAR RESULT)
401for each unmatched character."
402 (define initial-pattern
403 ;; The poor developer's regexp.
404 (if (string? pattern)
405 (map char-set (string->list pattern))
406 pattern))
407
408 ;; Note: we're not really striving for performance here...
409 (let loop ((chars '())
410 (pattern initial-pattern)
411 (matched '())
412 (result init))
413 (cond ((null? chars)
414 (loop (list (get-char port))
415 pattern
416 matched
417 result))
418 ((null? pattern)
419 (loop chars
420 initial-pattern
421 '()
422 (proc (list->string (reverse matched)) result)))
423 ((eof-object? (car chars))
424 (fold-right unmatched result matched))
425 ((char-set-contains? (car pattern) (car chars))
426 (loop (cdr chars)
427 (cdr pattern)
428 (cons (car chars) matched)
429 result))
430 ((null? matched) ; common case
431 (loop (cdr chars)
432 pattern
433 matched
434 (unmatched (car chars) result)))
435 (else
436 (let ((matched (reverse matched)))
437 (loop (append (cdr matched) chars)
438 initial-pattern
439 '()
440 (unmatched (car matched) result)))))))
441
442(define* (remove-store-references file
443 #:optional (store (or (getenv "NIX_STORE")
444 "/nix/store")))
445 "Remove from FILE occurrences of file names in STORE; return #t when
446store paths were encountered in FILE, #f otherwise. This procedure is
447known as `nuke-refs' in Nixpkgs."
448 (define pattern
449 (let ((nix-base32-chars
450 '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9
451 #\a #\b #\c #\d #\f #\g #\h #\i #\j #\k #\l #\m #\n
452 #\p #\q #\r #\s #\v #\w #\x #\y #\z)))
453 `(,@(map char-set (string->list store))
454 ,(char-set #\/)
455 ,@(make-list 32 (list->char-set nix-base32-chars))
456 ,(char-set #\-))))
457
458 (with-fluids ((%default-port-encoding #f))
459 (with-atomic-file-replacement file
460 (lambda (in out)
461 ;; We cannot use `regexp-exec' here because it cannot deal with
462 ;; strings containing NUL characters.
463 (format #t "removing store references from `~a'...~%" file)
464 (setvbuf in _IOFBF 65536)
465 (setvbuf out _IOFBF 65536)
466 (fold-port-matches (lambda (match result)
467 (put-string out store)
468 (put-char out #\/)
469 (put-string out
470 "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-")
471 #t)
472 #f
473 pattern
474 in
475 (lambda (char result)
476 (put-char out char)
477 result))))))
478
b0e0d0e9
LC
479;;; Local Variables:
480;;; eval: (put 'call-with-output-file/atomic 'scheme-indent-function 1)
481;;; eval: (put 'with-throw-handler 'scheme-indent-function 1)
8197c978 482;;; eval: (put 'let-matches 'scheme-indent-function 3)
dcd72906 483;;; eval: (put 'with-atomic-file-replacement 'scheme-indent-function 1)
b0e0d0e9 484;;; End: