guix-play/guix/build/profiles.scm
Ludovic Courtès b9a95420ab
profiles: Build union of inputs in the right order.
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.
2021-06-22 17:55:05 +02:00

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