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