b9a95420ab
Fixes <https://bugs.gnu.org/49102>.
Reported by Mathieu Othacehe <othacehe@gnu.org>
and Tobias Geerinckx-Rice <me@tobias.gr>.
Fixes a regression introduced in
8cef92d063
, whereby in case of file
collisions, the "wrong" one would take precedence.
* guix/build/profiles.scm (manifest-sexp->inputs+search-paths): Perform
a breadth-first traversal. Reverse INPUTS and SEARCH-PATHS in the base
case.
* tests/profiles.scm ("profile-derivation, ordering & collisions"):
New test.
216 lines
8.4 KiB
Scheme
216 lines
8.4 KiB
Scheme
;;; GNU Guix --- Functional package management for GNU
|
|
;;; Copyright © 2015, 2017, 2018, 2019, 2020, 2021 Ludovic Courtès <ludo@gnu.org>
|
|
;;;
|
|
;;; This file is part of GNU Guix.
|
|
;;;
|
|
;;; GNU Guix is free software; you can redistribute it and/or modify it
|
|
;;; under the terms of the GNU General Public License as published by
|
|
;;; the Free Software Foundation; either version 3 of the License, or (at
|
|
;;; your option) any later version.
|
|
;;;
|
|
;;; GNU Guix is distributed in the hope that it will be useful, but
|
|
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;;; GNU General Public License for more details.
|
|
;;;
|
|
;;; You should have received a copy of the GNU General Public License
|
|
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
(define-module (guix build profiles)
|
|
#:use-module (guix build union)
|
|
#:use-module (guix build utils)
|
|
#:use-module (guix search-paths)
|
|
#:use-module (srfi srfi-1)
|
|
#:use-module (srfi srfi-11)
|
|
#:use-module (srfi srfi-26)
|
|
#:use-module (ice-9 ftw)
|
|
#:use-module (ice-9 match)
|
|
#:use-module (ice-9 pretty-print)
|
|
#:re-export (symlink-relative) ;for convenience
|
|
#:export (ensure-writable-directory
|
|
build-profile))
|
|
|
|
;;; Commentary:
|
|
;;;
|
|
;;; Build a user profile (essentially the union of all the installed packages)
|
|
;;; with its associated meta-data.
|
|
;;;
|
|
;;; Code:
|
|
|
|
(define (abstract-profile profile)
|
|
"Return a procedure that replaces PROFILE in VALUE with a reference to the
|
|
'GUIX_PROFILE' environment variable. This allows users to specify what the
|
|
user-friendly name of the profile is, for instance ~/.guix-profile rather than
|
|
/gnu/store/...-profile."
|
|
(let ((replacement (string-append "${GUIX_PROFILE:-" profile "}"))
|
|
(crop (cute string-drop <> (string-length profile))))
|
|
(match-lambda
|
|
((search-path . value)
|
|
(match (search-path-specification-separator search-path)
|
|
(#f
|
|
(cons search-path
|
|
(string-append replacement (crop value))))
|
|
((? string? separator)
|
|
(let ((items (string-tokenize* value separator)))
|
|
(cons search-path
|
|
(string-join (map (lambda (str)
|
|
(string-append replacement (crop str)))
|
|
items)
|
|
separator)))))))))
|
|
|
|
(define (write-environment-variable-definition port)
|
|
"Write the given environment variable definition to PORT."
|
|
(match-lambda
|
|
((search-path . value)
|
|
(display (search-path-definition search-path value #:kind 'prefix)
|
|
port)
|
|
(newline port))))
|
|
|
|
(define (build-etc/profile output search-paths)
|
|
"Build the 'OUTPUT/etc/profile' shell file containing environment variable
|
|
definitions for all the SEARCH-PATHS."
|
|
(define file
|
|
(string-append output "/etc/profile"))
|
|
|
|
(mkdir-p (dirname file))
|
|
(when (file-exists? file)
|
|
(delete-file file))
|
|
|
|
(call-with-output-file file
|
|
(lambda (port)
|
|
;; The use of $GUIX_PROFILE described below is not great. Another
|
|
;; option would have been to use "$1" and have users run:
|
|
;;
|
|
;; source ~/.guix-profile/etc/profile ~/.guix-profile
|
|
;;
|
|
;; However, when 'source' is used with no arguments, $1 refers to the
|
|
;; first positional parameter of the calling script, so we cannot rely
|
|
;; on it.
|
|
(display "\
|
|
# Source this file to define all the relevant environment variables in Bash
|
|
# for this profile. You may want to define the 'GUIX_PROFILE' environment
|
|
# variable to point to the \"visible\" name of the profile, like this:
|
|
#
|
|
# GUIX_PROFILE=/path/to/profile ; \\
|
|
# source /path/to/profile/etc/profile
|
|
#
|
|
# When GUIX_PROFILE is undefined, the various environment variables refer
|
|
# to this specific profile generation.
|
|
\n" port)
|
|
(let ((variables (evaluate-search-paths search-paths
|
|
(list output))))
|
|
(for-each (write-environment-variable-definition port)
|
|
(map (abstract-profile output) variables))))))
|
|
|
|
(define* (ensure-writable-directory directory
|
|
#:key (symlink symlink))
|
|
"Ensure DIRECTORY exists and is writable. If DIRECTORY is currently a
|
|
symlink (to a read-only directory in the store), then delete the symlink and
|
|
instead make DIRECTORY a \"real\" directory containing symlinks."
|
|
(define (absolute? file)
|
|
(string-prefix? "/" file))
|
|
|
|
(define (unsymlink link)
|
|
(let* ((target (match (readlink link)
|
|
((? absolute? target)
|
|
target)
|
|
((? string? relative)
|
|
(string-append (dirname link) "/" relative))))
|
|
;; TARGET might itself be a symlink, so append "/" to make sure
|
|
;; 'scandir' enters it.
|
|
(files (scandir (string-append target "/")
|
|
(negate (cut member <> '("." ".."))))))
|
|
(delete-file link)
|
|
(mkdir link)
|
|
(for-each (lambda (file)
|
|
(symlink (string-append target "/" file)
|
|
(string-append link "/" file)))
|
|
files)))
|
|
|
|
(catch 'system-error
|
|
(lambda ()
|
|
(mkdir directory))
|
|
(lambda args
|
|
(let ((errno (system-error-errno args)))
|
|
(if (= errno EEXIST)
|
|
(let ((stat (lstat directory)))
|
|
(case (stat:type stat)
|
|
((symlink)
|
|
;; "Unsymlink" DIRECTORY so that it is writable.
|
|
(unsymlink directory))
|
|
((directory)
|
|
#t)
|
|
(else
|
|
(error "cannot mkdir because a same-named file exists"
|
|
directory))))
|
|
(apply throw args))))))
|
|
|
|
(define (manifest-sexp->inputs+search-paths manifest)
|
|
"Parse MANIFEST, an sexp as produced by 'manifest->gexp', and return two
|
|
values: the list of store items of its manifest entries, and the list of
|
|
search path specifications."
|
|
(match manifest ;this must match 'manifest->gexp'
|
|
(('manifest ('version 3)
|
|
('packages (entries ...)))
|
|
(let loop ((entries entries)
|
|
(inputs '())
|
|
(search-paths '()))
|
|
(match entries
|
|
(((name version output item
|
|
('propagated-inputs deps)
|
|
('search-paths paths) _ ...) . rest)
|
|
(loop (append rest deps) ;breadth-first traversal
|
|
(cons item inputs)
|
|
(append paths search-paths)))
|
|
(()
|
|
(values (reverse inputs)
|
|
(delete-duplicates
|
|
(cons $PATH
|
|
(map sexp->search-path-specification
|
|
(reverse search-paths)))))))))))
|
|
|
|
(define* (build-profile output manifest
|
|
#:key (extra-inputs '()) (symlink symlink))
|
|
"Build a user profile from MANIFEST, an sexp, and EXTRA-INPUTS, a list of
|
|
store items, in directory OUTPUT, using SYMLINK to create symlinks. Create
|
|
OUTPUT/etc/profile with Bash definitions for all the variables listed in the
|
|
search paths of MANIFEST's entries."
|
|
(define manifest-file
|
|
(string-append output "/manifest"))
|
|
|
|
(let-values (((inputs search-paths)
|
|
(manifest-sexp->inputs+search-paths manifest)))
|
|
|
|
;; Make the symlinks.
|
|
(union-build output (append extra-inputs inputs)
|
|
#:symlink symlink
|
|
#:log-port (%make-void-port "w"))
|
|
|
|
;; If one of the INPUTS provides a '/manifest' file, delete it. That can
|
|
;; happen if MANIFEST contains something such as a Guix instance, which is
|
|
;; ultimately built as a profile.
|
|
(when (file-exists? manifest-file)
|
|
(delete-file manifest-file))
|
|
|
|
;; Store meta-data.
|
|
(call-with-output-file manifest-file
|
|
(lambda (p)
|
|
(display "\
|
|
;; This file was automatically generated and is for internal use only.
|
|
;; It cannot be passed to the '--manifest' option.
|
|
;; Run 'guix package --export-manifest' if you want to export a file
|
|
;; suitable for '--manifest'.\n\n"
|
|
p)
|
|
(pretty-print manifest p)))
|
|
|
|
;; Make sure we can write to 'OUTPUT/etc'. 'union-build' above could have
|
|
;; made 'etc' a symlink to a read-only sub-directory in the store so we
|
|
;; need to work around that.
|
|
(ensure-writable-directory (string-append output "/etc")
|
|
#:symlink symlink)
|
|
|
|
;; Write 'OUTPUT/etc/profile'.
|
|
(build-etc/profile output search-paths)))
|
|
|
|
;;; profile.scm ends here
|