;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2012-2021 Ludovic Courtès ;;; Copyright © 2017, 2020 Jan (janneke) Nieuwenhuizen ;;; Copyright © 2018, 2019 Clément Lassieur ;;; Copyright © 2020 Julien Lepiller ;;; Copyright © 2020, 2021 Mathieu Othacehe ;;; ;;; 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 . (define-module (gnu ci) #:use-module (guix channels) #:use-module (guix config) #:use-module (guix describe) #:use-module (guix store) #:use-module (guix grafts) #:use-module (guix profiles) #:use-module (guix packages) #:autoload (guix transformations) (tunable-package? tuned-package) #:use-module (guix channels) #:use-module (guix config) #:use-module (guix derivations) #:use-module (guix build-system) #:use-module (guix monads) #:use-module (guix gexp) #:use-module (guix ui) #:use-module ((guix licenses) #:select (gpl3+ license? license-name)) #:use-module ((guix utils) #:select (%current-system)) #:use-module ((guix scripts system) #:select (read-operating-system)) #:use-module ((guix scripts pack) #:select (lookup-compressor self-contained-tarball)) #:use-module (gnu bootloader) #:use-module (gnu bootloader u-b
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2019 David Thompson <davet@gnu.org>
;;; Copyright © 2019 Jakob L. Kreuze <zerodaysfordays@sdf.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 (gnu machine)
  #:use-module (gnu system)
  #:use-module (guix derivations)
  #:use-module (guix monads)
  #:use-module (guix records)
  #:use-module (guix store)
  #:use-module ((guix diagnostics) #:select (source-properties->location))
  #:use-module (srfi srfi-35)
  #:export (environment-type
            environment-type?
            environment-type-name
            environment-type-description
            environment-type-location

            machine
            machine?

            machine-operating-system
            machine-environment
            machine-configuration
            machine-display-name

            deploy-machine
            roll-back-machine
            machine-remote-eval

            &deploy-error
            deploy-error?
            deploy-error-should-roll-back
            deploy-error-captured-args))

;;; Commentary:
;;;
;;; This module provides the types used to declare individual machines in a
;;; heterogeneous Guix deployment. The interface allows users to specify system
;;; configurations and the means by which resources should be provisioned on a
;;; per-host basis.
;;;
;;; Code:


;;;
;;; Declarations for resources that can be provisioned.
;;;

(define-record-type* <environment-type> environment-type
  make-environment-type
  environment-type?

  ;; Interface to the environment type's deployment code. Each procedure
  ;; should take the same arguments as the top-level procedure of this file
  ;; that shares the same name. For example, 'machine-remote-eval' should be
  ;; of the form '(machine-remote-eval machine exp)'.
  (machine-remote-eval environment-type-machine-remote-eval) ; procedure
  (deploy-machine      environment-type-deploy-machine)      ; procedure
  (roll-back-machine   environment-type-roll-back-machine)   ; procedure

  ;; Metadata.
  (name        environment-type-name)       ; symbol
  (description environment-type-description ; string
               (default #f))
  (location    environment-type-location    ; <location>
               (default (and=> (current-source-location)
                               source-properties->location))
               (innate)))


;;;
;;; Declarations for machines in a deployment.
;;;

(define-record-type* <machine> machine make-machine
  machine?
  (operating-system %machine-operating-system); <operating-system>
  (environment      machine-environment)      ; symbol
  (configuration    machine-configuration     ; configuration object
                    (default #f)))            ; specific to environment

(define (machine-operating-system machine)
  "Return the operating system of MACHINE."
  (operating-system-with-provenance
   (%machine-operating-system machine)))

(define (machine-display-name machine)
  "Return the host-name identifying MACHINE."
  (operating-system-host-name (machine-operating-system machine)))

(define (machine-remote-eval machine exp)
  "Evaluate EXP, a gexp, on MACHINE. Ensure that all the elements EXP refers to
are built and deployed to MACHINE beforehand."
  (let ((environment (machine-environment machine)))
    ((environment-type-machine-remote-eval environment) machine exp)))

(define (deploy-machine machine)
  "Monadic procedure transferring the new system's OS closure to the remote
MACHINE, activating it on MACHINE and switching MACHINE to the new generation."
  (let ((environment (machine-environment machine)))
    ((environment-type-deploy-machine environment) machine)))

(define (roll-back-machine machine)
  "Monadic procedure rolling back to the previous system generation on
MACHINE. Return the number of the generation that was current before switching
and the new generation number."
  (let ((environment (machine-environment machine)))
    ((environment-type-roll-back-machine environment) machine)))


;;;
;;; Error types.
;;;

(define-condition-type &deploy-error &error
  deploy-error?
  (should-roll-back deploy-error-should-roll-back)
  (captured-args deploy-error-captured-args))
-gnuabi64. (and (or (string-prefix? "i686-" system) (string-prefix? "i586-" system) (string-prefix? "armhf-" system)) (string-contains target "64"))) ;x86_64, mips64el, aarch64, etc. (define (same? target) ;; Return true if SYSTEM and TARGET are the same thing. This is so we ;; don't try to cross-compile to 'mips64el-linux-gnu' from ;; 'mips64el-linux'. (or (string-contains target system) (and (string-prefix? "armhf" system) ;armhf-linux (string-prefix? "arm" target)))) ;arm-linux-gnueabihf (define (pointless? target) ;; Return #t if it makes no sense to cross-build to TARGET from SYSTEM. (match system ((or "x86_64-linux" "i686-linux") (if (string-contains target "mingw") (not (string=? "x86_64-linux" system)) #f)) (_ ;; Don't try to cross-compile from non-Intel platforms: this isn't ;; very useful and these are often brittle configurations. #t))) (define (either proc1 proc2 proc3) (lambda (x) (or (proc1 x) (proc2 x) (proc3 x)))) (append-map (lambda (target) (map (lambda (package) (package-cross-job store (job-name package) package target system)) (packages-to-cross-build target))) (remove (either from-32-to-64? same? pointless?) %cross-targets))) (define* (guix-jobs store systems #:key source commit) "Return a list of jobs for Guix itself." (define build (primitive-load (string-append source "/build-aux/build-self.scm"))) (map (lambda (system) (let ((name (string->symbol (string-append "guix." system))) (drv (run-with-store store (build source #:version commit #:system system #:pull-version 1 #:guile-version "2.2")))) (derivation->job name drv))) systems)) ;; Architectures that are able to build or cross-build Guix System images. ;; This does not mean that other architectures are not supported, only that ;; they are often not fast enough to support Guix System images building. (define %guix-system-supported-systems '("x86_64-linux" "i686-linux")) (define %guix-system-images (list hurd-barebones-qcow2-image pine64-barebones-raw-image pinebook-pro-barebones-raw-image novena-barebones-raw-image)) (define (hours hours) (* 3600 hours)) (define* (image->job store image #:key name system) "Return the job for IMAGE on SYSTEM. If NAME is passed, use it as job name, otherwise use the IMAGE name." (let* ((image-name (or name (symbol->string (image-name image)))) (name (string-append image-name "." system)) (drv (run-with-store store (mbegin %store-monad (set-guile-for-build (default-guile)) (lower-object (system-image image)))))) (parameterize ((%graft? #f)) (derivation->job name drv)))) (define (image-jobs store system) "Return a list of jobs that build images for SYSTEM." (define MiB (expt 2 20)) (if (member system %guix-system-supported-systems) `(,(image->job store (image (inherit efi-disk-image) (operating-system installation-os)) #:name "usb-image" #:system system) ,(image->job store (image (inherit (image-with-label iso9660-image (string-append "GUIX_" system "_" (if (> (string-length %guix-version) 7) (substring %guix-version 0 7) %guix-version)))) (operating-system installation-os)) #:name "iso9660-image" #:system system) ;; Only cross-compile Guix System images from x86_64-linux for now. ,@(if (string=? system "x86_64-linux") (map (cut image->job store <> #:system system) %guix-system-images) '())) '())) (define channel-build-system ;; Build system used to "convert" a channel instance to a package. (let* ((build (lambda* (name inputs #:key source commit system #:allow-other-keys) (mlet* %store-monad ((source (if (string? source) (return source) (lower-object source))) (instance -> (checkout->channel-instance source #:commit commit))) (channel-instances->derivation (list instance))))) (lower (lambda* (name #:key system source commit #:allow-other-keys) (bag (name name) (system system) (build build) (arguments `(#:source ,source #:commit ,commit)))))) (build-system (name 'channel) (description "Turn a channel instance into a package.") (lower lower)))) (define* (channel-source->package source #:key commit) "Return a package for the given channel SOURCE, a lowerable object." (package (inherit guix) (version (string-append (package-version guix) "+")) (build-system channel-build-system) (arguments `(#:source ,source #:commit ,commit)) (inputs '()) (native-inputs '()) (propagated-inputs '()))) (define* (system-test-jobs store system #:key source commit) "Return a list of jobs for the system tests." (define (->job test) (let ((name (string-append "test." (system-test-name test) "." system)) (drv (run-with-store store (mbegin %store-monad (set-current-system system) (set-grafting #f) (set-guile-for-build (default-guile)) (system-test-value test))))) (derivation->job name drv))) (if (member system %guix-system-supported-systems) ;; Override the value of 'current-guix' used by system tests. Using a ;; channel instance makes tests that rely on 'current-guix' less ;; expensive. It also makes sure we get a valid Guix package when this ;; code is not running from a checkout. (parameterize ((current-guix-package (channel-source->package source #:commit commit))) (map ->job (all-system-tests))) '())) (define (tarball-jobs store system) "Return jobs to build the self-contained Guix binary tarball." (define (->job name drv) (let ((name (string-append name "." system))) (parameterize ((%graft? #f)) (derivation->job name drv)))) ;; XXX: Add a job for the stable Guix? (list (->job "binary-tarball" (run-with-store store (mbegin %store-monad (set-guile-for-build (default-guile)) (>>= (profile-derivation (packages->manifest (list guix))) (lambda (profile) (self-contained-tarball "guix-binary" profile #:profile-name "current-guix" #:localstatedir? #t #:compressor (lookup-compressor "xz"))))) #:system system)))) (define job-name ;; Return the name of a package's job. package-name) (define package->job (let ((base-packages (delete-duplicates (append-map (match-lambda ((_ package _ ...) (match (package-transitive-inputs package) (((_ inputs _ ...) ...) inputs)))) (%final-inputs))))) (lambda* (store package system #:key (suffix "")) "Return a job for PACKAGE on SYSTEM, or #f if this combination is not valid. Append SUFFIX to the job name." (cond ((member package base-packages) (package-job store (string-append "base." (job-name package)) package system #:suffix suffix)) ((supported-package? package system) (let ((drv (package-derivation store package system #:graft? #f))) (and (substitutable-derivation? drv) (package-job store (job-name package) package system #:suffix suffix)))) (else #f))))) (define %x86-64-micro-architectures ;; Micro-architectures for which we build tuned variants. '("westmere" "ivybridge" "haswell" "skylake" "skylake-avx512")) (define (tuned-package-jobs store package system) "Return a list of jobs for PACKAGE tuned for SYSTEM's micro-architectures." (filter-map (lambda (micro-architecture) (define suffix (string-append "." micro-architecture)) (package->job store (tuned-package package micro-architecture) system #:suffix suffix)) (match system ("x86_64-linux" %x86-64-micro-architectures) (_ '())))) (define (all-packages) "Return the list of packages to build." (define (adjust package result) (cond ((package-replacement package) ;; XXX: If PACKAGE and its replacement have the same name/version, ;; then both Cuirass jobs will have the same name, which ;; effectively means that the second one will be ignored. Thus, ;; return the replacement first. (cons* (package-replacement package) ;build both package result)) ((package-superseded package) result) ;don't build it (else (cons package result)))) (fold-packages adjust (fold adjust '() ;include base packages (match (%final-inputs) (((labels packages _ ...) ...) packages))) #:select? (const #t))) ;include hidden packages (define (arguments->manifests arguments channels) "Return the list of manifests extracted from ARGUMENTS." (map (lambda (manifest) (any (lambda (checkout) (let ((path (in-vicinity checkout manifest))) (and (file-exists? path) path))) (map channel-url channels))) arguments)) (define (manifests->jobs store manifests) "Return the list of jobs for the entries in MANIFESTS, a list of file names." (define (load-manifest manifest) (save-module-excursion (lambda () (set-current-module (make-user-module '((guix profiles) (gnu)))) (primitive-load manifest)))) (define (manifest-entry-job-name entry) (string-append (manifest-entry-name entry) "-" (manifest-entry-version entry))) (define (manifest-entry->job entry) (let* ((obj (manifest-entry-item entry)) (drv (parameterize ((%graft? #f)) (run-with-store store (lower-object obj)))) (max-silent-time (or (and (package? obj) (assoc-ref (package-properties obj) 'max-silent-time)) 3600)) (timeout (or (and (package? obj) (assoc-ref (package-properties obj) 'timeout)) (* 5 3600)))) (derivation->job (manifest-entry-job-name entry) drv #:max-silent-time max-silent-time #:timeout timeout))) (map manifest-entry->job (delete-duplicates (append-map (compose manifest-entries load-manifest) manifests) manifest-entry=?))) (define (arguments->systems arguments) "Return the systems list from ARGUMENTS." (match (assoc-ref arguments 'systems) (#f %cuirass-supported-systems) ((lst ...) lst) ((? string? str) (call-with-input-string str read)))) ;;; ;;; Cuirass entry point. ;;; (define (cuirass-jobs store arguments) "Register Cuirass jobs." (define subset (assoc-ref arguments 'subset)) (define systems (arguments->systems arguments)) (define channels (let ((channels (assq-ref arguments 'channels))) (map sexp->channel channels))) (define guix (find guix-channel? channels)) (define commit (channel-commit guix)) (define source (channel-url guix)) ;; Turn off grafts. Grafting is meant to happen on the user's machines. (parameterize ((%graft? #f)) ;; Return one job for each package, except bootstrap packages. (append-map (lambda (system) (format (current-error-port) "evaluating for '~a' (heap size: ~a MiB)...~%" system (round (/ (assoc-ref (gc-stats) 'heap-size) (expt 2. 20)))) (invalidate-derivation-caches!) (match subset ('all ;; Build everything, including replacements. (let ((all (all-packages)) (jobs (lambda (package) (match (package->job store package system) (#f '()) (main-job (cons main-job (if (tunable-package? package) (tuned-package-jobs store package system) '()))))))) (append (append-map jobs all) (cross-jobs store system)))) ('core ;; Build core packages only. (append (map (lambda (package) (package-job store (job-name package) package system)) (append (commencement-packages system) %core-packages)) (cross-jobs store system))) ('guix ;; Build Guix modules only. (guix-jobs store systems #:source source #:commit commit)) ('hello ;; Build hello package only. (let ((hello (specification->package "hello"))) (list (package-job store (job-name hello) hello system)))) ('images ;; Build Guix System images only. (image-jobs store system)) ('system-tests ;; Build Guix System tests only. (system-test-jobs store system #:source source #:commit commit)) ('tarball ;; Build Guix tarball only. (tarball-jobs store system)) (('custom . modules) ;; Build custom modules jobs only. (append-map (lambda (module) (let ((proc (module-ref (resolve-interface module) 'cuirass-jobs))) (proc store arguments))) modules)) (('channels . channels) ;; Build only the packages from CHANNELS. (let ((all (all-packages))) (filter-map (lambda (package) (any (lambda (channel) (and (member (channel-name channel) channels) (package->job store package system))) (package-channels package))) all))) (('packages . rest) ;; Build selected list of packages only. (let ((packages (map specification->package rest))) (map (lambda (package) (package-job store (job-name package) package system)) packages))) (('manifests . rest) ;; Build packages in the list of manifests. (let ((manifests (arguments->manifests rest channels))) (manifests->jobs store manifests))) (else (error "unknown subset" subset)))) systems)))