summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicolas Graves <ngraves@ngraves.fr>2023-11-02 16:16:48 +0100
committerLudovic Courtès <ludo@gnu.org>2023-12-18 23:16:28 +0100
commitb7e3945283daec8d6bc676b659d00ed73b8e82b7 (patch)
tree1f9c8511898585a00a0ea5b020865db15039339f
parent32d4f40589e32cbac6549c50962011084f13d9fc (diff)
guix: import: Add composer importer.
* guix/import/composer.scm: New file. * guix/scripts/import/composer.scm: New file. * guix/tests/composer.scm: New file. * Makefile.am: Add them. * guix/scripts/import.scm: Add composer importer. * doc/guix.texi (Invoking guix import): Mention it. Change-Id: I44a89b8cc80ef5b4a3cd15e8fbba4a18c1cea0b1 Co-authored-by: Julien Lepiller <julien@lepiller.eu> Co-authored-by: Ludovic Courtès <ludo@gnu.org>
-rw-r--r--Makefile.am3
-rw-r--r--doc/guix.texi20
-rw-r--r--guix/import/composer.scm268
-rw-r--r--guix/scripts/import.scm2
-rw-r--r--guix/scripts/import/composer.scm107
-rw-r--r--tests/composer.scm88
6 files changed, 487 insertions, 1 deletions
diff --git a/Makefile.am b/Makefile.am
index 0b733108d43..ee0f5ba4fc6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -283,6 +283,7 @@ MODULES = \
283 guix/search-paths.scm \ 283 guix/search-paths.scm \
284 guix/packages.scm \ 284 guix/packages.scm \
285 guix/import/cabal.scm \ 285 guix/import/cabal.scm \
286 guix/import/composer.scm \
286 guix/import/cpan.scm \ 287 guix/import/cpan.scm \
287 guix/import/cran.scm \ 288 guix/import/cran.scm \
288 guix/import/crate.scm \ 289 guix/import/crate.scm \
@@ -341,6 +342,7 @@ MODULES = \
341 guix/scripts/home/import.scm \ 342 guix/scripts/home/import.scm \
342 guix/scripts/lint.scm \ 343 guix/scripts/lint.scm \
343 guix/scripts/challenge.scm \ 344 guix/scripts/challenge.scm \
345 guix/scripts/import/composer.scm \
344 guix/scripts/import/crate.scm \ 346 guix/scripts/import/crate.scm \
345 guix/scripts/import/cpan.scm \ 347 guix/scripts/import/cpan.scm \
346 guix/scripts/import/cran.scm \ 348 guix/scripts/import/cran.scm \
@@ -509,6 +511,7 @@ SCM_TESTS = \
509 tests/challenge.scm \ 511 tests/challenge.scm \
510 tests/channels.scm \ 512 tests/channels.scm \
511 tests/combinators.scm \ 513 tests/combinators.scm \
514 tests/composer.scm \
512 tests/containers.scm \ 515 tests/containers.scm \
513 tests/cpan.scm \ 516 tests/cpan.scm \
514 tests/cpio.scm \ 517 tests/cpio.scm \
diff --git a/doc/guix.texi b/doc/guix.texi
index 7d7697d3181..ffd8ae331d7 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -14539,6 +14539,26 @@ Additional options include:
14539Traverse the dependency graph of the given upstream package recursively 14539Traverse the dependency graph of the given upstream package recursively
14540and generate package expressions for all those packages that are not yet 14540and generate package expressions for all those packages that are not yet
14541in Guix. 14541in Guix.
14542@end table
14543
14544@item composer
14545@cindex Composer
14546@cindex PHP
14547Import metadata from the @uref{https://getcomposer.org/, Composer} package
14548archive used by the PHP community, as in this example:
14549
14550@example
14551guix import composer phpunit/phpunit
14552@end example
14553
14554Additional options include:
14555
14556@table @code
14557@item --recursive
14558@itemx -r
14559Traverse the dependency graph of the given upstream package recursively
14560and generate package expressions for all those packages that are not yet
14561in Guix.
14542@item --repo 14562@item --repo
14543By default, packages are searched in the official OPAM repository. This 14563By default, packages are searched in the official OPAM repository. This
14544option, which can be used more than once, lets you add other repositories 14564option, which can be used more than once, lets you add other repositories
diff --git a/guix/import/composer.scm b/guix/import/composer.scm
new file mode 100644
index 00000000000..1ad608964b1
--- /dev/null
+++ b/guix/import/composer.scm
@@ -0,0 +1,268 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2019 Julien Lepiller <julien@lepiller.eu>
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 (guix import composer)
20 #:use-module (ice-9 match)
21 #:use-module (json)
22 #:use-module (guix hash)
23 #:use-module (guix base32)
24 #:use-module (guix build git)
25 #:use-module (guix build utils)
26 #:use-module (guix build-system)
27 #:use-module (guix build-system composer)
28 #:use-module (guix import json)
29 #:use-module (guix import utils)
30 #:use-module ((guix licenses) #:prefix license:)
31 #:use-module (guix memoization)
32 #:use-module (guix packages)
33 #:use-module (guix serialization)
34 #:use-module (guix upstream)
35 #:use-module (guix utils)
36 #:use-module (srfi srfi-1)
37 #:use-module (srfi srfi-11)
38 #:use-module (srfi srfi-26)
39 #:export (composer->guix-package
40 %composer-updater
41 composer-recursive-import
42
43 %composer-base-url))
44
45(define %composer-base-url
46 (make-parameter "https://repo.packagist.org"))
47
48(define (fix-version version)
49 "Return a fixed version from a version string. For instance, v10.1 -> 10.1"
50 (cond
51 ((string-prefix? "version" version)
52 (if (char-set-contains? char-set:digit (string-ref version 7))
53 (substring version 7)
54 (substring version 8)))
55 ((string-prefix? "v" version)
56 (substring version 1))
57 (else version)))
58
59(define (latest-version versions)
60 (fold (lambda (a b) (if (version>? (fix-version a) (fix-version b)) a b))
61 (car versions) versions))
62
63(define (json->require dict)
64 (if dict
65 (let loop ((result '()) (require dict))
66 (match require
67 (() result)
68 ((((? (cut string-contains <> "/") name) . _)
69 require ...)
70 (loop (cons name result) require))
71 ((_ require ...) (loop result require))
72 (_ result)))
73 '()))
74
75(define-json-mapping <composer-source> make-composer-source composer-source?
76 json->composer-source
77 (type composer-source-type)
78 (url composer-source-url)
79 (reference composer-source-reference))
80
81(define-json-mapping <composer-package> make-composer-package composer-package?
82 json->composer-package
83 (description composer-package-description)
84 (homepage composer-package-homepage)
85 (source composer-package-source "source" json->composer-source)
86 (name composer-package-name "name" php-package-name)
87 (version composer-package-version "version" fix-version)
88 (require composer-package-require "require" json->require)
89 (dev-require composer-package-dev-require "require-dev" json->require)
90 (license composer-package-license "license"
91 (lambda (vector)
92 (let ((l (map string->license (vector->list vector))))
93 (if (eq? (length l) 1)
94 (car l)
95 `(list ,@l))))))
96
97(define (valid-version? v)
98 (let ((d (string-downcase v)))
99 (and (not (string-contains d "dev"))
100 (not (string-contains d "beta"))
101 (not (string-contains d "rc")))))
102
103(define* (composer-fetch name #:key (version #f))
104 "Return a composer-package representation of the Composer metadata for the
105package NAME with optional VERSION, or #f on failure."
106 (let* ((url (string-append (%composer-base-url) "/p/" name ".json"))
107 (packages (and=> (json-fetch url)
108 (lambda (pkg)
109 (let ((pkgs (assoc-ref pkg "packages")))
110 (or (assoc-ref pkgs name) pkg))))))
111 (if packages
112 (json->composer-package
113 (if version
114 (assoc-ref packages version)
115 (cdr
116 (reduce
117 (lambda (new cur-max)
118 (match new
119 (((? valid-version? version) . tail)
120 (if (version>? (fix-version version)
121 (fix-version (car cur-max)))
122 (cons* version tail)
123 cur-max))
124 (_ cur-max)))
125 (cons* "0.0.0" #f)
126 packages))))
127 #f)))
128
129(define (php-package-name name)
130 "Given the NAME of a package on Packagist, return a Guix-compliant name for
131the package."
132 (let ((name (string-join (string-split name #\/) "-")))
133 (if (string-prefix? "php-" name)
134 (snake-case name)
135 (string-append "php-" (snake-case name)))))
136
137(define (make-php-sexp composer-package)
138 "Return the `package' s-expression for a PHP package for the given
139COMPOSER-PACKAGE."
140 (let* ((source (composer-package-source composer-package))
141 (dependencies (map php-package-name
142 (composer-package-require composer-package)))
143 (dev-dependencies (map php-package-name
144 (composer-package-dev-require composer-package)))
145 (git? (equal? (composer-source-type source) "git")))
146 ((if git? call-with-temporary-directory call-with-temporary-output-file)
147 (lambda* (temp #:optional port)
148 (and (if git?
149 (begin
150 (mkdir-p temp)
151 (git-fetch (composer-source-url source)
152 (composer-source-reference source)
153 temp))
154 (url-fetch (composer-source-url source) temp))
155 `(package
156 (name ,(composer-package-name composer-package))
157 (version ,(composer-package-version composer-package))
158 (source
159 (origin
160 ,@(if git?
161 `((method git-fetch)
162 (uri (git-reference
163 (url ,(if (string-suffix?
164 ".git"
165 (composer-source-url source))
166 (string-drop-right
167 (composer-source-url source)
168 (string-length ".git"))
169 (composer-source-url source)))
170 (commit ,(composer-source-reference source))))
171 (file-name (git-file-name name version))
172 (sha256
173 (base32
174 ,(bytevector->nix-base32-string
175 (file-hash* temp)))))
176 `((method url-fetch)
177 (uri ,(composer-source-url source))
178 (sha256 (base32 ,(guix-hash-url temp)))))))
179 (build-system composer-build-system)
180 ,@(if (null? dependencies)
181 '()
182 `((inputs
183 (list ,@(map string->symbol dependencies)))))
184 ,@(if (null? dev-dependencies)
185 '()
186 `((native-inputs
187 (list ,@(map string->symbol dev-dependencies)))))
188 (synopsis "")
189 (description ,(composer-package-description composer-package))
190 (home-page ,(composer-package-homepage composer-package))
191 (license ,(or (composer-package-license composer-package)
192 'unknown-license!))))))))
193
194(define composer->guix-package
195 (memoize
196 (lambda* (package-name #:key (version #f) #:allow-other-keys)
197 "Fetch the metadata for PACKAGE-NAME from packagist.org, and return the
198`package' s-expression corresponding to that package and its list of
199dependencies, or #f and the empty list on failure."
200 (let ((package (composer-fetch package-name #:version version)))
201 (if package
202 (let* ((dependencies-names (composer-package-require package))
203 (dev-dependencies-names (composer-package-dev-require package)))
204 (values (make-php-sexp package)
205 (append dependencies-names dev-dependencies-names)))
206 (values #f '()))))))
207
208(define (guix-name->composer-name name)
209 "Given a guix package name, return the name of the package in Packagist."
210 (if (string-prefix? "php-" name)
211 (let ((components (string-split (substring name 4) #\-)))
212 (match components
213 ((namespace name ...)
214 (string-append namespace "/" (string-join name "-")))))
215 name))
216
217(define (guix-package->composer-name package)
218 "Given a Composer PACKAGE built from Packagist, return the name of the
219package in Packagist."
220 (let ((upstream-name (assoc-ref
221 (package-properties package)
222 'upstream-name))
223 (name (package-name package)))
224 (if upstream-name
225 upstream-name
226 (guix-name->composer-name name))))
227
228(define (string->license str)
229 "Convert the string STR into a license object."
230 (or (spdx-string->license str)
231 (match str
232 ("GNU LGPL" 'license:lgpl2.0)
233 ("GPL" 'license:gpl3)
234 ((or "BSD" "BSD License") 'license:bsd-3)
235 ((or "MIT" "MIT license" "Expat license") 'license:expat)
236 ("Public domain" 'license:public-domain)
237 ((or "Apache License, Version 2.0" "Apache 2.0") 'license:asl2.0)
238 (_ 'unknown-license!))))
239
240(define (php-package? package)
241 "Return true if PACKAGE is a PHP package from Packagist."
242 (and
243 (eq? (package-build-system package) composer-build-system)
244 (string-prefix? "php-" (package-name package))))
245
246(define (latest-release package)
247 "Return an <upstream-source> for the latest release of PACKAGE."
248 (let* ((php-name (guix-package->composer-name package))
249 (package (composer-fetch php-name))
250 (version (composer-package-version package))
251 (url (composer-source-url (composer-package-source package))))
252 (upstream-source
253 (package (package-name package))
254 (version version)
255 (urls (list url)))))
256
257(define %composer-updater
258 (upstream-updater
259 (name 'composer)
260 (description "Updater for Composer packages")
261 (pred php-package?)
262 (import latest-release)))
263
264(define* (composer-recursive-import package-name #:optional version)
265 (recursive-import package-name
266 #:version version
267 #:repo->guix-package composer->guix-package
268 #:guix-name php-package-name))
diff --git a/guix/scripts/import.scm b/guix/scripts/import.scm
index 1e8ffd25ec3..d2a1cee56e8 100644
--- a/guix/scripts/import.scm
+++ b/guix/scripts/import.scm
@@ -47,7 +47,7 @@
47 47
48(define importers '("gnu" "pypi" "cpan" "hackage" "stackage" "egg" "elpa" 48(define importers '("gnu" "pypi" "cpan" "hackage" "stackage" "egg" "elpa"
49 "gem" "go" "cran" "crate" "texlive" "json" "opam" 49 "gem" "go" "cran" "crate" "texlive" "json" "opam"
50 "minetest" "elm" "hexpm")) 50 "minetest" "elm" "hexpm" "composer"))
51 51
52(define (resolve-importer name) 52(define (resolve-importer name)
53 (let ((module (resolve-interface 53 (let ((module (resolve-interface
diff --git a/guix/scripts/import/composer.scm b/guix/scripts/import/composer.scm
new file mode 100644
index 00000000000..412bae6318b
--- /dev/null
+++ b/guix/scripts/import/composer.scm
@@ -0,0 +1,107 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2015 David Thompson <davet@gnu.org>
3;;; Copyright © 2018 Oleg Pykhalov <go.wigust@gmail.com>
4;;;
5;;; This file is part of GNU Guix.
6;;;
7;;; GNU Guix is free software; you can redistribute it and/or modify it
8;;; under the terms of the GNU General Public License as published by
9;;; the Free Software Foundation; either version 3 of the License, or (at
10;;; your option) any later version.
11;;;
12;;; GNU Guix is distributed in the hope that it will be useful, but
13;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;;; GNU General Public License for more details.
16;;;
17;;; You should have received a copy of the GNU General Public License
18;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20(define-module (guix scripts import composer)
21 #:use-module (guix ui)
22 #:use-module (guix utils)
23 #:use-module (guix scripts)
24 #:use-module (guix import composer)
25 #:use-module (guix scripts import)
26 #:use-module (srfi srfi-1)
27 #:use-module (srfi srfi-11)
28 #:use-module (srfi srfi-37)
29 #:use-module (srfi srfi-41)
30 #:use-module (ice-9 match)
31 #:use-module (ice-9 format)
32 #:export (guix-import-composer))
33
34
35;;;
36;;; Command-line options.
37;;;
38
39(define %default-options
40 '())
41
42(define (show-help)
43 (display (G_ "Usage: guix import composer PACKAGE-NAME
44Import and convert the Composer package for PACKAGE-NAME.\n"))
45 (display (G_ "
46 -h, --help display this help and exit"))
47 (display (G_ "
48 -V, --version display version information and exit"))
49 (display (G_ "
50 -r, --recursive generate package expressions for all Composer packages\
51 that are not yet in Guix"))
52 (newline)
53 (show-bug-report-information))
54
55(define %options
56 ;; Specification of the command-line options.
57 (cons* (option '(#\h "help") #f #f
58 (lambda args
59 (show-help)
60 (exit 0)))
61 (option '(#\V "version") #f #f
62 (lambda args
63 (show-version-and-exit "guix import composer")))
64 (option '(#\r "recursive") #f #f
65 (lambda (opt name arg result)
66 (alist-cons 'recursive #t result)))
67 %standard-import-options))
68
69
70;;;
71;;; Entry point.
72;;;
73
74(define (guix-import-composer . args)
75 (define (parse-options)
76 ;; Return the alist of option values.
77 (args-fold* args %options
78 (lambda (opt name arg result)
79 (leave (G_ "~A: unrecognized option~%") name))
80 (lambda (arg result)
81 (alist-cons 'argument arg result))
82 %default-options))
83
84 (let* ((opts (parse-options))
85 (args (filter-map (match-lambda
86 (('argument . value)
87 value)
88 (_ #f))
89 (reverse opts))))
90 (match args
91 ((package-name)
92 (if (assoc-ref opts 'recursive)
93 (map (match-lambda
94 ((and ('package ('name name) . rest) pkg)
95 `(define-public ,(string->symbol name)
96 ,pkg))
97 (_ #f))
98 (composer-recursive-import package-name))
99 (let ((sexp (composer->guix-package package-name)))
100 (unless sexp
101 (leave (G_ "failed to download meta-data for package '~a'~%")
102 package-name))
103 sexp)))
104 (()
105 (leave (G_ "too few arguments~%")))
106 ((many ...)
107 (leave (G_ "too many arguments~%"))))))
diff --git a/tests/composer.scm b/tests/composer.scm
new file mode 100644
index 00000000000..9114fef19ed
--- /dev/null
+++ b/tests/composer.scm
@@ -0,0 +1,88 @@
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2020 Julien Lepiller <julien@lepiller.eu>
3;;; Copyright © 2023 Nicolas Graves <ngraves@ngraves.fr>
4;;;
5;;; This file is part of GNU Guix.
6;;;
7;;; GNU Guix is free software; you can redistribute it and/or modify it
8;;; under the terms of the GNU General Public License as published by
9;;; the Free Software Foundation; either version 3 of the License, or (at
10;;; your option) any later version.
11;;;
12;;; GNU Guix is distributed in the hope that it will be useful, but
13;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;;; GNU General Public License for more details.
16;;;
17;;; You should have received a copy of the GNU General Public License
18;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20(define-module (test-composer)
21 #:use-module (guix import composer)
22 #:use-module (guix base32)
23 #:use-module (gcrypt hash)
24 #:use-module (guix tests http)
25 #:use-module (guix grafts)
26 #:use-module (srfi srfi-64)
27 #:use-module (web client)
28 #:use-module (ice-9 match))
29
30;; Globally disable grafts because they can trigger early builds.
31(%graft? #f)
32
33(define test-json
34 "{
35 \"packages\": {
36 \"foo/bar\": {
37 \"0.1\": {
38 \"name\": \"foo/bar\",
39 \"description\": \"description\",
40 \"keywords\": [\"testing\"],
41 \"homepage\": \"http://example.com\",
42 \"version\": \"0.1\",
43 \"license\": [\"BSD-3-Clause\"],
44 \"source\": {
45 \"type\": \"url\",
46 \"url\": \"http://example.com/Bar-0.1.tar.gz\"
47 },
48 \"require\": {},
49 \"require-dev\": {\"phpunit/phpunit\": \"1.0.0\"}
50 }
51 }
52 }
53}")
54
55(define test-source
56 "foobar")
57
58(test-begin "composer")
59
60(test-assert "composer->guix-package"
61 ;; Replace network resources with sample data.
62 (with-http-server `((200 ,test-json)
63 (200 ,test-source))
64 (parameterize ((%composer-base-url (%local-url))
65 (current-http-proxy (%local-url)))
66 (match (composer->guix-package "foo/bar")
67 (`(package
68 (name "php-foo-bar")
69 (version "0.1")
70 (source (origin
71 (method url-fetch)
72 (uri "http://example.com/Bar-0.1.tar.gz")
73 (sha256
74 (base32
75 ,(? string? hash)))))
76 (build-system composer-build-system)
77 (native-inputs (list php-phpunit-phpunit))
78 (synopsis "")
79 (description "description")
80 (home-page "http://example.com")
81 (license license:bsd-3))
82 (string=? (bytevector->nix-base32-string
83 (call-with-input-string test-source port-sha256))
84 hash))
85 (x
86 (pk 'fail x #f))))))
87
88(test-end "composer")