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