utils: Add `which'.
[jackhill/guix/guix.git] / guix / build / utils.scm
CommitLineData
c36db98c 1;;; Guix --- Nix package management from Guile. -*- coding: utf-8 -*-
93b03575 2;;; Copyright (C) 2012, 2013 Ludovic Courtès <ludo@gnu.org>
c36db98c
LC
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?
d0084152 29 executable-file?
c0895112 30 call-with-ascii-input-file
b0e0d0e9 31 with-directory-excursion
7da95264 32 mkdir-p
c0746cc9 33 copy-recursively
4c261f41
LC
34 find-files
35
b0e0d0e9 36 set-path-environment-variable
ebe2f31f
LC
37 search-path-as-string->list
38 list->search-path-as-string
7584f822
LC
39 which
40
b0e0d0e9
LC
41 alist-cons-before
42 alist-cons-after
43 alist-replace
dcd72906 44 with-atomic-file-replacement
10c87717 45 substitute
ebe2f31f
LC
46 substitute*
47 dump-port
bc5bf85f 48 set-file-time
91133c2d 49 patch-shebang
c0895112 50 patch-makefile-SHELL
91133c2d
LC
51 fold-port-matches
52 remove-store-references))
b0e0d0e9
LC
53
54\f
55;;;
56;;; Directories.
57;;;
c36db98c
LC
58
59(define (directory-exists? dir)
60 "Return #t if DIR exists and is a directory."
9f55cf8d
LC
61 (let ((s (stat dir #f)))
62 (and s
63 (eq? 'directory (stat:type s)))))
c36db98c 64
d0084152
LC
65(define (executable-file? file)
66 "Return #t if FILE exists and is executable."
67 (let ((s (stat file #f)))
68 (and s
69 (not (zero? (logand (stat:mode s) #o100))))))
70
c0895112
LC
71(define (call-with-ascii-input-file file proc)
72 "Open FILE as an ASCII or binary file, and pass the resulting port to
73PROC. FILE is closed when PROC's dynamic extent is left. Return the
74return values of applying PROC to the port."
75 (let ((port (with-fluids ((%default-port-encoding #f))
76 ;; Use "b" so that `open-file' ignores `coding:' cookies.
77 (open-file file "rb"))))
78 (dynamic-wind
79 (lambda ()
80 #t)
81 (lambda ()
82 (proc port))
83 (lambda ()
84 (close-input-port port)))))
85
b0e0d0e9
LC
86(define-syntax-rule (with-directory-excursion dir body ...)
87 "Run BODY with DIR as the process's current directory."
88 (let ((init (getcwd)))
89 (dynamic-wind
90 (lambda ()
91 (chdir dir))
92 (lambda ()
93 body ...)
94 (lambda ()
95 (chdir init)))))
96
7da95264
LC
97(define (mkdir-p dir)
98 "Create directory DIR and all its ancestors."
99 (define absolute?
100 (string-prefix? "/" dir))
101
102 (define not-slash
103 (char-set-complement (char-set #\/)))
104
105 (let loop ((components (string-tokenize dir not-slash))
106 (root (if absolute?
107 ""
108 ".")))
109 (match components
110 ((head tail ...)
111 (let ((path (string-append root "/" head)))
112 (catch 'system-error
113 (lambda ()
114 (mkdir path)
115 (loop tail path))
116 (lambda args
117 (if (= EEXIST (system-error-errno args))
118 (loop tail path)
119 (apply throw args))))))
120 (() #t))))
121
c0746cc9
LC
122(define* (copy-recursively source destination
123 #:optional (log (current-output-port)))
124 "Copy SOURCE directory to DESTINATION."
125 (define strip-source
126 (let ((len (string-length source)))
127 (lambda (file)
128 (substring file len))))
129
130 (file-system-fold (const #t) ; enter?
131 (lambda (file stat result) ; leaf
132 (let ((dest (string-append destination
133 (strip-source file))))
134 (format log "`~a' -> `~a'~%" file dest)
135 (copy-file file dest)))
136 (lambda (dir stat result) ; down
137 (mkdir-p (string-append destination
138 (strip-source dir))))
139 (lambda (dir stat result) ; up
140 result)
141 (const #t) ; skip
142 (lambda (file stat errno result)
143 (format (current-error-port) "i/o error: ~a: ~a~%"
144 file (strerror errno))
145 #f)
146 #t
147 source))
148
4c261f41
LC
149(define (find-files dir regexp)
150 "Return the list of files under DIR whose basename matches REGEXP."
151 (define file-rx
152 (if (regexp? regexp)
153 regexp
154 (make-regexp regexp)))
155
156 (file-system-fold (const #t)
157 (lambda (file stat result) ; leaf
158 (if (regexp-exec file-rx (basename file))
159 (cons file result)
160 result))
161 (lambda (dir stat result) ; down
162 result)
163 (lambda (dir stat result) ; up
164 result)
165 (lambda (file stat result) ; skip
166 result)
167 (lambda (file stat errno result)
168 (format (current-error-port) "find-files: ~a: ~a~%"
169 file (strerror errno))
170 #f)
171 '()
172 dir))
173
b0e0d0e9
LC
174\f
175;;;
176;;; Search paths.
177;;;
178
c36db98c
LC
179(define (search-path-as-list sub-directories input-dirs)
180 "Return the list of directories among SUB-DIRECTORIES that exist in
181INPUT-DIRS. Example:
182
183 (search-path-as-list '(\"share/emacs/site-lisp\" \"share/emacs/24.1\")
184 (list \"/package1\" \"/package2\" \"/package3\"))
185 => (\"/package1/share/emacs/site-lisp\"
186 \"/package3/share/emacs/site-lisp\")
187
188"
189 (append-map (lambda (input)
190 (filter-map (lambda (dir)
191 (let ((dir (string-append input "/"
192 dir)))
193 (and (directory-exists? dir)
194 dir)))
195 sub-directories))
196 input-dirs))
197
198(define (list->search-path-as-string lst separator)
199 (string-join lst separator))
200
ebe2f31f
LC
201(define* (search-path-as-string->list path #:optional (separator #\:))
202 (string-tokenize path (char-set-complement (char-set separator))))
203
c36db98c
LC
204(define* (set-path-environment-variable env-var sub-directories input-dirs
205 #:key (separator ":"))
206 "Look for each of SUB-DIRECTORIES in INPUT-DIRS. Set ENV-VAR to a
207SEPARATOR-separated path accordingly. Example:
208
209 (set-path-environment-variable \"PKG_CONFIG\"
210 '(\"lib/pkgconfig\")
211 (list package1 package2))
212"
9d9ef458
LC
213 (let* ((path (search-path-as-list sub-directories input-dirs))
214 (value (list->search-path-as-string path separator)))
215 (setenv env-var value)
216 (format #t "environment variable `~a' set to `~a'~%"
217 env-var value)))
b0e0d0e9 218
7584f822
LC
219(define (which program)
220 "Return the complete file name for PROGRAM as found in $PATH, or #f if
221PROGRAM could not be found."
222 (search-path (search-path-as-string->list (getenv "PATH"))
223 program))
224
b0e0d0e9
LC
225\f
226;;;
227;;; Phases.
228;;;
229;;; In (guix build gnu-build-system), there are separate phases (configure,
230;;; build, test, install). They are represented as a list of name/procedure
231;;; pairs. The following procedures make it easy to change the list of
232;;; phases.
233;;;
234
235(define* (alist-cons-before reference key value alist
236 #:optional (key=? equal?))
237 "Insert the KEY/VALUE pair before the first occurrence of a pair whose key
238is REFERENCE in ALIST. Use KEY=? to compare keys."
239 (let-values (((before after)
240 (break (match-lambda
241 ((k . _)
242 (key=? k reference)))
243 alist)))
244 (append before (alist-cons key value after))))
245
246(define* (alist-cons-after reference key value alist
247 #:optional (key=? equal?))
248 "Insert the KEY/VALUE pair after the first occurrence of a pair whose key
249is REFERENCE in ALIST. Use KEY=? to compare keys."
250 (let-values (((before after)
251 (break (match-lambda
252 ((k . _)
253 (key=? k reference)))
254 alist)))
255 (match after
256 ((reference after ...)
257 (append before (cons* reference `(,key . ,value) after)))
258 (()
259 (append before `((,key . ,value)))))))
260
261(define* (alist-replace key value alist #:optional (key=? equal?))
262 "Replace the first pair in ALIST whose car is KEY with the KEY/VALUE pair.
263An error is raised when no such pair exists."
264 (let-values (((before after)
265 (break (match-lambda
266 ((k . _)
267 (key=? k key)))
268 alist)))
269 (match after
270 ((_ after ...)
271 (append before (alist-cons key value after))))))
272
273\f
274;;;
275;;; Text substitution (aka. sed).
276;;;
277
dcd72906
LC
278(define (with-atomic-file-replacement file proc)
279 "Call PROC with two arguments: an input port for FILE, and an output
280port for the file that is going to replace FILE. Upon success, FILE is
281atomically replaced by what has been written to the output port, and
282PROC's result is returned."
283 (let* ((template (string-append file ".XXXXXX"))
d9dbab18
LC
284 (out (mkstemp! template))
285 (mode (stat:mode (stat file))))
b0e0d0e9
LC
286 (with-throw-handler #t
287 (lambda ()
288 (call-with-input-file file
289 (lambda (in)
dcd72906
LC
290 (let ((result (proc in out)))
291 (close out)
292 (chmod template mode)
293 (rename-file template file)
294 result))))
b0e0d0e9
LC
295 (lambda (key . args)
296 (false-if-exception (delete-file template))))))
297
dcd72906
LC
298(define (substitute file pattern+procs)
299 "PATTERN+PROCS is a list of regexp/two-argument procedure. For each line
300of FILE, and for each PATTERN that it matches, call the corresponding PROC
301as (PROC LINE MATCHES); PROC must return the line that will be written as a
302substitution of the original line."
303 (let ((rx+proc (map (match-lambda
304 (((? regexp? pattern) . proc)
305 (cons pattern proc))
306 ((pattern . proc)
307 (cons (make-regexp pattern regexp/extended)
308 proc)))
309 pattern+procs)))
310 (with-atomic-file-replacement file
311 (lambda (in out)
312 (let loop ((line (read-line in 'concat)))
313 (if (eof-object? line)
314 #t
315 (let ((line (fold (lambda (r+p line)
316 (match r+p
317 ((regexp . proc)
318 (match (list-matches regexp line)
319 ((and m+ (_ _ ...))
320 (proc line m+))
321 (_ line)))))
322 line
323 rx+proc)))
324 (display line out)
325 (loop (read-line in 'concat)))))))))
326
10c87717
LC
327
328(define-syntax let-matches
329 ;; Helper macro for `substitute*'.
330 (syntax-rules (_)
331 ((let-matches index match (_ vars ...) body ...)
332 (let-matches (+ 1 index) match (vars ...)
333 body ...))
334 ((let-matches index match (var vars ...) body ...)
335 (let ((var (match:substring match index)))
336 (let-matches (+ 1 index) match (vars ...)
337 body ...)))
338 ((let-matches index match () body ...)
339 (begin body ...))))
340
8773648e
LC
341(define-syntax substitute*
342 (syntax-rules ()
343 "Substitute REGEXP in FILE by the string returned by BODY. BODY is
10c87717
LC
344evaluated with each MATCH-VAR bound to the corresponding positional regexp
345sub-expression. For example:
346
4fa697e9 347 (substitute* file
20d83444
LC
348 ((\"hello\")
349 \"good morning\\n\")
350 ((\"foo([a-z]+)bar(.*)$\" all letters end)
351 (string-append \"baz\" letter end)))
4fa697e9
LC
352
353Here, anytime a line of FILE contains \"hello\", it is replaced by \"good
354morning\". Anytime a line of FILE matches the second regexp, ALL is bound to
355the complete match, LETTERS is bound to the first sub-expression, and END is
356bound to the last one.
357
358When one of the MATCH-VAR is `_', no variable is bound to the corresponding
20d83444
LC
359match substring.
360
361Alternatively, FILE may be a list of file names, in which case they are
362all subject to the substitutions."
8773648e 363 ((substitute* file ((regexp match-var ...) body ...) ...)
20d83444
LC
364 (let ()
365 (define (substitute-one-file file-name)
366 (substitute
367 file-name
368 (list (cons regexp
369 (lambda (l m+)
370 ;; Iterate over matches M+ and return the
371 ;; modified line based on L.
372 (let loop ((m* m+) ; matches
373 (o 0) ; offset in L
374 (r '())) ; result
375 (match m*
376 (()
377 (let ((r (cons (substring l o) r)))
378 (string-concatenate-reverse r)))
379 ((m . rest)
380 (let-matches 0 m (match-var ...)
381 (loop rest
382 (match:end m)
383 (cons*
384 (begin body ...)
385 (substring l o (match:start m))
386 r))))))))
387 ...)))
388
389 (match file
390 ((files (... ...))
391 (for-each substitute-one-file files))
392 ((? string? f)
393 (substitute-one-file f)))))))
10c87717 394
ebe2f31f
LC
395\f
396;;;
397;;; Patching shebangs---e.g., /bin/sh -> /nix/store/xyz...-bash/bin/sh.
398;;;
399
a18b4d08
LC
400(define* (dump-port in out
401 #:key (buffer-size 16384)
402 (progress (lambda (t k) (k))))
74baf333 403 "Read as much data as possible from IN and write it to OUT, using
a18b4d08
LC
404chunks of BUFFER-SIZE bytes. Call PROGRESS after each successful
405transfer of BUFFER-SIZE bytes or less, passing it the total number of
406bytes transferred and the continuation of the transfer as a thunk."
ebe2f31f
LC
407 (define buffer
408 (make-bytevector buffer-size))
409
a18b4d08
LC
410 (let loop ((total 0)
411 (bytes (get-bytevector-n! in buffer 0 buffer-size)))
ebe2f31f 412 (or (eof-object? bytes)
a18b4d08 413 (let ((total (+ total bytes)))
ebe2f31f 414 (put-bytevector out buffer 0 bytes)
a18b4d08
LC
415 (progress total
416 (lambda ()
417 (loop total
418 (get-bytevector-n! in buffer 0 buffer-size))))))))
ebe2f31f 419
bc5bf85f
LC
420(define (set-file-time file stat)
421 "Set the atime/mtime of FILE to that specified by STAT."
422 (utime file
423 (stat:atime stat)
424 (stat:mtime stat)
425 (stat:atimensec stat)
426 (stat:mtimensec stat)))
427
ebe2f31f 428(define patch-shebang
525a59d6
LC
429 (let ((shebang-rx (make-regexp "^[[:blank:]]*([[:graph:]]+)(.*)$")))
430 (lambda* (file
bc5bf85f
LC
431 #:optional
432 (path (search-path-as-string->list (getenv "PATH")))
433 #:key (keep-mtime? #t))
525a59d6
LC
434 "Replace the #! interpreter file name in FILE by a valid one found in
435PATH, when FILE actually starts with a shebang. Return #t when FILE was
bc5bf85f
LC
436patched, #f otherwise. When KEEP-MTIME? is true, the atime/mtime of
437FILE are kept unchanged."
ebe2f31f
LC
438 (define (patch p interpreter rest-of-line)
439 (let* ((template (string-append file ".XXXXXX"))
440 (out (mkstemp! template))
bc5bf85f
LC
441 (st (stat file))
442 (mode (stat:mode st)))
ebe2f31f
LC
443 (with-throw-handler #t
444 (lambda ()
445 (format out "#!~a~a~%"
446 interpreter rest-of-line)
447 (dump-port p out)
448 (close out)
449 (chmod template mode)
450 (rename-file template file)
bc5bf85f
LC
451 (when keep-mtime?
452 (set-file-time file st))
ebe2f31f
LC
453 #t)
454 (lambda (key . args)
455 (format (current-error-port)
456 "patch-shebang: ~a: error: ~a ~s~%"
457 file key args)
458 (false-if-exception (delete-file template))
459 #f))))
460
c0895112
LC
461 (call-with-ascii-input-file file
462 (lambda (p)
463 (and (eq? #\# (read-char p))
464 (eq? #\! (read-char p))
465 (let ((line (false-if-exception (read-line p))))
466 (and=> (and line (regexp-exec shebang-rx line))
467 (lambda (m)
468 (let* ((cmd (match:substring m 1))
469 (bin (search-path path (basename cmd))))
470 (if bin
471 (if (string=? bin cmd)
472 #f ; nothing to do
473 (begin
474 (format (current-error-port)
475 "patch-shebang: ~a: changing `~a' to `~a'~%"
476 file cmd bin)
477 (patch p bin (match:substring m 2))))
478 (begin
479 (format (current-error-port)
480 "patch-shebang: ~a: warning: no binary for interpreter `~a' found in $PATH~%"
481 file (basename cmd))
482 #f))))))))))))
483
bc5bf85f
LC
484(define* (patch-makefile-SHELL file #:key (keep-mtime? #t))
485 "Patch the `SHELL' variable in FILE, which is supposedly a makefile.
486When KEEP-MTIME? is true, the atime/mtime of FILE are kept unchanged."
c0895112
LC
487
488 ;; For instance, Gettext-generated po/Makefile.in.in do not honor $SHELL.
489
490 ;; XXX: Unlike with `patch-shebang', FILE is always touched.
491
492 (define (find-shell name)
493 (let ((shell
494 (search-path (search-path-as-string->list (getenv "PATH"))
495 name)))
496 (unless shell
497 (format (current-error-port)
498 "patch-makefile-SHELL: warning: no binary for shell `~a' found in $PATH~%"
499 name))
500 shell))
501
bc5bf85f
LC
502 (let ((st (stat file)))
503 (substitute* file
504 (("^ *SHELL[[:blank:]]*=[[:blank:]]*([[:graph:]]*/)([[:graph:]]+)[[:blank:]]*" _ dir shell)
505 (let* ((old (string-append dir shell))
506 (new (or (find-shell shell) old)))
507 (unless (string=? new old)
508 (format (current-error-port)
509 "patch-makefile-SHELL: ~a: changing `SHELL' from `~a' to `~a'~%"
510 file old new))
511 (string-append "SHELL = " new "\n"))))
512
513 (when keep-mtime?
514 (set-file-time file st))))
10c87717 515
91133c2d
LC
516(define* (fold-port-matches proc init pattern port
517 #:optional (unmatched (lambda (_ r) r)))
518 "Read from PORT character-by-character; for each match against
519PATTERN, call (PROC MATCH RESULT), where RESULT is seeded with INIT.
520PATTERN is a list of SRFI-14 char-sets. Call (UNMATCHED CHAR RESULT)
521for each unmatched character."
522 (define initial-pattern
523 ;; The poor developer's regexp.
524 (if (string? pattern)
525 (map char-set (string->list pattern))
526 pattern))
527
93b03575
LC
528 (define (get-char p)
529 ;; We call it `get-char', but that's really a binary version
530 ;; thereof. (The real `get-char' cannot be used here because our
531 ;; bootstrap Guile is hacked to always use UTF-8.)
532 (match (get-u8 p)
533 ((? integer? x) (integer->char x))
534 (x x)))
535
91133c2d
LC
536 ;; Note: we're not really striving for performance here...
537 (let loop ((chars '())
538 (pattern initial-pattern)
539 (matched '())
540 (result init))
541 (cond ((null? chars)
542 (loop (list (get-char port))
543 pattern
544 matched
545 result))
546 ((null? pattern)
547 (loop chars
548 initial-pattern
549 '()
550 (proc (list->string (reverse matched)) result)))
551 ((eof-object? (car chars))
552 (fold-right unmatched result matched))
553 ((char-set-contains? (car pattern) (car chars))
554 (loop (cdr chars)
555 (cdr pattern)
556 (cons (car chars) matched)
557 result))
558 ((null? matched) ; common case
559 (loop (cdr chars)
560 pattern
561 matched
562 (unmatched (car chars) result)))
563 (else
564 (let ((matched (reverse matched)))
565 (loop (append (cdr matched) chars)
566 initial-pattern
567 '()
568 (unmatched (car matched) result)))))))
569
570(define* (remove-store-references file
571 #:optional (store (or (getenv "NIX_STORE")
572 "/nix/store")))
573 "Remove from FILE occurrences of file names in STORE; return #t when
574store paths were encountered in FILE, #f otherwise. This procedure is
575known as `nuke-refs' in Nixpkgs."
576 (define pattern
577 (let ((nix-base32-chars
578 '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9
579 #\a #\b #\c #\d #\f #\g #\h #\i #\j #\k #\l #\m #\n
580 #\p #\q #\r #\s #\v #\w #\x #\y #\z)))
581 `(,@(map char-set (string->list store))
582 ,(char-set #\/)
583 ,@(make-list 32 (list->char-set nix-base32-chars))
584 ,(char-set #\-))))
585
586 (with-fluids ((%default-port-encoding #f))
587 (with-atomic-file-replacement file
588 (lambda (in out)
589 ;; We cannot use `regexp-exec' here because it cannot deal with
590 ;; strings containing NUL characters.
591 (format #t "removing store references from `~a'...~%" file)
592 (setvbuf in _IOFBF 65536)
593 (setvbuf out _IOFBF 65536)
594 (fold-port-matches (lambda (match result)
93b03575
LC
595 (put-bytevector out (string->utf8 store))
596 (put-u8 out (char->integer #\/))
597 (put-bytevector out
598 (string->utf8
599 "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-"))
91133c2d
LC
600 #t)
601 #f
602 pattern
603 in
604 (lambda (char result)
93b03575 605 (put-u8 out (char->integer char))
91133c2d
LC
606 result))))))
607
b0e0d0e9
LC
608;;; Local Variables:
609;;; eval: (put 'call-with-output-file/atomic 'scheme-indent-function 1)
610;;; eval: (put 'with-throw-handler 'scheme-indent-function 1)
8197c978 611;;; eval: (put 'let-matches 'scheme-indent-function 3)
dcd72906 612;;; eval: (put 'with-atomic-file-replacement 'scheme-indent-function 1)
b0e0d0e9 613;;; End: