diff options
| -rw-r--r-- | Makefile.am | 1 | ||||
| -rw-r--r-- | doc/guix.texi | 33 | ||||
| -rw-r--r-- | guix/channels.scm | 122 | ||||
| -rw-r--r-- | tests/channels.scm | 139 |
4 files changed, 279 insertions, 16 deletions
diff --git a/Makefile.am b/Makefile.am index a7a67e81cfc..4a190c40957 100644 --- a/Makefile.am +++ b/Makefile.am | |||
| @@ -329,6 +329,7 @@ SCM_TESTS = \ | |||
| 329 | tests/base16.scm \ | 329 | tests/base16.scm \ |
| 330 | tests/base32.scm \ | 330 | tests/base32.scm \ |
| 331 | tests/base64.scm \ | 331 | tests/base64.scm \ |
| 332 | tests/channels.scm \ | ||
| 332 | tests/cpan.scm \ | 333 | tests/cpan.scm \ |
| 333 | tests/cpio.scm \ | 334 | tests/cpio.scm \ |
| 334 | tests/crate.scm \ | 335 | tests/crate.scm \ |
diff --git a/doc/guix.texi b/doc/guix.texi index 4ef2601579a..20b5013fd9f 100644 --- a/doc/guix.texi +++ b/doc/guix.texi | |||
| @@ -3037,6 +3037,39 @@ the new and upgraded packages that are listed, some like @code{my-gimp} and | |||
| 3037 | @code{my-emacs-with-cool-features} might come from | 3037 | @code{my-emacs-with-cool-features} might come from |
| 3038 | @code{my-personal-packages}, while others come from the Guix default channel. | 3038 | @code{my-personal-packages}, while others come from the Guix default channel. |
| 3039 | 3039 | ||
| 3040 | @cindex dependencies, channels | ||
| 3041 | @cindex meta-data, channels | ||
| 3042 | @subsection Declaring Channel Dependencies | ||
| 3043 | |||
| 3044 | Channel authors may decide to augment a package collection provided by other | ||
| 3045 | channels. They can declare their channel to be dependent on other channels in | ||
| 3046 | a meta-data file @file{.guix-channel}, which is to be placed in the root of | ||
| 3047 | the channel repository. | ||
| 3048 | |||
| 3049 | The meta-data file should contain a simple S-expression like this: | ||
| 3050 | |||
| 3051 | @lisp | ||
| 3052 | (channel | ||
| 3053 | (version 0) | ||
| 3054 | (dependencies | ||
| 3055 | (channel | ||
| 3056 | (name 'some-collection) | ||
| 3057 | (url "https://example.org/first-collection.git")) | ||
| 3058 | (channel | ||
| 3059 | (name 'some-other-collection) | ||
| 3060 | (url "https://example.org/second-collection.git") | ||
| 3061 | (branch "testing")))) | ||
| 3062 | @end lisp | ||
| 3063 | |||
| 3064 | In the above example this channel is declared to depend on two other channels, | ||
| 3065 | which will both be fetched automatically. The modules provided by the channel | ||
| 3066 | will be compiled in an environment where the modules of all these declared | ||
| 3067 | channels are available. | ||
| 3068 | |||
| 3069 | For the sake of reliability and maintainability, you should avoid dependencies | ||
| 3070 | on channels that you don't control, and you should aim to keep the number of | ||
| 3071 | dependencies to a minimum. | ||
| 3072 | |||
| 3040 | @subsection Replicating Guix | 3073 | @subsection Replicating Guix |
| 3041 | 3074 | ||
| 3042 | @cindex pinning, channels | 3075 | @cindex pinning, channels |
diff --git a/guix/channels.scm b/guix/channels.scm index e57da68149a..75503bb0aeb 100644 --- a/guix/channels.scm +++ b/guix/channels.scm | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | 1 | ;;; GNU Guix --- Functional package management for GNU |
| 2 | ;;; Copyright © 2018 Ludovic Courtès <ludo@gnu.org> | 2 | ;;; Copyright © 2018 Ludovic Courtès <ludo@gnu.org> |
| 3 | ;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net> | ||
| 3 | ;;; | 4 | ;;; |
| 4 | ;;; This file is part of GNU Guix. | 5 | ;;; This file is part of GNU Guix. |
| 5 | ;;; | 6 | ;;; |
| @@ -27,6 +28,7 @@ | |||
| 27 | #:use-module (guix store) | 28 | #:use-module (guix store) |
| 28 | #:use-module (guix i18n) | 29 | #:use-module (guix i18n) |
| 29 | #:use-module (srfi srfi-1) | 30 | #:use-module (srfi srfi-1) |
| 31 | #:use-module (srfi srfi-2) | ||
| 30 | #:use-module (srfi srfi-9) | 32 | #:use-module (srfi srfi-9) |
| 31 | #:use-module (srfi srfi-11) | 33 | #:use-module (srfi srfi-11) |
| 32 | #:autoload (guix self) (whole-package) | 34 | #:autoload (guix self) (whole-package) |
| @@ -73,7 +75,6 @@ | |||
| 73 | (commit channel-commit (default #f)) | 75 | (commit channel-commit (default #f)) |
| 74 | (location channel-location | 76 | (location channel-location |
| 75 | (default (current-source-location)) (innate))) | 77 | (default (current-source-location)) (innate))) |
| 76 | ;; TODO: Add a way to express dependencies among channels. | ||
| 77 | 78 | ||
| 78 | (define %default-channels | 79 | (define %default-channels |
| 79 | ;; Default list of channels. | 80 | ;; Default list of channels. |
| @@ -93,6 +94,12 @@ | |||
| 93 | (commit channel-instance-commit) | 94 | (commit channel-instance-commit) |
| 94 | (checkout channel-instance-checkout)) | 95 | (checkout channel-instance-checkout)) |
| 95 | 96 | ||
| 97 | (define-record-type <channel-metadata> | ||
| 98 | (channel-metadata version dependencies) | ||
| 99 | channel-metadata? | ||
| 100 | (version channel-metadata-version) | ||
| 101 | (dependencies channel-metadata-dependencies)) | ||
| 102 | |||
| 96 | (define (channel-reference channel) | 103 | (define (channel-reference channel) |
| 97 | "Return the \"reference\" for CHANNEL, an sexp suitable for | 104 | "Return the \"reference\" for CHANNEL, an sexp suitable for |
| 98 | 'latest-repository-commit'." | 105 | 'latest-repository-commit'." |
| @@ -100,20 +107,90 @@ | |||
| 100 | (#f `(branch . ,(channel-branch channel))) | 107 | (#f `(branch . ,(channel-branch channel))) |
| 101 | (commit `(commit . ,(channel-commit channel))))) | 108 | (commit `(commit . ,(channel-commit channel))))) |
| 102 | 109 | ||
| 103 | (define (latest-channel-instances store channels) | 110 | (define (read-channel-metadata instance) |
| 111 | "Return a channel-metadata record read from the channel INSTANCE's | ||
| 112 | description file, or return #F if the channel instance does not include the | ||
| 113 | file." | ||
| 114 | (let* ((source (channel-instance-checkout instance)) | ||
| 115 | (meta-file (string-append source "/.guix-channel"))) | ||
| 116 | (and (file-exists? meta-file) | ||
| 117 | (and-let* ((raw (call-with-input-file meta-file read)) | ||
| 118 | (version (and=> (assoc-ref raw 'version) first)) | ||
| 119 | (dependencies (or (assoc-ref raw 'dependencies) '()))) | ||
| 120 | (channel-metadata | ||
| 121 | version | ||
| 122 | (map (lambda (item) | ||
| 123 | (let ((get (lambda* (key #:optional default) | ||
| 124 | (or (and=> (assoc-ref item key) first) default)))) | ||
| 125 | (and-let* ((name (get 'name)) | ||
| 126 | (url (get 'url)) | ||
| 127 | (branch (get 'branch "master"))) | ||
| 128 | (channel | ||
| 129 | (name name) | ||
| 130 | (branch branch) | ||
| 131 | (url url) | ||
| 132 | (commit (get 'commit)))))) | ||
| 133 | dependencies)))))) | ||
| 134 | |||
| 135 | (define (channel-instance-dependencies instance) | ||
| 136 | "Return the list of channels that are declared as dependencies for the given | ||
| 137 | channel INSTANCE." | ||
| 138 | (match (read-channel-metadata instance) | ||
| 139 | (#f '()) | ||
| 140 | (($ <channel-metadata> version dependencies) | ||
| 141 | dependencies))) | ||
| 142 | |||
| 143 | (define* (latest-channel-instances store channels #:optional (previous-channels '())) | ||
| 104 | "Return a list of channel instances corresponding to the latest checkouts of | 144 | "Return a list of channel instances corresponding to the latest checkouts of |
| 105 | CHANNELS." | 145 | CHANNELS and the channels on which they depend. PREVIOUS-CHANNELS is a list |
| 106 | (map (lambda (channel) | 146 | of previously processed channels." |
| 107 | (format (current-error-port) | 147 | ;; Only process channels that are unique, or that are more specific than a |
| 108 | (G_ "Updating channel '~a' from Git repository at '~a'...~%") | 148 | ;; previous channel specification. |
| 109 | (channel-name channel) | 149 | (define (ignore? channel others) |
| 110 | (channel-url channel)) | 150 | (member channel others |
| 111 | (let-values (((checkout commit) | 151 | (lambda (a b) |
| 112 | (latest-repository-commit store (channel-url channel) | 152 | (and (eq? (channel-name a) (channel-name b)) |
| 113 | #:ref (channel-reference | 153 | (or (channel-commit b) |
| 114 | channel)))) | 154 | (not (or (channel-commit a) |
| 115 | (channel-instance channel commit checkout))) | 155 | (channel-commit b)))))))) |
| 116 | channels)) | 156 | ;; Accumulate a list of instances. A list of processed channels is also |
| 157 | ;; accumulated to decide on duplicate channel specifications. | ||
| 158 | (match (fold (lambda (channel acc) | ||
| 159 | (match acc | ||
| 160 | ((#:channels previous-channels #:instances instances) | ||
| 161 | (if (ignore? channel previous-channels) | ||
| 162 | acc | ||
| 163 | (begin | ||
| 164 | (format (current-error-port) | ||
| 165 | (G_ "Updating channel '~a' from Git repository at '~a'...~%") | ||
| 166 | (channel-name channel) | ||
| 167 | (channel-url channel)) | ||
| 168 | (let-values (((checkout commit) | ||
| 169 | (latest-repository-commit store (channel-url channel) | ||
| 170 | #:ref (channel-reference | ||
| 171 | channel)))) | ||
| 172 | (let ((instance (channel-instance channel commit checkout))) | ||
| 173 | (let-values (((new-instances new-channels) | ||
| 174 | (latest-channel-instances | ||
| 175 | store | ||
| 176 | (channel-instance-dependencies instance) | ||
| 177 | previous-channels))) | ||
| 178 | `(#:channels | ||
| 179 | ,(append (cons channel new-channels) | ||
| 180 | previous-channels) | ||
| 181 | #:instances | ||
| 182 | ,(append (cons instance new-instances) | ||
| 183 | instances)))))))))) | ||
| 184 | `(#:channels ,previous-channels #:instances ()) | ||
| 185 | channels) | ||
| 186 | ((#:channels channels #:instances instances) | ||
| 187 | (let ((instance-name (compose channel-name channel-instance-channel))) | ||
| 188 | ;; Remove all earlier channel specifications if they are followed by a | ||
| 189 | ;; more specific one. | ||
| 190 | (values (delete-duplicates instances | ||
| 191 | (lambda (a b) | ||
| 192 | (eq? (instance-name a) (instance-name b)))) | ||
| 193 | channels))))) | ||
| 117 | 194 | ||
| 118 | (define* (checkout->channel-instance checkout | 195 | (define* (checkout->channel-instance checkout |
| 119 | #:key commit | 196 | #:key commit |
| @@ -235,8 +312,21 @@ INSTANCES." | |||
| 235 | (lambda (instance) | 312 | (lambda (instance) |
| 236 | (if (eq? instance core-instance) | 313 | (if (eq? instance core-instance) |
| 237 | (return core) | 314 | (return core) |
| 238 | (build-channel-instance instance | 315 | (match (channel-instance-dependencies instance) |
| 239 | (cons core dependencies)))) | 316 | (() |
| 317 | (build-channel-instance instance | ||
| 318 | (cons core dependencies))) | ||
| 319 | (channels | ||
| 320 | (mlet %store-monad ((dependencies-derivation | ||
| 321 | (latest-channel-derivation | ||
| 322 | ;; %default-channels is used here to | ||
| 323 | ;; ensure that the core channel is | ||
| 324 | ;; available for channels declared as | ||
| 325 | ;; dependencies. | ||
| 326 | (append channels %default-channels)))) | ||
| 327 | (build-channel-instance instance | ||
| 328 | (cons dependencies-derivation | ||
| 329 | (cons core dependencies)))))))) | ||
| 240 | instances))) | 330 | instances))) |
| 241 | 331 | ||
| 242 | (define (whole-package-for-legacy name modules) | 332 | (define (whole-package-for-legacy name modules) |
diff --git a/tests/channels.scm b/tests/channels.scm new file mode 100644 index 00000000000..f3fc383ac31 --- /dev/null +++ b/tests/channels.scm | |||
| @@ -0,0 +1,139 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | ||
| 2 | ;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net> | ||
| 3 | ;;; | ||
| 4 | ;;; This file is part of GNU Guix. | ||
| 5 | ;;; | ||
| 6 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | ;;; under the terms of the GNU General Public License as published by | ||
| 8 | ;;; the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | ;;; your option) any later version. | ||
| 10 | ;;; | ||
| 11 | ;;; GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | ;;; GNU General Public License for more details. | ||
| 15 | ;;; | ||
| 16 | ;;; You should have received a copy of the GNU General Public License | ||
| 17 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | ||
| 18 | |||
| 19 | (define-module (test-channels) | ||
| 20 | #:use-module (guix channels) | ||
| 21 | #:use-module ((guix build syscalls) #:select (mkdtemp!)) | ||
| 22 | #:use-module (guix tests) | ||
| 23 | #:use-module (srfi srfi-1) | ||
| 24 | #:use-module (srfi srfi-64) | ||
| 25 | #:use-module (ice-9 match)) | ||
| 26 | |||
| 27 | (test-begin "channels") | ||
| 28 | |||
| 29 | (define* (make-instance #:key | ||
| 30 | (name 'fake) | ||
| 31 | (commit "cafebabe") | ||
| 32 | (spec #f)) | ||
| 33 | (define instance-dir (mkdtemp! "/tmp/checkout.XXXXXX")) | ||
| 34 | (and spec | ||
| 35 | (with-output-to-file (string-append instance-dir "/.guix-channel") | ||
| 36 | (lambda _ (format #t "~a" spec)))) | ||
| 37 | ((@@ (guix channels) channel-instance) | ||
| 38 | name commit instance-dir)) | ||
| 39 | |||
| 40 | (define instance--boring (make-instance)) | ||
| 41 | (define instance--no-deps | ||
| 42 | (make-instance #:spec | ||
| 43 | '(channel | ||
| 44 | (version 0) | ||
| 45 | (dependencies | ||
| 46 | (channel | ||
| 47 | (name test-channel) | ||
| 48 | (url "https://example.com/test-channel")))))) | ||
| 49 | (define instance--simple | ||
| 50 | (make-instance #:spec | ||
| 51 | '(channel | ||
| 52 | (version 0) | ||
| 53 | (dependencies | ||
| 54 | (channel | ||
| 55 | (name test-channel) | ||
| 56 | (url "https://example.com/test-channel")))))) | ||
| 57 | (define instance--with-dupes | ||
| 58 | (make-instance #:spec | ||
| 59 | '(channel | ||
| 60 | (version 0) | ||
| 61 | (dependencies | ||
| 62 | (channel | ||
| 63 | (name test-channel) | ||
| 64 | (url "https://example.com/test-channel")) | ||
| 65 | (channel | ||
| 66 | (name test-channel) | ||
| 67 | (url "https://example.com/test-channel") | ||
| 68 | (commit "abc1234")) | ||
| 69 | (channel | ||
| 70 | (name test-channel) | ||
| 71 | (url "https://example.com/test-channel-elsewhere")))))) | ||
| 72 | |||
| 73 | (define read-channel-metadata | ||
| 74 | (@@ (guix channels) read-channel-metadata)) | ||
| 75 | |||
| 76 | |||
| 77 | (test-equal "read-channel-metadata returns #f if .guix-channel does not exist" | ||
| 78 | #f | ||
| 79 | (read-channel-metadata instance--boring)) | ||
| 80 | |||
| 81 | (test-assert "read-channel-metadata returns <channel-metadata>" | ||
| 82 | (every (@@ (guix channels) channel-metadata?) | ||
| 83 | (map read-channel-metadata | ||
| 84 | (list instance--no-deps | ||
| 85 | instance--simple | ||
| 86 | instance--with-dupes)))) | ||
| 87 | |||
| 88 | (test-assert "read-channel-metadata dependencies are channels" | ||
| 89 | (let ((deps ((@@ (guix channels) channel-metadata-dependencies) | ||
| 90 | (read-channel-metadata instance--simple)))) | ||
| 91 | (match deps | ||
| 92 | (((? channel? dep)) #t) | ||
| 93 | (_ #f)))) | ||
| 94 | |||
| 95 | (test-assert "latest-channel-instances includes channel dependencies" | ||
| 96 | (let* ((channel (channel | ||
| 97 | (name 'test) | ||
| 98 | (url "test"))) | ||
| 99 | (test-dir (channel-instance-checkout instance--simple))) | ||
| 100 | (mock ((guix git) latest-repository-commit | ||
| 101 | (lambda* (store url #:key ref) | ||
| 102 | (match url | ||
| 103 | ("test" (values test-dir 'whatever)) | ||
| 104 | (_ (values "/not-important" 'not-important))))) | ||
| 105 | (let ((instances (latest-channel-instances #f (list channel)))) | ||
| 106 | (and (eq? 2 (length instances)) | ||
| 107 | (lset= eq? | ||
| 108 | '(test test-channel) | ||
| 109 | (map (compose channel-name channel-instance-channel) | ||
| 110 | instances))))))) | ||
| 111 | |||
| 112 | (test-assert "latest-channel-instances excludes duplicate channel dependencies" | ||
| 113 | (let* ((channel (channel | ||
| 114 | (name 'test) | ||
| 115 | (url "test"))) | ||
| 116 | (test-dir (channel-instance-checkout instance--with-dupes))) | ||
| 117 | (mock ((guix git) latest-repository-commit | ||
| 118 | (lambda* (store url #:key ref) | ||
| 119 | (match url | ||
| 120 | ("test" (values test-dir 'whatever)) | ||
| 121 | (_ (values "/not-important" 'not-important))))) | ||
| 122 | (let ((instances (latest-channel-instances #f (list channel)))) | ||
| 123 | (and (eq? 2 (length instances)) | ||
| 124 | (lset= eq? | ||
| 125 | '(test test-channel) | ||
| 126 | (map (compose channel-name channel-instance-channel) | ||
| 127 | instances)) | ||
| 128 | ;; only the most specific channel dependency should remain, | ||
| 129 | ;; i.e. the one with a specified commit. | ||
| 130 | (find (lambda (instance) | ||
| 131 | (and (eq? (channel-name | ||
| 132 | (channel-instance-channel instance)) | ||
| 133 | 'test-channel) | ||
| 134 | (eq? (channel-commit | ||
| 135 | (channel-instance-channel instance)) | ||
| 136 | 'abc1234))) | ||
| 137 | instances)))))) | ||
| 138 | |||
| 139 | (test-end "channels") | ||
