utils: Move '&fix-hint' to (guix diagnostics).
[jackhill/guix/guix.git] / guix / utils.scm
CommitLineData
233e7676 1;;; GNU Guix --- Functional package management for GNU
6a7c4636 2;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
3f00ff8b 3;;; Copyright © 2013, 2014, 2015 Mark H Weaver <mhw@netris.org>
516e3b6f 4;;; Copyright © 2014 Eric Bavier <bavier@member.fsf.org>
29a7c98a 5;;; Copyright © 2014 Ian Denhardt <ian@zenhack.net>
1b846da8 6;;; Copyright © 2016 Mathieu Lirzin <mthl@gnu.org>
bbd00d20 7;;; Copyright © 2015 David Thompson <davet@gnu.org>
e45b573c 8;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
fb9a57a8 9;;; Copyright © 2018, 2020 Marius Bakke <marius@gnu.org>
e3deeebb 10;;;
233e7676 11;;; This file is part of GNU Guix.
e3deeebb 12;;;
233e7676 13;;; GNU Guix is free software; you can redistribute it and/or modify it
e3deeebb
LC
14;;; under the terms of the GNU General Public License as published by
15;;; the Free Software Foundation; either version 3 of the License, or (at
16;;; your option) any later version.
17;;;
233e7676 18;;; GNU Guix is distributed in the hope that it will be useful, but
e3deeebb
LC
19;;; WITHOUT ANY WARRANTY; without even the implied warranty of
20;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21;;; GNU General Public License for more details.
22;;;
23;;; You should have received a copy of the GNU General Public License
233e7676 24;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
e3deeebb
LC
25
26(define-module (guix utils)
00e219d1 27 #:use-module (guix config)
38b3122a 28 #:use-module (srfi srfi-1)
72d86963 29 #:use-module (srfi srfi-9)
01ac19dc 30 #:use-module (srfi srfi-11)
38b3122a 31 #:use-module (srfi srfi-26)
de4c3f26 32 #:use-module (srfi srfi-39)
27f7cbc9 33 #:use-module (ice-9 ftw)
6a7c4636 34 #:use-module (rnrs io ports) ;need 'port-position' etc.
c8be6f0d 35 #:use-module ((rnrs bytevectors) #:select (bytevector-u8-set!))
f9704f17 36 #:use-module (guix memoization)
27f7cbc9 37 #:use-module ((guix build utils) #:select (dump-port mkdir-p delete-file-recursively))
1752a17a 38 #:use-module ((guix build syscalls) #:select (mkdtemp! fdatasync))
a5e2fc73 39 #:use-module (guix diagnostics) ;<location>, &error-location, etc.
38b3122a 40 #:use-module (ice-9 format)
98090557 41 #:use-module (ice-9 regex)
72d86963 42 #:use-module (ice-9 match)
8ef3401f 43 #:use-module (ice-9 format)
31044751 44 #:use-module ((ice-9 iconv) #:prefix iconv:)
2cd5c038 45 #:use-module (system foreign)
6a79eed9 46 #:re-export (<location> ;for backwards compatibility
a5e2fc73
LC
47 location
48 location?
49 location-file
50 location-line
51 location-column
52 source-properties->location
53 location->source-properties
54
55 &error-location
56 error-location?
f9a8dd05
LC
57 error-location
58
59 &fix-hint
60 fix-hint?
61 condition-fix-hint)
4c0c4db0 62 #:export (strip-keyword-arguments
0af2c24e
LC
63 default-keyword-arguments
64 substitute-keyword-arguments
6071122b 65 ensure-keyword-arguments
ff352cfb 66
07c8a98c
LC
67 current-source-directory
68
8eb80484 69 nix-system->gnu-triplet
98090557 70 gnu-triplet->nix-system
9b48fb88 71 %current-system
9c1edabd 72 %current-target-system
1b846da8 73 package-name->name+version
cba36e64 74 target-mingw?
e45b573c 75 target-arm32?
74f17180
MO
76 target-aarch64?
77 target-arm?
259341cf 78 target-64bit?
fb9a57a8
MB
79 cc-for-target
80
0d1e6ce4
MW
81 version-compare
82 version>?
cde1e967 83 version>=?
29a7c98a
ID
84 version-prefix
85 version-major+minor
47dc9a0d 86 version-major
7db3ff4a 87 guile-version>?
437f62f0 88 version-prefix?
56b943de 89 string-replace-substring
0fdd3bea 90 file-extension
ac10e0e1 91 file-sans-extension
da1027a7 92 tarball-sans-extension
089b1678 93 compressed-file?
3bb168b0 94 switch-symlinks
861693f3 95 call-with-temporary-output-file
db6e5e2b 96 call-with-temporary-directory
04d4c8a4 97 with-atomic-file-output
f0e492f0 98
d67a8819
LC
99 with-environment-variables
100 arguments-from-environment-variable
101
f0e492f0 102 config-directory
739ab68b 103 cache-directory
f0e492f0 104
d50cb56d 105 readlink*
50a3d594 106 edit-expression
7a8024a3
LC
107
108 filtered-port
109 compressed-port
80dea563 110 decompressed-port
01ac19dc
LC
111 call-with-decompressed-port
112 compressed-output-port
c8be6f0d 113 call-with-compressed-output-port
8c348825 114 canonical-newline-port))
e3deeebb 115
38b3122a 116\f
d67a8819
LC
117;;;
118;;; Environment variables.
119;;;
120
121(define (call-with-environment-variables variables thunk)
122 "Call THUNK with the environment VARIABLES set."
123 (let ((environment (environ)))
124 (dynamic-wind
125 (lambda ()
126 (for-each (match-lambda
127 ((variable value)
128 (setenv variable value)))
129 variables))
130 thunk
131 (lambda ()
132 (environ environment)))))
133
134(define-syntax-rule (with-environment-variables variables exp ...)
135 "Evaluate EXP with the given environment VARIABLES set."
136 (call-with-environment-variables variables
137 (lambda () exp ...)))
138
139(define (arguments-from-environment-variable variable)
140 "Retrieve value of environment variable denoted by string VARIABLE in the
141form of a list of strings (`char-set:graphic' tokens) suitable for consumption
142by `args-fold', if VARIABLE is defined, otherwise return an empty list."
143 (let ((env (getenv variable)))
144 (if env
145 (string-tokenize env char-set:graphic)
146 '())))
147
148\f
e0fbbc88
LC
149;;;
150;;; Filtering & pipes.
151;;;
152
153(define (filtered-port command input)
154 "Return an input port where data drained from INPUT is filtered through
155COMMAND (a list). In addition, return a list of PIDs that the caller must
101d9f3f
LC
156wait. When INPUT is a file port, it must be unbuffered; otherwise, any
157buffered data is lost."
e0fbbc88 158 (let loop ((input input)
443eb4e9 159 (pids '()))
e0fbbc88
LC
160 (if (file-port? input)
161 (match (pipe)
162 ((in . out)
163 (match (primitive-fork)
164 (0
443eb4e9
LC
165 (dynamic-wind
166 (const #f)
167 (lambda ()
168 (close-port in)
169 (close-port (current-input-port))
170 (dup2 (fileno input) 0)
171 (close-port (current-output-port))
172 (dup2 (fileno out) 1)
173 (catch 'system-error
174 (lambda ()
175 (apply execl (car command) command))
176 (lambda args
177 (format (current-error-port)
178 "filtered-port: failed to execute '~{~a ~}': ~a~%"
179 command (strerror (system-error-errno args))))))
180 (lambda ()
181 (primitive-_exit 1))))
e0fbbc88
LC
182 (child
183 (close-port out)
184 (values in (cons child pids))))))
185
186 ;; INPUT is not a file port, so fork just for the sake of tunneling it
187 ;; through a file port.
188 (match (pipe)
189 ((in . out)
190 (match (primitive-fork)
191 (0
192 (dynamic-wind
193 (const #t)
194 (lambda ()
195 (close-port in)
196 (dump-port input out))
197 (lambda ()
5efa0e4d 198 (close-port input)
e0fbbc88 199 (false-if-exception (close out))
443eb4e9 200 (primitive-_exit 0))))
e0fbbc88 201 (child
5efa0e4d 202 (close-port input)
e0fbbc88
LC
203 (close-port out)
204 (loop in (cons child pids)))))))))
205
4e48923e
LC
206(define (lzip-port proc port . args)
207 "Return the lzip port produced by calling PROC (a symbol) on PORT and ARGS.
208Raise an error if lzlib support is missing."
209 (let* ((lzlib (false-if-exception (resolve-interface '(guix lzlib))))
210 (supported? (and lzlib
211 ((module-ref lzlib 'lzlib-available?)))))
212 (if supported?
213 (let ((make-port (module-ref lzlib proc)))
214 (values (make-port port) '()))
215 (error "lzip compression not supported" lzlib))))
216
7a8024a3
LC
217(define (decompressed-port compression input)
218 "Return an input port where INPUT is decompressed according to COMPRESSION,
219a symbol such as 'xz."
220 (match compression
221 ((or #f 'none) (values input '()))
222 ('bzip2 (filtered-port `(,%bzip2 "-dc") input))
e9be2c54 223 ('xz (filtered-port `(,%xz "-dc") input))
7a8024a3 224 ('gzip (filtered-port `(,%gzip "-dc") input))
4e48923e
LC
225 ('lzip (values (lzip-port 'make-lzip-input-port input)
226 '()))
227 (_ (error "unsupported compression scheme" compression))))
7a8024a3
LC
228
229(define (compressed-port compression input)
4e48923e 230 "Return an input port where INPUT is compressed according to COMPRESSION,
7a8024a3
LC
231a symbol such as 'xz."
232 (match compression
233 ((or #f 'none) (values input '()))
234 ('bzip2 (filtered-port `(,%bzip2 "-c") input))
e9be2c54 235 ('xz (filtered-port `(,%xz "-c") input))
7a8024a3 236 ('gzip (filtered-port `(,%gzip "-c") input))
4e48923e
LC
237 ('lzip (values (lzip-port 'make-lzip-input-port/compressed input)
238 '()))
239 (_ (error "unsupported compression scheme" compression))))
7a8024a3 240
01ac19dc
LC
241(define (call-with-decompressed-port compression port proc)
242 "Call PROC with a wrapper around PORT, a file port, that decompresses data
30ce8012 243read from PORT according to COMPRESSION, a symbol such as 'xz."
01ac19dc
LC
244 (let-values (((decompressed pids)
245 (decompressed-port compression port)))
246 (dynamic-wind
247 (const #f)
248 (lambda ()
01ac19dc
LC
249 (proc decompressed))
250 (lambda ()
251 (close-port decompressed)
252 (unless (every (compose zero? cdr waitpid) pids)
253 (error "decompressed-port failure" pids))))))
254
80dea563
LC
255(define (filtered-output-port command output)
256 "Return an output port. Data written to that port is filtered through
257COMMAND and written to OUTPUT, an output file port. In addition, return a
258list of PIDs to wait for. OUTPUT must be unbuffered; otherwise, any buffered
259data is lost."
260 (match (pipe)
261 ((in . out)
262 (match (primitive-fork)
263 (0
264 (dynamic-wind
265 (const #f)
266 (lambda ()
267 (close-port out)
268 (close-port (current-input-port))
269 (dup2 (fileno in) 0)
270 (close-port (current-output-port))
271 (dup2 (fileno output) 1)
272 (catch 'system-error
273 (lambda ()
274 (apply execl (car command) command))
275 (lambda args
276 (format (current-error-port)
277 "filtered-output-port: failed to execute '~{~a ~}': ~a~%"
278 command (strerror (system-error-errno args))))))
279 (lambda ()
280 (primitive-_exit 1))))
281 (child
282 (close-port in)
283 (values out (list child)))))))
284
62d2b571
LC
285(define* (compressed-output-port compression output
286 #:key (options '()))
80dea563
LC
287 "Return an output port whose input is compressed according to COMPRESSION,
288a symbol such as 'xz, and then written to OUTPUT. In addition return a list
62d2b571
LC
289of PIDs to wait for. OPTIONS is a list of strings passed to the compression
290program--e.g., '(\"--fast\")."
80dea563
LC
291 (match compression
292 ((or #f 'none) (values output '()))
62d2b571 293 ('bzip2 (filtered-output-port `(,%bzip2 "-c" ,@options) output))
e9be2c54 294 ('xz (filtered-output-port `(,%xz "-c" ,@options) output))
62d2b571 295 ('gzip (filtered-output-port `(,%gzip "-c" ,@options) output))
4e48923e
LC
296 ('lzip (values (lzip-port 'make-lzip-output-port output)
297 '()))
298 (_ (error "unsupported compression scheme" compression))))
80dea563 299
62d2b571
LC
300(define* (call-with-compressed-output-port compression port proc
301 #:key (options '()))
01ac19dc 302 "Call PROC with a wrapper around PORT, a file port, that compresses data
62d2b571
LC
303that goes to PORT according to COMPRESSION, a symbol such as 'xz. OPTIONS is
304a list of command-line arguments passed to the compression program."
01ac19dc 305 (let-values (((compressed pids)
62d2b571
LC
306 (compressed-output-port compression port
307 #:options options)))
01ac19dc
LC
308 (dynamic-wind
309 (const #f)
310 (lambda ()
01ac19dc
LC
311 (proc compressed))
312 (lambda ()
313 (close-port compressed)
314 (unless (every (compose zero? cdr waitpid) pids)
315 (error "compressed-output-port failure" pids))))))
316
50a3d594
SB
317(define* (edit-expression source-properties proc #:key (encoding "UTF-8"))
318 "Edit the expression specified by SOURCE-PROPERTIES using PROC, which should
319be a procedure that takes the original expression in string and returns a new
320one. ENCODING will be used to interpret all port I/O, it default to UTF-8.
321This procedure returns #t on success."
322 (with-fluids ((%default-port-encoding encoding))
323 (let* ((file (assq-ref source-properties 'filename))
324 (line (assq-ref source-properties 'line))
325 (column (assq-ref source-properties 'column))
326 (in (open-input-file file))
327 ;; The start byte position of the expression.
328 (start (begin (while (not (and (= line (port-line in))
329 (= column (port-column in))))
330 (when (eof-object? (read-char in))
331 (error (format #f "~a: end of file~%" in))))
332 (ftell in)))
333 ;; The end byte position of the expression.
334 (end (begin (read in) (ftell in))))
335 (seek in 0 SEEK_SET) ; read from the beginning of the file.
336 (let* ((pre-bv (get-bytevector-n in start))
337 ;; The expression in string form.
31044751 338 (str (iconv:bytevector->string
50a3d594
SB
339 (get-bytevector-n in (- end start))
340 (port-encoding in)))
341 (post-bv (get-bytevector-all in))
342 (str* (proc str)))
343 ;; Verify the edited expression is still a scheme expression.
344 (call-with-input-string str* read)
345 ;; Update the file with edited expression.
346 (with-atomic-file-output file
347 (lambda (out)
348 (put-bytevector out pre-bv)
349 (display str* out)
350 ;; post-bv maybe the end-of-file object.
351 (when (not (eof-object? post-bv))
352 (put-bytevector out post-bv))
353 #t))))))
354
e0fbbc88 355\f
de4c3f26 356;;;
958dd3ce 357;;; Keyword arguments.
de4c3f26
LC
358;;;
359
5e110382
LC
360(define (strip-keyword-arguments keywords args)
361 "Remove all of the keyword arguments listed in KEYWORDS from ARGS."
362 (let loop ((args args)
363 (result '()))
364 (match args
365 (()
366 (reverse result))
367 (((? keyword? kw) arg . rest)
368 (loop rest
369 (if (memq kw keywords)
370 result
371 (cons* arg kw result))))
372 ((head . tail)
373 (loop tail (cons head result))))))
374
0af2c24e
LC
375(define (default-keyword-arguments args defaults)
376 "Return ARGS augmented with any keyword/value from DEFAULTS for
377keywords not already present in ARGS."
378 (let loop ((defaults defaults)
379 (args args))
380 (match defaults
381 ((kw value rest ...)
382 (loop rest
347df601 383 (if (memq kw args)
0af2c24e
LC
384 args
385 (cons* kw value args))))
386 (()
387 args))))
388
b8b129eb
EB
389(define-syntax collect-default-args
390 (syntax-rules ()
391 ((_)
392 '())
393 ((_ (_ _) rest ...)
394 (collect-default-args rest ...))
395 ((_ (kw _ dflt) rest ...)
396 (cons* kw dflt (collect-default-args rest ...)))))
397
0af2c24e
LC
398(define-syntax substitute-keyword-arguments
399 (syntax-rules ()
400 "Return a new list of arguments where the value for keyword arg KW is
b8b129eb
EB
401replaced by EXP. EXP is evaluated in a context where VAR is bound to the
402previous value of the keyword argument, or DFLT if given."
403 ((_ original-args ((kw var dflt ...) exp) ...)
404 (let loop ((args (default-keyword-arguments
405 original-args
406 (collect-default-args (kw var dflt ...) ...)))
0af2c24e
LC
407 (before '()))
408 (match args
409 ((kw var rest (... ...))
410 (loop rest (cons* exp kw before)))
411 ...
412 ((x rest (... ...))
413 (loop rest (cons x before)))
414 (()
415 (reverse before)))))))
416
6071122b
LC
417(define (delkw kw lst)
418 "Remove KW and its associated value from LST, a keyword/value list such
419as '(#:foo 1 #:bar 2)."
420 (let loop ((lst lst)
421 (result '()))
422 (match lst
423 (()
424 (reverse result))
425 ((kw? value rest ...)
426 (if (eq? kw? kw)
427 (append (reverse result) rest)
428 (loop rest (cons* value kw? result)))))))
429
430(define (ensure-keyword-arguments args kw/values)
431 "Force the keywords arguments KW/VALUES in the keyword argument list ARGS.
432For instance:
433
434 (ensure-keyword-arguments '(#:foo 2) '(#:foo 2))
435 => (#:foo 2)
436
437 (ensure-keyword-arguments '(#:foo 2) '(#:bar 3))
438 => (#:foo 2 #:bar 3)
439
440 (ensure-keyword-arguments '(#:foo 2) '(#:bar 3 #:foo 42))
441 => (#:foo 42 #:bar 3)
442"
443 (let loop ((args args)
444 (kw/values kw/values)
445 (result '()))
446 (match args
447 (()
448 (append (reverse result) kw/values))
449 ((kw value rest ...)
450 (match (memq kw kw/values)
451 ((_ value . _)
452 (loop rest (delkw kw kw/values) (cons* value kw result)))
453 (#f
454 (loop rest kw/values (cons* value kw result))))))))
455
958dd3ce
LC
456\f
457;;;
458;;; System strings.
459;;;
460
8eb80484
MW
461(define* (nix-system->gnu-triplet
462 #:optional (system (%current-system)) (vendor "unknown"))
463 "Return a guess of the GNU triplet corresponding to Nix system
464identifier SYSTEM."
3f00ff8b
MW
465 (match system
466 ("armhf-linux"
467 (string-append "arm-" vendor "-linux-gnueabihf"))
468 (_
469 (let* ((dash (string-index system #\-))
470 (arch (substring system 0 dash))
471 (os (substring system (+ 1 dash))))
472 (string-append arch
473 "-" vendor "-"
474 (if (string=? os "linux")
475 "linux-gnu"
476 os))))))
8eb80484 477
98090557
LC
478(define (gnu-triplet->nix-system triplet)
479 "Return the Nix system type corresponding to TRIPLET, a GNU triplet as
480returned by `config.guess'."
481 (let ((triplet (cond ((string-match "^i[345]86-(.*)$" triplet)
482 =>
483 (lambda (m)
484 (string-append "i686-" (match:substring m 1))))
485 (else triplet))))
3f00ff8b
MW
486 (cond ((string-match "^arm[^-]*-([^-]+-)?linux-gnueabihf" triplet)
487 "armhf-linux")
488 ((string-match "^([^-]+)-([^-]+-)?linux-gnu.*" triplet)
98090557
LC
489 =>
490 (lambda (m)
491 ;; Nix omits `-gnu' for GNU/Linux.
492 (string-append (match:substring m 1) "-linux")))
493 ((string-match "^([^-]+)-([^-]+-)?([[:alpha:]]+)([0-9]+\\.?)*$" triplet)
494 =>
495 (lambda (m)
496 ;; Nix strip the version number from names such as `gnu0.3',
497 ;; `darwin10.2.0', etc., and always strips the vendor part.
498 (string-append (match:substring m 1) "-"
499 (match:substring m 3))))
500 (else triplet))))
501
502(define %current-system
503 ;; System type as expected by Nix, usually ARCHITECTURE-KERNEL.
d8eea3d2
LC
504 ;; By default, this is equal to (gnu-triplet->nix-system %host-type).
505 (make-parameter %system))
ff352cfb 506
9c1edabd
LC
507(define %current-target-system
508 ;; Either #f or a GNU triplet representing the target system we are
509 ;; cross-building to.
510 (make-parameter #f))
511
aa042770
LC
512(define* (package-name->name+version spec
513 #:optional (delimiter #\@))
1b846da8
ML
514 "Given SPEC, a package name like \"foo@0.9.1b\", return two values: \"foo\"
515and \"0.9.1b\". When the version part is unavailable, SPEC and #f are
aa042770
LC
516returned. Both parts must not contain any '@'. Optionally, DELIMITER can be
517a character other than '@'."
518 (match (string-rindex spec delimiter)
1b846da8
ML
519 (#f (values spec #f))
520 (idx (values (substring spec 0 idx)
521 (substring spec (1+ idx))))))
522
cba36e64
JN
523(define* (target-mingw? #:optional (target (%current-target-system)))
524 (and target
525 (string-suffix? "-mingw32" target)))
526
300a54bb
LC
527(define* (target-arm32? #:optional (target (or (%current-target-system)
528 (%current-system))))
529 (string-prefix? "arm" target))
e45b573c 530
300a54bb
LC
531(define* (target-aarch64? #:optional (target (or (%current-target-system)
532 (%current-system))))
533 (string-prefix? "aarch64" target))
74f17180 534
300a54bb
LC
535(define* (target-arm? #:optional (target (or (%current-target-system)
536 (%current-system))))
537 (or (target-arm32? target) (target-aarch64? target)))
74f17180 538
300a54bb
LC
539(define* (target-64bit? #:optional (system (or (%current-target-system)
540 (%current-system))))
541 (any (cut string-prefix? <> system) '("x86_64" "aarch64" "mips64" "ppc64")))
259341cf 542
fb9a57a8
MB
543(define* (cc-for-target #:optional (target (%current-target-system)))
544 (if target
545 (string-append target "-gcc")
546 "gcc"))
547
0d1e6ce4
MW
548(define version-compare
549 (let ((strverscmp
550 (let ((sym (or (dynamic-func "strverscmp" (dynamic-link))
551 (error "could not find `strverscmp' (from GNU libc)"))))
552 (pointer->procedure int sym (list '* '*)))))
553 (lambda (a b)
554 "Return '> when A denotes a newer version than B,
555'< when A denotes a older version than B,
556or '= when they denote equal versions."
557 (let ((result (strverscmp (string->pointer a) (string->pointer b))))
558 (cond ((positive? result) '>)
559 ((negative? result) '<)
560 (else '=))))))
561
29a7c98a
ID
562(define (version-prefix version-string num-parts)
563 "Truncate version-string to the first num-parts components of the version.
564For example, (version-prefix \"2.1.47.4.23\" 3) returns \"2.1.47\""
565 (string-join (take (string-split version-string #\.) num-parts) "."))
566
567
568(define (version-major+minor version-string)
569 "Return \"<major>.<minor>\", where major and minor are the major and
570minor version numbers from version-string."
571 (version-prefix version-string 2))
572
47dc9a0d 573(define (version-major version-string)
574 "Return the major version number as string from the version-string."
575 (version-prefix version-string 1))
576
0d1e6ce4 577(define (version>? a b)
cde1e967 578 "Return #t when A denotes a version strictly newer than B."
0d1e6ce4
MW
579 (eq? '> (version-compare a b)))
580
cde1e967
LC
581(define (version>=? a b)
582 "Return #t when A denotes a version newer or equal to B."
583 (case (version-compare a b)
584 ((> =) #t)
585 (else #f)))
586
7db3ff4a
LC
587(define (guile-version>? str)
588 "Return #t if the running Guile version is greater than STR."
589 ;; Note: Using (version>? (version) "2.0.5") or similar doesn't work,
590 ;; because the result of (version) can have a prefix, like "2.0.5-deb1".
591 (version>? (string-append (major-version) "."
592 (minor-version) "."
593 (micro-version))
594 str))
595
437f62f0
LC
596(define version-prefix?
597 (let ((not-dot (char-set-complement (char-set #\.))))
598 (lambda (v1 v2)
599 "Return true if V1 is a version prefix of V2:
600
601 (version-prefix? \"4.1\" \"4.16.2\") => #f
602 (version-prefix? \"4.1\" \"4.1.2\") => #t
603"
604 (define (list-prefix? lst1 lst2)
605 (match lst1
606 (() #t)
607 ((head1 tail1 ...)
608 (match lst2
609 (() #f)
610 ((head2 tail2 ...)
611 (and (equal? head1 head2)
612 (list-prefix? tail1 tail2)))))))
613
614 (list-prefix? (string-tokenize v1 not-dot)
615 (string-tokenize v2 not-dot)))))
616
d67a8819
LC
617\f
618;;;
619;;; Files.
620;;;
621
0fdd3bea
LC
622(define (file-extension file)
623 "Return the extension of FILE or #f if there is none."
624 (let ((dot (string-rindex file #\.)))
625 (and dot (substring file (+ 1 dot) (string-length file)))))
626
ac10e0e1
LC
627(define (file-sans-extension file)
628 "Return the substring of FILE without its extension, if any."
629 (let ((dot (string-rindex file #\.)))
630 (if dot
631 (substring file 0 dot)
632 file)))
633
da1027a7
HG
634(define (tarball-sans-extension tarball)
635 "Return TARBALL without its .tar.* or .zip extension."
636 (let ((end (or (string-contains tarball ".tar")
637 (string-contains tarball ".zip"))))
638 (substring tarball 0 end)))
639
089b1678
LC
640(define (compressed-file? file)
641 "Return true if FILE denotes a compressed file."
642 (->bool (member (file-extension file)
f4111243 643 '("gz" "bz2" "xz" "lz" "lzma" "tgz" "tbz2" "zip"))))
089b1678 644
3bb168b0
LC
645(define (switch-symlinks link target)
646 "Atomically switch LINK, a symbolic link, to point to TARGET. Works
647both when LINK already exists and when it does not."
648 (let ((pivot (string-append link ".new")))
649 (symlink target pivot)
650 (rename-file pivot link)))
651
56b943de
LC
652(define* (string-replace-substring str substr replacement
653 #:optional
654 (start 0)
655 (end (string-length str)))
656 "Replace all occurrences of SUBSTR in the START--END range of STR by
657REPLACEMENT."
658 (match (string-length substr)
659 (0
660 (error "string-replace-substring: empty substring"))
661 (substr-length
662 (let loop ((start start)
663 (pieces (list (substring str 0 start))))
664 (match (string-contains str substr start end)
665 (#f
666 (string-concatenate-reverse
667 (cons (substring str start) pieces)))
668 (index
669 (loop (+ index substr-length)
670 (cons* replacement
671 (substring str start index)
672 pieces))))))))
673
861693f3
LC
674(define (call-with-temporary-output-file proc)
675 "Call PROC with a name of a temporary file and open output port to that
676file; close the file and delete it when leaving the dynamic extent of this
677call."
57db49cc
LC
678 (let* ((directory (or (getenv "TMPDIR") "/tmp"))
679 (template (string-append directory "/guix-file.XXXXXX"))
680 (out (mkstemp! template)))
861693f3
LC
681 (dynamic-wind
682 (lambda ()
683 #t)
684 (lambda ()
685 (proc template out))
686 (lambda ()
687 (false-if-exception (close out))
688 (false-if-exception (delete-file template))))))
04d4c8a4 689
db6e5e2b
DT
690(define (call-with-temporary-directory proc)
691 "Call PROC with a name of a temporary directory; close the directory and
692delete it when leaving the dynamic extent of this call."
693 (let* ((directory (or (getenv "TMPDIR") "/tmp"))
694 (template (string-append directory "/guix-directory.XXXXXX"))
695 (tmp-dir (mkdtemp! template)))
696 (dynamic-wind
697 (const #t)
698 (lambda ()
699 (proc tmp-dir))
700 (lambda ()
27f7cbc9 701 (false-if-exception (delete-file-recursively tmp-dir))))))
db6e5e2b 702
04d4c8a4
LC
703(define (with-atomic-file-output file proc)
704 "Call PROC with an output port for the file that is going to replace FILE.
705Upon success, FILE is atomically replaced by what has been written to the
706output port, and PROC's result is returned."
707 (let* ((template (string-append file ".XXXXXX"))
708 (out (mkstemp! template)))
709 (with-throw-handler #t
710 (lambda ()
711 (let ((result (proc out)))
1752a17a
LC
712 (fdatasync out)
713 (close-port out)
04d4c8a4
LC
714 (rename-file template file)
715 result))
716 (lambda (key . args)
c25637df
LC
717 (false-if-exception (delete-file template))
718 (close-port out)))))
861693f3 719
f0e492f0
LC
720(define* (xdg-directory variable suffix #:key (ensure? #t))
721 "Return the name of the XDG directory that matches VARIABLE and SUFFIX,
722after making sure that it exists if ENSURE? is true. VARIABLE is an
723environment variable name like \"XDG_CONFIG_HOME\"; SUFFIX is a suffix like
724\"/.config\". Honor the XDG specs,
725<http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html>."
726 (let ((dir (and=> (or (getenv variable)
727 (and=> (or (getenv "HOME")
728 (passwd:dir (getpwuid (getuid))))
729 (cut string-append <> suffix)))
730 (cut string-append <> "/guix"))))
731 (when ensure?
732 (mkdir-p dir))
733 dir))
734
735(define config-directory
736 (cut xdg-directory "XDG_CONFIG_HOME" "/.config" <...>))
737
738(define cache-directory
739 (cut xdg-directory "XDG_CACHE_HOME" "/.cache" <...>))
739ab68b 740
d50cb56d
LC
741(define (readlink* file)
742 "Call 'readlink' until the result is not a symlink."
743 (define %max-symlink-depth 50)
744
745 (let loop ((file file)
746 (depth 0))
747 (define (absolute target)
748 (if (absolute-file-name? target)
749 target
750 (string-append (dirname file) "/" target)))
751
752 (if (>= depth %max-symlink-depth)
753 file
754 (call-with-values
755 (lambda ()
756 (catch 'system-error
757 (lambda ()
758 (values #t (readlink file)))
759 (lambda args
760 (let ((errno (system-error-errno args)))
761 (if (or (= errno EINVAL))
762 (values #f file)
763 (apply throw args))))))
764 (lambda (success? target)
765 (if success?
766 (loop (absolute target) (+ depth 1))
767 file))))))
c8be6f0d
FB
768
769(define (canonical-newline-port port)
770 "Return an input port that wraps PORT such that all newlines consist
3149c002 771 of a single linefeed."
c8be6f0d
FB
772 (define (get-position)
773 (if (port-has-port-position? port) (port-position port) #f))
774 (define (set-position! position)
775 (if (port-has-set-port-position!? port)
776 (set-port-position! position port)
777 #f))
778 (define (close) (close-port port))
779 (define (read! bv start n)
780 (let loop ((count 0)
781 (byte (get-u8 port)))
782 (cond ((eof-object? byte) count)
3149c002
RV
783 ;; XXX: consume all CRs even if not followed by LF.
784 ((eqv? byte (char->integer #\return)) (loop count (get-u8 port)))
c8be6f0d
FB
785 ((= count (- n 1))
786 (bytevector-u8-set! bv (+ start count) byte)
787 n)
c8be6f0d
FB
788 (else
789 (bytevector-u8-set! bv (+ start count) byte)
790 (loop (+ count 1) (get-u8 port))))))
791 (make-custom-binary-input-port "canonical-newline-port"
792 read!
793 get-position
794 set-position!
795 close))
ff352cfb
LC
796\f
797;;;
798;;; Source location.
799;;;
800
87b711d2
LC
801(define absolute-dirname
802 ;; Memoize to avoid repeated 'stat' storms from 'search-path'.
803 (mlambda (file)
804 "Return the absolute name of the directory containing FILE, or #f upon
cbbbb7be 805failure."
87b711d2
LC
806 (match (search-path %load-path file)
807 (#f #f)
808 ((? string? file)
809 ;; If there are relative names in %LOAD-PATH, FILE can be relative and
810 ;; needs to be canonicalized.
811 (if (string-prefix? "/" file)
812 (dirname file)
813 (canonicalize-path (dirname file)))))))
cbbbb7be 814
5dbae738
LC
815(define-syntax current-source-directory
816 (lambda (s)
d4dd37fc
LC
817 "Return the absolute name of the current directory, or #f if it could not
818be determined."
5dbae738
LC
819 (syntax-case s ()
820 ((_)
82781d87 821 (match (assq 'filename (or (syntax-source s) '()))
5dbae738 822 (('filename . (? string? file-name))
d4dd37fc 823 ;; If %FILE-PORT-NAME-CANONICALIZATION is 'relative, then FILE-NAME
cbbbb7be
LC
824 ;; can be relative. In that case, we try to find out at run time
825 ;; the absolute file name by looking at %LOAD-PATH; doing this at
826 ;; run time rather than expansion time is necessary to allow files
827 ;; to be moved on the file system.
b997d432
AS
828 (if (string-prefix? "/" file-name)
829 (dirname file-name)
830 #`(absolute-dirname #,file-name)))
831 ((or ('filename . #f) #f)
832 ;; raising an error would upset Geiser users
5dbae738 833 #f))))))
07c8a98c 834
79864851
SB
835;;; Local Variables:
836;;; eval: (put 'call-with-progress-reporter 'scheme-indent-function 1)
837;;; End: