build-system/gnu: Strip 'ar' archives as well.
[jackhill/guix/guix.git] / guix / build / utils.scm
CommitLineData
4155e2a9 1;;; GNU Guix --- Functional package management for GNU
8be3b8a3 2;;; Copyright © 2012, 2013, 2014 Ludovic Courtès <ludo@gnu.org>
11996d85 3;;; Copyright © 2013 Andreas Enge <andreas@enge.fr>
30db6af1 4;;; Copyright © 2013 Nikita Karetnikov <nikita@karetnikov.org>
c36db98c 5;;;
4155e2a9 6;;; This file is part of GNU Guix.
c36db98c 7;;;
4155e2a9 8;;; GNU Guix is free software; you can redistribute it and/or modify it
c36db98c
LC
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;;;
4155e2a9 13;;; GNU Guix is distributed in the hope that it will be useful, but
c36db98c
LC
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
4155e2a9 19;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
c36db98c
LC
20
21(define-module (guix build utils)
22 #:use-module (srfi srfi-1)
b0e0d0e9 23 #:use-module (srfi srfi-11)
c0746cc9 24 #:use-module (ice-9 ftw)
b0e0d0e9
LC
25 #:use-module (ice-9 match)
26 #:use-module (ice-9 regex)
27 #:use-module (ice-9 rdelim)
c041886d 28 #:use-module (ice-9 format)
ebe2f31f
LC
29 #:use-module (rnrs bytevectors)
30 #:use-module (rnrs io ports)
d475b259
LC
31 #:re-export (alist-cons
32 alist-delete)
8be3b8a3 33 #:export (%store-directory
e6039b9c
LC
34 parallel-job-count
35
8be3b8a3 36 directory-exists?
d0084152 37 executable-file?
c0895112 38 call-with-ascii-input-file
99533da5 39 elf-file?
91ee959b 40 ar-file?
b0e0d0e9 41 with-directory-excursion
7da95264 42 mkdir-p
c0746cc9 43 copy-recursively
e65df6a6 44 delete-file-recursively
4c261f41
LC
45 find-files
46
b0e0d0e9 47 set-path-environment-variable
ebe2f31f
LC
48 search-path-as-string->list
49 list->search-path-as-string
7584f822
LC
50 which
51
b0e0d0e9
LC
52 alist-cons-before
53 alist-cons-after
54 alist-replace
dcd72906 55 with-atomic-file-replacement
10c87717 56 substitute
ebe2f31f
LC
57 substitute*
58 dump-port
bc5bf85f 59 set-file-time
91133c2d 60 patch-shebang
c0895112 61 patch-makefile-SHELL
91133c2d 62 fold-port-matches
30db6af1
NK
63 remove-store-references
64 wrap-program))
b0e0d0e9 65
11996d85 66
b0e0d0e9
LC
67;;;
68;;; Directories.
69;;;
c36db98c 70
8be3b8a3
LC
71(define (%store-directory)
72 "Return the directory name of the store."
73 (or (getenv "NIX_STORE")
74 "/gnu/store"))
75
e6039b9c
LC
76(define (parallel-job-count)
77 "Return the number of processes to be passed next to GNU Make's `-j'
78argument."
79 (match (getenv "NIX_BUILD_CORES") ;set by the daemon
80 (#f 1)
81 ("0" (current-processor-count))
82 (x (or (string->number x) 1))))
83
c36db98c
LC
84(define (directory-exists? dir)
85 "Return #t if DIR exists and is a directory."
9f55cf8d
LC
86 (let ((s (stat dir #f)))
87 (and s
88 (eq? 'directory (stat:type s)))))
c36db98c 89
d0084152
LC
90(define (executable-file? file)
91 "Return #t if FILE exists and is executable."
92 (let ((s (stat file #f)))
93 (and s
94 (not (zero? (logand (stat:mode s) #o100))))))
95
c0895112
LC
96(define (call-with-ascii-input-file file proc)
97 "Open FILE as an ASCII or binary file, and pass the resulting port to
98PROC. FILE is closed when PROC's dynamic extent is left. Return the
99return values of applying PROC to the port."
100 (let ((port (with-fluids ((%default-port-encoding #f))
101 ;; Use "b" so that `open-file' ignores `coding:' cookies.
102 (open-file file "rb"))))
103 (dynamic-wind
104 (lambda ()
105 #t)
106 (lambda ()
107 (proc port))
108 (lambda ()
109 (close-input-port port)))))
110
2bbc6db5
LC
111(define (file-header-match header)
112 "Return a procedure that returns true when its argument is a file starting
113with the bytes in HEADER, a bytevector."
114 (define len
115 (bytevector-length header))
99533da5 116
2bbc6db5
LC
117 (lambda (file)
118 "Return true if FILE starts with the right magic bytes."
119 (define (get-header)
120 (call-with-input-file file
121 (lambda (port)
122 (get-bytevector-n port len))
123 #:binary #t #:guess-encoding #f))
124
125 (equal? (get-header) header)))
126
127(define %elf-magic-bytes
128 ;; Magic bytes of ELF files. See <elf.h>.
129 (u8-list->bytevector (map char->integer (string->list "\x7FELF"))))
130
131(define elf-file?
132 (file-header-match %elf-magic-bytes))
99533da5 133
91ee959b
LC
134(define %ar-magic-bytes
135 ;; Magic bytes of archives created by 'ar'. See <ar.h>.
136 (u8-list->bytevector (map char->integer (string->list "!<arch>\n"))))
137
2bbc6db5
LC
138(define ar-file?
139 (file-header-match %ar-magic-bytes))
91ee959b 140
b0e0d0e9
LC
141(define-syntax-rule (with-directory-excursion dir body ...)
142 "Run BODY with DIR as the process's current directory."
143 (let ((init (getcwd)))
144 (dynamic-wind
145 (lambda ()
146 (chdir dir))
147 (lambda ()
148 body ...)
149 (lambda ()
150 (chdir init)))))
151
7da95264
LC
152(define (mkdir-p dir)
153 "Create directory DIR and all its ancestors."
154 (define absolute?
155 (string-prefix? "/" dir))
156
157 (define not-slash
158 (char-set-complement (char-set #\/)))
159
160 (let loop ((components (string-tokenize dir not-slash))
161 (root (if absolute?
162 ""
163 ".")))
164 (match components
165 ((head tail ...)
166 (let ((path (string-append root "/" head)))
167 (catch 'system-error
168 (lambda ()
169 (mkdir path)
170 (loop tail path))
171 (lambda args
172 (if (= EEXIST (system-error-errno args))
173 (loop tail path)
174 (apply throw args))))))
175 (() #t))))
176
c0746cc9 177(define* (copy-recursively source destination
12761f48
LC
178 #:key
179 (log (current-output-port))
8d846470
LC
180 (follow-symlinks? #f)
181 keep-mtime?)
12761f48 182 "Copy SOURCE directory to DESTINATION. Follow symlinks if FOLLOW-SYMLINKS?
8d846470
LC
183is true; otherwise, just preserve them. When KEEP-MTIME? is true, keep the
184modification time of the files in SOURCE on those of DESTINATION. Write
185verbose output to the LOG port."
c0746cc9
LC
186 (define strip-source
187 (let ((len (string-length source)))
188 (lambda (file)
189 (substring file len))))
190
191 (file-system-fold (const #t) ; enter?
192 (lambda (file stat result) ; leaf
193 (let ((dest (string-append destination
194 (strip-source file))))
195 (format log "`~a' -> `~a'~%" file dest)
12761f48
LC
196 (case (stat:type stat)
197 ((symlink)
198 (let ((target (readlink file)))
199 (symlink target dest)))
200 (else
8d846470
LC
201 (copy-file file dest)
202 (when keep-mtime?
203 (set-file-time dest stat))))))
c0746cc9 204 (lambda (dir stat result) ; down
8d846470
LC
205 (let ((target (string-append destination
206 (strip-source dir))))
207 (mkdir-p target)
208 (when keep-mtime?
209 (set-file-time target stat))))
c0746cc9
LC
210 (lambda (dir stat result) ; up
211 result)
212 (const #t) ; skip
213 (lambda (file stat errno result)
214 (format (current-error-port) "i/o error: ~a: ~a~%"
215 file (strerror errno))
216 #f)
217 #t
12761f48
LC
218 source
219
220 (if follow-symlinks?
221 stat
222 lstat)))
c0746cc9 223
d84a7be6
LC
224(define* (delete-file-recursively dir
225 #:key follow-mounts?)
226 "Delete DIR recursively, like `rm -rf', without following symlinks. Don't
227follow mount points either, unless FOLLOW-MOUNTS? is true. Report but ignore
228errors."
229 (let ((dev (stat:dev (lstat dir))))
230 (file-system-fold (lambda (dir stat result) ; enter?
231 (or follow-mounts?
232 (= dev (stat:dev stat))))
233 (lambda (file stat result) ; leaf
234 (delete-file file))
235 (const #t) ; down
236 (lambda (dir stat result) ; up
237 (rmdir dir))
238 (const #t) ; skip
239 (lambda (file stat errno result)
240 (format (current-error-port)
241 "warning: failed to delete ~a: ~a~%"
242 file (strerror errno)))
243 #t
244 dir
245
246 ;; Don't follow symlinks.
247 lstat)))
e65df6a6 248
4c261f41 249(define (find-files dir regexp)
66018f1c
LC
250 "Return the lexicographically sorted list of files under DIR whose basename
251matches REGEXP."
4c261f41
LC
252 (define file-rx
253 (if (regexp? regexp)
254 regexp
255 (make-regexp regexp)))
256
66018f1c
LC
257 ;; Sort the result to get deterministic results.
258 (sort (file-system-fold (const #t)
259 (lambda (file stat result) ; leaf
260 (if (regexp-exec file-rx (basename file))
261 (cons file result)
262 result))
263 (lambda (dir stat result) ; down
264 result)
265 (lambda (dir stat result) ; up
266 result)
267 (lambda (file stat result) ; skip
268 result)
269 (lambda (file stat errno result)
270 (format (current-error-port) "find-files: ~a: ~a~%"
271 file (strerror errno))
f1352766 272 result)
66018f1c
LC
273 '()
274 dir)
275 string<?))
4c261f41 276
b0e0d0e9
LC
277\f
278;;;
279;;; Search paths.
280;;;
281
c36db98c
LC
282(define (search-path-as-list sub-directories input-dirs)
283 "Return the list of directories among SUB-DIRECTORIES that exist in
284INPUT-DIRS. Example:
285
286 (search-path-as-list '(\"share/emacs/site-lisp\" \"share/emacs/24.1\")
287 (list \"/package1\" \"/package2\" \"/package3\"))
288 => (\"/package1/share/emacs/site-lisp\"
289 \"/package3/share/emacs/site-lisp\")
290
291"
292 (append-map (lambda (input)
293 (filter-map (lambda (dir)
294 (let ((dir (string-append input "/"
295 dir)))
296 (and (directory-exists? dir)
297 dir)))
298 sub-directories))
299 input-dirs))
300
301(define (list->search-path-as-string lst separator)
302 (string-join lst separator))
303
ebe2f31f
LC
304(define* (search-path-as-string->list path #:optional (separator #\:))
305 (string-tokenize path (char-set-complement (char-set separator))))
306
c36db98c
LC
307(define* (set-path-environment-variable env-var sub-directories input-dirs
308 #:key (separator ":"))
309 "Look for each of SUB-DIRECTORIES in INPUT-DIRS. Set ENV-VAR to a
310SEPARATOR-separated path accordingly. Example:
311
312 (set-path-environment-variable \"PKG_CONFIG\"
313 '(\"lib/pkgconfig\")
314 (list package1 package2))
315"
9d9ef458
LC
316 (let* ((path (search-path-as-list sub-directories input-dirs))
317 (value (list->search-path-as-string path separator)))
b15669f3
LC
318 (if (string-null? value)
319 (begin
320 ;; Never set ENV-VAR to an empty string because often, the empty
321 ;; string is equivalent to ".". This is the case for
322 ;; GUILE_LOAD_PATH in Guile 2.0, for instance.
323 (unsetenv env-var)
324 (format #t "environment variable `~a' unset~%" env-var))
325 (begin
326 (setenv env-var value)
327 (format #t "environment variable `~a' set to `~a'~%"
328 env-var value)))))
b0e0d0e9 329
7584f822
LC
330(define (which program)
331 "Return the complete file name for PROGRAM as found in $PATH, or #f if
332PROGRAM could not be found."
333 (search-path (search-path-as-string->list (getenv "PATH"))
334 program))
335
b0e0d0e9
LC
336\f
337;;;
338;;; Phases.
339;;;
340;;; In (guix build gnu-build-system), there are separate phases (configure,
341;;; build, test, install). They are represented as a list of name/procedure
342;;; pairs. The following procedures make it easy to change the list of
343;;; phases.
344;;;
345
346(define* (alist-cons-before reference key value alist
347 #:optional (key=? equal?))
348 "Insert the KEY/VALUE pair before the first occurrence of a pair whose key
349is REFERENCE in ALIST. Use KEY=? to compare keys."
350 (let-values (((before after)
351 (break (match-lambda
352 ((k . _)
353 (key=? k reference)))
354 alist)))
355 (append before (alist-cons key value after))))
356
357(define* (alist-cons-after reference key value alist
358 #:optional (key=? equal?))
359 "Insert the KEY/VALUE pair after the first occurrence of a pair whose key
360is REFERENCE in ALIST. Use KEY=? to compare keys."
361 (let-values (((before after)
362 (break (match-lambda
363 ((k . _)
364 (key=? k reference)))
365 alist)))
366 (match after
367 ((reference after ...)
368 (append before (cons* reference `(,key . ,value) after)))
369 (()
370 (append before `((,key . ,value)))))))
371
372(define* (alist-replace key value alist #:optional (key=? equal?))
373 "Replace the first pair in ALIST whose car is KEY with the KEY/VALUE pair.
374An error is raised when no such pair exists."
375 (let-values (((before after)
376 (break (match-lambda
377 ((k . _)
378 (key=? k key)))
379 alist)))
380 (match after
381 ((_ after ...)
382 (append before (alist-cons key value after))))))
383
384\f
385;;;
386;;; Text substitution (aka. sed).
387;;;
388
dcd72906
LC
389(define (with-atomic-file-replacement file proc)
390 "Call PROC with two arguments: an input port for FILE, and an output
391port for the file that is going to replace FILE. Upon success, FILE is
392atomically replaced by what has been written to the output port, and
393PROC's result is returned."
394 (let* ((template (string-append file ".XXXXXX"))
d9dbab18
LC
395 (out (mkstemp! template))
396 (mode (stat:mode (stat file))))
b0e0d0e9
LC
397 (with-throw-handler #t
398 (lambda ()
399 (call-with-input-file file
400 (lambda (in)
dcd72906
LC
401 (let ((result (proc in out)))
402 (close out)
403 (chmod template mode)
404 (rename-file template file)
405 result))))
b0e0d0e9
LC
406 (lambda (key . args)
407 (false-if-exception (delete-file template))))))
408
dcd72906
LC
409(define (substitute file pattern+procs)
410 "PATTERN+PROCS is a list of regexp/two-argument procedure. For each line
411of FILE, and for each PATTERN that it matches, call the corresponding PROC
412as (PROC LINE MATCHES); PROC must return the line that will be written as a
413substitution of the original line."
414 (let ((rx+proc (map (match-lambda
415 (((? regexp? pattern) . proc)
416 (cons pattern proc))
417 ((pattern . proc)
418 (cons (make-regexp pattern regexp/extended)
419 proc)))
420 pattern+procs)))
421 (with-atomic-file-replacement file
422 (lambda (in out)
423 (let loop ((line (read-line in 'concat)))
424 (if (eof-object? line)
425 #t
426 (let ((line (fold (lambda (r+p line)
427 (match r+p
428 ((regexp . proc)
429 (match (list-matches regexp line)
430 ((and m+ (_ _ ...))
431 (proc line m+))
432 (_ line)))))
433 line
434 rx+proc)))
435 (display line out)
436 (loop (read-line in 'concat)))))))))
437
10c87717
LC
438
439(define-syntax let-matches
440 ;; Helper macro for `substitute*'.
441 (syntax-rules (_)
442 ((let-matches index match (_ vars ...) body ...)
443 (let-matches (+ 1 index) match (vars ...)
444 body ...))
445 ((let-matches index match (var vars ...) body ...)
446 (let ((var (match:substring match index)))
447 (let-matches (+ 1 index) match (vars ...)
448 body ...)))
449 ((let-matches index match () body ...)
450 (begin body ...))))
451
8773648e
LC
452(define-syntax substitute*
453 (syntax-rules ()
454 "Substitute REGEXP in FILE by the string returned by BODY. BODY is
10c87717
LC
455evaluated with each MATCH-VAR bound to the corresponding positional regexp
456sub-expression. For example:
457
4fa697e9 458 (substitute* file
20d83444
LC
459 ((\"hello\")
460 \"good morning\\n\")
461 ((\"foo([a-z]+)bar(.*)$\" all letters end)
462 (string-append \"baz\" letter end)))
4fa697e9
LC
463
464Here, anytime a line of FILE contains \"hello\", it is replaced by \"good
465morning\". Anytime a line of FILE matches the second regexp, ALL is bound to
466the complete match, LETTERS is bound to the first sub-expression, and END is
467bound to the last one.
468
469When one of the MATCH-VAR is `_', no variable is bound to the corresponding
20d83444
LC
470match substring.
471
472Alternatively, FILE may be a list of file names, in which case they are
473all subject to the substitutions."
8773648e 474 ((substitute* file ((regexp match-var ...) body ...) ...)
20d83444
LC
475 (let ()
476 (define (substitute-one-file file-name)
477 (substitute
478 file-name
479 (list (cons regexp
480 (lambda (l m+)
481 ;; Iterate over matches M+ and return the
482 ;; modified line based on L.
483 (let loop ((m* m+) ; matches
484 (o 0) ; offset in L
485 (r '())) ; result
486 (match m*
487 (()
488 (let ((r (cons (substring l o) r)))
489 (string-concatenate-reverse r)))
490 ((m . rest)
491 (let-matches 0 m (match-var ...)
492 (loop rest
493 (match:end m)
494 (cons*
495 (begin body ...)
496 (substring l o (match:start m))
497 r))))))))
498 ...)))
499
500 (match file
501 ((files (... ...))
502 (for-each substitute-one-file files))
503 ((? string? f)
504 (substitute-one-file f)))))))
10c87717 505
ebe2f31f
LC
506\f
507;;;
8be3b8a3 508;;; Patching shebangs---e.g., /bin/sh -> /gnu/store/xyz...-bash/bin/sh.
ebe2f31f
LC
509;;;
510
a18b4d08
LC
511(define* (dump-port in out
512 #:key (buffer-size 16384)
513 (progress (lambda (t k) (k))))
74baf333 514 "Read as much data as possible from IN and write it to OUT, using
a18b4d08
LC
515chunks of BUFFER-SIZE bytes. Call PROGRESS after each successful
516transfer of BUFFER-SIZE bytes or less, passing it the total number of
517bytes transferred and the continuation of the transfer as a thunk."
ebe2f31f
LC
518 (define buffer
519 (make-bytevector buffer-size))
520
a18b4d08
LC
521 (let loop ((total 0)
522 (bytes (get-bytevector-n! in buffer 0 buffer-size)))
ebe2f31f 523 (or (eof-object? bytes)
a18b4d08 524 (let ((total (+ total bytes)))
ebe2f31f 525 (put-bytevector out buffer 0 bytes)
a18b4d08
LC
526 (progress total
527 (lambda ()
528 (loop total
529 (get-bytevector-n! in buffer 0 buffer-size))))))))
ebe2f31f 530
bc5bf85f
LC
531(define (set-file-time file stat)
532 "Set the atime/mtime of FILE to that specified by STAT."
533 (utime file
534 (stat:atime stat)
535 (stat:mtime stat)
536 (stat:atimensec stat)
537 (stat:mtimensec stat)))
538
ebe2f31f 539(define patch-shebang
11996d85 540 (let ((shebang-rx (make-regexp "^[[:blank:]]*([[:graph:]]+)[[:blank:]]*([[:graph:]]*)(.*)$")))
525a59d6 541 (lambda* (file
bc5bf85f
LC
542 #:optional
543 (path (search-path-as-string->list (getenv "PATH")))
544 #:key (keep-mtime? #t))
525a59d6
LC
545 "Replace the #! interpreter file name in FILE by a valid one found in
546PATH, when FILE actually starts with a shebang. Return #t when FILE was
bc5bf85f
LC
547patched, #f otherwise. When KEEP-MTIME? is true, the atime/mtime of
548FILE are kept unchanged."
ebe2f31f
LC
549 (define (patch p interpreter rest-of-line)
550 (let* ((template (string-append file ".XXXXXX"))
551 (out (mkstemp! template))
bc5bf85f
LC
552 (st (stat file))
553 (mode (stat:mode st)))
ebe2f31f
LC
554 (with-throw-handler #t
555 (lambda ()
556 (format out "#!~a~a~%"
557 interpreter rest-of-line)
558 (dump-port p out)
559 (close out)
560 (chmod template mode)
561 (rename-file template file)
bc5bf85f
LC
562 (when keep-mtime?
563 (set-file-time file st))
ebe2f31f
LC
564 #t)
565 (lambda (key . args)
566 (format (current-error-port)
567 "patch-shebang: ~a: error: ~a ~s~%"
568 file key args)
569 (false-if-exception (delete-file template))
570 #f))))
571
c0895112
LC
572 (call-with-ascii-input-file file
573 (lambda (p)
574 (and (eq? #\# (read-char p))
575 (eq? #\! (read-char p))
576 (let ((line (false-if-exception (read-line p))))
577 (and=> (and line (regexp-exec shebang-rx line))
578 (lambda (m)
11996d85
AE
579 (let* ((interp (match:substring m 1))
580 (arg1 (match:substring m 2))
581 (rest (match:substring m 3))
582 (has-env (string-suffix? "/env" interp))
583 (cmd (if has-env arg1 (basename interp)))
584 (bin (search-path path cmd)))
c0895112 585 (if bin
11996d85 586 (if (string=? bin interp)
c0895112 587 #f ; nothing to do
11996d85
AE
588 (if has-env
589 (begin
590 (format (current-error-port)
591 "patch-shebang: ~a: changing `~a' to `~a'~%"
592 file (string-append interp " " arg1) bin)
593 (patch p bin rest))
594 (begin
595 (format (current-error-port)
596 "patch-shebang: ~a: changing `~a' to `~a'~%"
597 file interp bin)
598 (patch p bin
ca8def6e
AE
599 (if (string-null? arg1)
600 ""
601 (string-append " " arg1 rest))))))
c0895112
LC
602 (begin
603 (format (current-error-port)
604 "patch-shebang: ~a: warning: no binary for interpreter `~a' found in $PATH~%"
605 file (basename cmd))
606 #f))))))))))))
607
bc5bf85f
LC
608(define* (patch-makefile-SHELL file #:key (keep-mtime? #t))
609 "Patch the `SHELL' variable in FILE, which is supposedly a makefile.
610When KEEP-MTIME? is true, the atime/mtime of FILE are kept unchanged."
c0895112
LC
611
612 ;; For instance, Gettext-generated po/Makefile.in.in do not honor $SHELL.
613
614 ;; XXX: Unlike with `patch-shebang', FILE is always touched.
615
616 (define (find-shell name)
617 (let ((shell
618 (search-path (search-path-as-string->list (getenv "PATH"))
619 name)))
620 (unless shell
621 (format (current-error-port)
622 "patch-makefile-SHELL: warning: no binary for shell `~a' found in $PATH~%"
623 name))
624 shell))
625
bc5bf85f
LC
626 (let ((st (stat file)))
627 (substitute* file
11459384 628 (("^ *SHELL[[:blank:]]*=[[:blank:]]*([[:graph:]]*/)([[:graph:]]+)(.*)$"
83291101 629 _ dir shell args)
bc5bf85f
LC
630 (let* ((old (string-append dir shell))
631 (new (or (find-shell shell) old)))
632 (unless (string=? new old)
633 (format (current-error-port)
634 "patch-makefile-SHELL: ~a: changing `SHELL' from `~a' to `~a'~%"
635 file old new))
11459384 636 (string-append "SHELL = " new args))))
bc5bf85f
LC
637
638 (when keep-mtime?
639 (set-file-time file st))))
10c87717 640
91133c2d
LC
641(define* (fold-port-matches proc init pattern port
642 #:optional (unmatched (lambda (_ r) r)))
643 "Read from PORT character-by-character; for each match against
644PATTERN, call (PROC MATCH RESULT), where RESULT is seeded with INIT.
645PATTERN is a list of SRFI-14 char-sets. Call (UNMATCHED CHAR RESULT)
646for each unmatched character."
647 (define initial-pattern
648 ;; The poor developer's regexp.
649 (if (string? pattern)
650 (map char-set (string->list pattern))
651 pattern))
652
93b03575
LC
653 (define (get-char p)
654 ;; We call it `get-char', but that's really a binary version
655 ;; thereof. (The real `get-char' cannot be used here because our
656 ;; bootstrap Guile is hacked to always use UTF-8.)
657 (match (get-u8 p)
658 ((? integer? x) (integer->char x))
659 (x x)))
660
91133c2d
LC
661 ;; Note: we're not really striving for performance here...
662 (let loop ((chars '())
663 (pattern initial-pattern)
664 (matched '())
665 (result init))
666 (cond ((null? chars)
667 (loop (list (get-char port))
668 pattern
669 matched
670 result))
671 ((null? pattern)
672 (loop chars
673 initial-pattern
674 '()
675 (proc (list->string (reverse matched)) result)))
676 ((eof-object? (car chars))
677 (fold-right unmatched result matched))
678 ((char-set-contains? (car pattern) (car chars))
679 (loop (cdr chars)
680 (cdr pattern)
681 (cons (car chars) matched)
682 result))
683 ((null? matched) ; common case
684 (loop (cdr chars)
685 pattern
686 matched
687 (unmatched (car chars) result)))
688 (else
689 (let ((matched (reverse matched)))
690 (loop (append (cdr matched) chars)
691 initial-pattern
692 '()
693 (unmatched (car matched) result)))))))
694
695(define* (remove-store-references file
8be3b8a3 696 #:optional (store (%store-directory)))
91133c2d
LC
697 "Remove from FILE occurrences of file names in STORE; return #t when
698store paths were encountered in FILE, #f otherwise. This procedure is
699known as `nuke-refs' in Nixpkgs."
700 (define pattern
701 (let ((nix-base32-chars
702 '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9
703 #\a #\b #\c #\d #\f #\g #\h #\i #\j #\k #\l #\m #\n
704 #\p #\q #\r #\s #\v #\w #\x #\y #\z)))
705 `(,@(map char-set (string->list store))
706 ,(char-set #\/)
707 ,@(make-list 32 (list->char-set nix-base32-chars))
708 ,(char-set #\-))))
709
710 (with-fluids ((%default-port-encoding #f))
711 (with-atomic-file-replacement file
712 (lambda (in out)
713 ;; We cannot use `regexp-exec' here because it cannot deal with
714 ;; strings containing NUL characters.
715 (format #t "removing store references from `~a'...~%" file)
716 (setvbuf in _IOFBF 65536)
717 (setvbuf out _IOFBF 65536)
718 (fold-port-matches (lambda (match result)
93b03575
LC
719 (put-bytevector out (string->utf8 store))
720 (put-u8 out (char->integer #\/))
721 (put-bytevector out
722 (string->utf8
723 "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-"))
91133c2d
LC
724 #t)
725 #f
726 pattern
727 in
728 (lambda (char result)
93b03575 729 (put-u8 out (char->integer char))
91133c2d
LC
730 result))))))
731
30db6af1 732(define* (wrap-program prog #:rest vars)
de611138 733 "Make a wrapper for PROG. VARS should look like this:
30db6af1
NK
734
735 '(VARIABLE DELIMITER POSITION LIST-OF-DIRECTORIES)
736
737where DELIMITER is optional. ':' will be used if DELIMITER is not given.
738
739For example, this command:
740
741 (wrap-program \"foo\"
de611138
EB
742 '(\"PATH\" \":\" = (\"/gnu/.../bar/bin\"))
743 '(\"CERT_PATH\" suffix (\"/gnu/.../baz/certs\"
30db6af1
NK
744 \"/qux/certs\")))
745
746will copy 'foo' to '.foo-real' and create the file 'foo' with the following
747contents:
748
749 #!location/of/bin/bash
de611138
EB
750 export PATH=\"/gnu/.../bar/bin\"
751 export CERT_PATH=\"$CERT_PATH${CERT_PATH:+:}/gnu/.../baz/certs:/qux/certs\"
30db6af1
NK
752 exec location/of/.foo-real
753
754This is useful for scripts that expect particular programs to be in $PATH, for
755programs that expect particular shared libraries to be in $LD_LIBRARY_PATH, or
de611138
EB
756modules in $GUILE_LOAD_PATH, etc.
757
758If PROG has previously been wrapped by wrap-program the wrapper will point to
759the previous wrapper."
760 (define (wrapper-file-name number)
761 (format #f "~a/.~a-wrap-~2'0d" (dirname prog) (basename prog) number))
762 (define (next-wrapper-number)
763 (let ((wrappers
764 (find-files (dirname prog)
765 (string-append "\\." (basename prog) "-wrap-.*"))))
766 (if (null? wrappers)
767 0
768 (string->number (string-take-right (last wrappers) 2)))))
769 (define (wrapper-target number)
770 (if (zero? number)
771 (let ((prog-real (string-append (dirname prog) "/."
772 (basename prog) "-real")))
773 (copy-file prog prog-real)
774 prog-real)
775 (wrapper-file-name number)))
776 (let* ((number (next-wrapper-number))
777 (target (wrapper-target number))
778 (wrapper (wrapper-file-name (1+ number)))
779 (prog-tmp (string-append target "-tmp")))
30db6af1
NK
780 (define (export-variable lst)
781 ;; Return a string that exports an environment variable.
782 (match lst
783 ((var sep '= rest)
784 (format #f "export ~a=\"~a\""
785 var (string-join rest sep)))
786 ((var sep 'prefix rest)
787 (format #f "export ~a=\"~a${~a~a+~a}$~a\""
788 var (string-join rest sep) var sep sep var))
789 ((var sep 'suffix rest)
790 (format #f "export ~a=\"$~a${~a~a+~a}~a\""
791 var var var sep sep (string-join rest sep)))
792 ((var '= rest)
793 (format #f "export ~a=\"~a\""
794 var (string-join rest ":")))
795 ((var 'prefix rest)
796 (format #f "export ~a=\"~a${~a:+:}$~a\""
797 var (string-join rest ":") var var))
798 ((var 'suffix rest)
799 (format #f "export ~a=\"$~a${~a:+:}~a\""
800 var var var (string-join rest ":")))))
801
30db6af1
NK
802 (with-output-to-file prog-tmp
803 (lambda ()
804 (format #t
01155b18 805 "#!~a~%~a~%exec \"~a\" \"$@\"~%"
30db6af1
NK
806 (which "bash")
807 (string-join (map export-variable vars)
808 "\n")
de611138 809 (canonicalize-path target))))
30db6af1
NK
810
811 (chmod prog-tmp #o755)
de611138
EB
812 (rename-file prog-tmp wrapper)
813 (symlink wrapper prog-tmp)
30db6af1
NK
814 (rename-file prog-tmp prog)))
815
b0e0d0e9
LC
816;;; Local Variables:
817;;; eval: (put 'call-with-output-file/atomic 'scheme-indent-function 1)
99533da5 818;;; eval: (put 'call-with-ascii-input-file 'scheme-indent-function 1)
b0e0d0e9 819;;; eval: (put 'with-throw-handler 'scheme-indent-function 1)
8197c978 820;;; eval: (put 'let-matches 'scheme-indent-function 3)
dcd72906 821;;; eval: (put 'with-atomic-file-replacement 'scheme-indent-function 1)
b0e0d0e9 822;;; End: