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