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