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