diff options
| -rw-r--r-- | Makefile.am | 3 | ||||
| -rw-r--r-- | doc/guix.texi | 33 | ||||
| -rw-r--r-- | guix/import/npm-binary.scm | 279 | ||||
| -rw-r--r-- | guix/scripts/import.scm | 2 | ||||
| -rw-r--r-- | guix/scripts/import/npm-binary.scm | 121 | ||||
| -rwxr-xr-x | tests/npm-binary.scm | 146 | 
6 files changed, 583 insertions, 1 deletions
| diff --git a/Makefile.am b/Makefile.am index 77c05ff63b..c93379e718 100644 --- a/Makefile.am +++ b/Makefile.am @@ -306,6 +306,7 @@ MODULES =					\    guix/import/kde.scm				\    guix/import/launchpad.scm   			\    guix/import/minetest.scm   			\ +  guix/import/npm-binary.scm			\    guix/import/opam.scm				\    guix/import/print.scm				\    guix/import/pypi.scm				\ @@ -360,6 +361,7 @@ MODULES =					\    guix/scripts/import/hexpm.scm			\    guix/scripts/import/json.scm  		\    guix/scripts/import/minetest.scm  		\ +  guix/scripts/import/npm-binary.scm		\    guix/scripts/import/opam.scm			\    guix/scripts/import/pypi.scm			\    guix/scripts/import/stackage.scm		\ @@ -554,6 +556,7 @@ SCM_TESTS =					\    tests/modules.scm				\    tests/monads.scm				\    tests/nar.scm				\ +  tests/npm-binary.scm				\    tests/networking.scm				\    tests/opam.scm				\    tests/openpgp.scm				\ diff --git a/doc/guix.texi b/doc/guix.texi index 0c22011161..8073e3f6d4 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -14433,6 +14433,39 @@ and generate package expressions for all those packages that are not yet  in Guix.  @end table +@item npm-binary +@cindex npm +@cindex Node.js +Import metadata from the @uref{https://registry.npmjs.org, npm +Registry}, as in this example: + +@example +guix import npm-binary buffer-crc32 +@end example + +The npm-binary importer also allows you to specify a version string: + +@example +guix import npm-binary buffer-crc32@@1.0.0 +@end example + +@quotation Note +Generated package expressions skip the build step of the +@code{node-build-system}. As such, generated package expressions often +refer to transpiled or generated files, instead of being built from +source. +@end quotation + +Additional options include: + +@table @code +@item --recursive +@itemx -r +Traverse the dependency graph of the given upstream package recursively +and generate package expressions for all those packages that are not yet +in Guix. +@end table +  @item opam  @cindex OPAM  @cindex OCaml diff --git a/guix/import/npm-binary.scm b/guix/import/npm-binary.scm new file mode 100644 index 0000000000..6dfedc4910 --- /dev/null +++ b/guix/import/npm-binary.scm @@ -0,0 +1,279 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2019, 2020 Timothy Sample <samplet@ngyro.com> +;;; Copyright © 2021 Lars-Dominik Braun <lars@6xq.net> +;;; Copyright © 2020, 2023, 2024 Jelle Licht <jlicht@fsfe.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 import npm-binary) +  #:use-module ((gnu services configuration) #:select (alist?)) +  #:use-module (gcrypt hash) +  #:use-module (gnu packages) +  #:use-module (guix base32) +  #:use-module (guix http-client) +  #:use-module (guix import json) +  #:use-module (guix import utils) +  #:use-module (guix memoization) +  #:use-module (guix utils) +  #:use-module (ice-9 match) +  #:use-module (ice-9 receive) +  #:use-module (ice-9 regex) +  #:use-module (json) +  #:use-module (srfi srfi-1) +  #:use-module (srfi srfi-26) +  #:use-module (srfi srfi-41) +  #:use-module (srfi srfi-9) +  #:use-module (web client) +  #:use-module (web response) +  #:use-module (web uri) +  #:export (npm-binary-recursive-import +            npm-binary->guix-package +            %npm-registry +            make-versioned-package +            name+version->symbol)) + +;; Autoload Guile-Semver so we only have a soft dependency. +(module-autoload! (current-module) +		  '(semver) +                  '(string->semver semver? semver->string semver=? semver>?)) +(module-autoload! (current-module) +		  '(semver ranges) +                  '(*semver-range-any* string->semver-range semver-range-contains?)) + +;; Dist-tags +(define-json-mapping <dist-tags> make-dist-tags dist-tags? +  json->dist-tags +  (latest dist-tags-latest "latest" string->semver)) + +(define-record-type <versioned-package> +  (make-versioned-package name version) +  versioned-package? +  (name  versioned-package-name)       ;string +  (version versioned-package-version)) ;string + +(define (dependencies->versioned-packages entries) +  (match entries +    (((names . versions) ...) +     (map make-versioned-package names versions)) +    (_ '()))) + +(define (extract-license license-string) +  (if (unspecified? license-string) +      'unspecified! +      (spdx-string->license license-string))) + +(define-json-mapping <dist> make-dist dist? +  json->dist +  (tarball dist-tarball)) + +(define (empty-or-string s) +  (if (string? s) s "")) + +(define-json-mapping <package-revision> make-package-revision package-revision? +  json->package-revision +  (name package-revision-name) +  (version package-revision-version "version"           ;semver +           string->semver) +  (home-page package-revision-home-page "homepage")     ;string +  (dependencies package-revision-dependencies           ;list of versioned-package +                "dependencies" +                dependencies->versioned-packages) +  (dev-dependencies package-revision-dev-dependencies   ;list of versioned-package +                    "devDependencies" dependencies->versioned-packages) +  (peer-dependencies package-revision-peer-dependencies ;list of versioned-package +                    "peerDependencies" dependencies->versioned-packages) +  (license package-revision-license "license"           ;license | #f +           (match-lambda +             ((? unspecified?) #f) +             ((? string? str) (spdx-string->license str)) +             ((? alist? alist) +              (match (assoc "type" alist) +                ((_ . (? string? type)) +                 (spdx-string->license type)) +                (_ #f))))) +  (description package-revision-description             ;string +               "description" empty-or-string) +  (dist package-revision-dist "dist" json->dist))       ;dist + +(define (versions->package-revisions versions) +  (match versions +    (((version . package-spec) ...) +     (map json->package-revision package-spec)) +    (_ '()))) + +(define (versions->package-versions versions) +  (match versions +    (((version . package-spec) ...) +     (map string->semver versions)) +    (_ '()))) + +(define-json-mapping <meta-package> make-meta-package meta-package? +  json->meta-package +  (name meta-package-name)                                       ;string +  (description meta-package-description)                         ;string +  (dist-tags meta-package-dist-tags "dist-tags" json->dist-tags) ;dist-tags +  (revisions meta-package-revisions "versions" versions->package-revisions)) + +(define %npm-registry +  (make-parameter "https://registry.npmjs.org")) +(define %default-page "https://www.npmjs.com/package") + +(define (lookup-meta-package name) +  (let ((json (json-fetch (string-append (%npm-registry) "/" (uri-encode name))))) +    (and=> json json->meta-package))) + +(define lookup-meta-package* (memoize lookup-meta-package)) + +(define (meta-package-versions meta) +  (map package-revision-version +       (meta-package-revisions meta))) + +(define (meta-package-latest meta) +  (and=> (meta-package-dist-tags meta) dist-tags-latest)) + +(define* (meta-package-package meta #:optional +                               (version (meta-package-latest meta))) +  (match version +    ((? semver?) (find (lambda (revision) +                         (semver=? version (package-revision-version revision))) +                       (meta-package-revisions meta))) +    ((? string?) (meta-package-package meta (string->semver version))) +    (_ #f))) + +(define* (semver-latest svs #:optional (svr *semver-range-any*)) +  (find (cut semver-range-contains? svr <>) +        (sort svs semver>?))) + +(define* (resolve-package name #:optional (svr *semver-range-any*)) +  (let ((meta (lookup-meta-package* name))) +    (and meta +         (let* ((version (semver-latest (or (meta-package-versions meta) '()) svr)) +                (pkg (meta-package-package meta version))) +           pkg)))) + + +;;; +;;; Converting packages +;;; + +(define (hash-url url) +  "Downloads the resource at URL and computes the base32 hash for it." +  (bytevector->nix-base32-string (port-sha256 (http-fetch url)))) + +(define (npm-name->name npm-name) +  "Return a Guix package name for the npm package with name NPM-NAME." +  (define (clean name) +    (string-map (lambda (chr) (if (char=? chr #\/) #\- chr)) +                (string-filter (negate (cut char=? <> #\@)) name))) +  (guix-name "node-" (clean npm-name))) + +(define (name+version->symbol name version) +  (string->symbol (string-append name "-" version))) + +(define (package-revision->symbol package) +  (let* ((npm-name (package-revision-name package)) +         (version (semver->string (package-revision-version package))) +         (name (npm-name->name npm-name))) +    (name+version->symbol name version))) + +(define (npm-package->package-sexp npm-package) +  "Return the `package' s-expression for an NPM-PACKAGE." +  (define resolve-spec +    (match-lambda +      (($ <versioned-package> name version) +       (resolve-package name (string->semver-range version))))) + +  (if (package-revision? npm-package) +      (let ((name (package-revision-name npm-package)) +            (version (package-revision-version npm-package)) +            (home-page (package-revision-home-page npm-package)) +            (dependencies (package-revision-dependencies npm-package)) +            (dev-dependencies (package-revision-dev-dependencies npm-package)) +            (peer-dependencies (package-revision-peer-dependencies npm-package)) +            (license (package-revision-license npm-package)) +            (description (package-revision-description npm-package)) +            (dist (package-revision-dist npm-package))) +        (let* ((name (npm-name->name name)) +               (url (dist-tarball dist)) +               (home-page (if (string? home-page) +                              home-page +                              (string-append %default-page "/" (uri-encode name)))) +               (synopsis description) +               (resolved-deps (map resolve-spec +                                   (append dependencies peer-dependencies))) +               (peer-names (map versioned-package-name peer-dependencies)) +               ;; lset-difference for treating peer-dependencies as dependencies, +               ;; which leads to dependency cycles.  lset-union for treating them as +               ;; (ignored) dev-dependencies, which leads to broken packages. +               (dev-names +                (lset-union string= +                            (map versioned-package-name dev-dependencies) +                            peer-names)) +               (extra-phases +                (match dev-names +                  (() '()) +                  ((dev-names ...) +                   `((add-after 'patch-dependencies 'delete-dev-dependencies +                       (lambda _ +                         (delete-dependencies '(,@(reverse dev-names)))))))))) +          (values +           `(package +              (name ,name) +              (version ,(semver->string (package-revision-version npm-package))) +              (source (origin +                        (method url-fetch) +                        (uri ,url) +                        (sha256 (base32 ,(hash-url url))))) +              (build-system node-build-system) +              (arguments +               (list +                #:tests? #f +                #:phases +                #~(modify-phases %standard-phases +                    (delete 'build) +                    ,@extra-phases))) +              ,@(match dependencies +                  (() '()) +                  ((dependencies ...) +                   `((inputs +                      (list ,@(map package-revision->symbol resolved-deps)))))) +              (home-page ,home-page) +              (synopsis ,synopsis) +              (description ,description) +              (license ,license)) +           (map (match-lambda (($ <package-revision> name version) +                               (list name (semver->string version)))) +                resolved-deps)))) +      (values #f '()))) + + +;;; +;;; Interface +;;; + +(define npm-binary->guix-package +  (lambda* (name #:key (version *semver-range-any*) #:allow-other-keys) +    (let* ((svr (match version +                  ((? string?) (string->semver-range version)) +                  (_ version))) +           (pkg (resolve-package name svr))) +      (npm-package->package-sexp pkg)))) + +(define* (npm-binary-recursive-import package-name #:key version) +  (recursive-import package-name +                    #:repo->guix-package (memoize npm-binary->guix-package) +                    #:version version +                    #:guix-name npm-name->name)) diff --git a/guix/scripts/import.scm b/guix/scripts/import.scm index 1f34cab088..d724f2bca3 100644 --- a/guix/scripts/import.scm +++ b/guix/scripts/import.scm @@ -49,7 +49,7 @@  (define importers '("gnu" "pypi" "cpan" "hackage" "stackage" "egg" "elpa"                      "gem" "go" "cran" "crate" "texlive" "json" "opam" -                    "minetest" "elm" "hexpm" "composer")) +                    "minetest" "elm" "hexpm" "composer" "npm-binary"))  (define (resolve-importer name)    (let ((module (resolve-interface diff --git a/guix/scripts/import/npm-binary.scm b/guix/scripts/import/npm-binary.scm new file mode 100644 index 0000000000..b2771bc539 --- /dev/null +++ b/guix/scripts/import/npm-binary.scm @@ -0,0 +1,121 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2014 David Thompson <davet@gnu.org> +;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net> +;;; Copyright © 2019 Timothy Sample <samplet@ngyro.com> +;;; +;;; 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 scripts import npm-binary) +  #:use-module (guix ui) +  #:use-module (guix utils) +  #:use-module (guix scripts) +  #:use-module (guix import npm-binary) +  #:use-module (guix scripts import) +  #:use-module (srfi srfi-1) +  #:use-module (srfi srfi-11) +  #:use-module (srfi srfi-37) +  #:use-module (srfi srfi-41) +  #:use-module (ice-9 match) +  #:use-module (ice-9 format) +  #:export (guix-import-npm-binary)) + + +;;; +;;; Command-line options. +;;; + +(define %default-options +  '()) + +(define (show-help) +  (display (G_ "Usage: guix import npm-binary PACKAGE-NAME [VERSION] +Import and convert the npm package PACKAGE-NAME using the +`node-build-system' (but without building the package from source).")) +  (display (G_ " +  -h, --help             display this help and exit")) +  (display (G_ " +  -r, --recursive        import packages recursively")) +  (display (G_ " +  -V, --version          display version information and exit")) +  (newline) +  (show-bug-report-information)) + +(define %options +  ;; Specification of the command-line options. +  (cons* (option '(#\h "help") #f #f +                 (lambda args +                   (show-help) +                   (exit 0))) +         (option '(#\V "version") #f #f +                 (lambda args +                   (show-version-and-exit "guix import npm-binary"))) +         (option '(#\r "recursive") #f #f +                 (lambda (opt name arg result) +                   (alist-cons 'recursive #t result))) +         %standard-import-options)) + +(define* (package-name->name+version* spec) +  "Given SPEC, a package name like \"@scope/pac@^0.9.1\", return two values: +\"@scope/pac\" and \"^0.9.1\".  When the version part is unavailable, SPEC and \"*\" +are returned.  The first part may start with '@', the latter part must not contain +contain '@'." +  (match (string-rindex spec #\@) +    (#f  (values spec "*")) +    (0  (values spec "*")) +    (idx (values (substring spec 0 idx) +                 (substring spec (1+ idx)))))) + + +;;; +;;; Entry point. +;;; + +(define (guix-import-npm-binary . args) +  (define (parse-options) +    ;; Return the alist of option values. +    (parse-command-line args %options (list %default-options) +                        #:build-options? #f)) + +  (let* ((opts (parse-options)) +         (args (filter-map (match-lambda +                             (('argument . value) +                              value) +                             (_ #f)) +                           (reverse opts)))) +    (match args +      ((spec) +       (define-values (package-name version) +         (package-name->name+version* spec)) +       (match (if (assoc-ref opts 'recursive) +                  ;; Recursive import +                  (npm-binary-recursive-import package-name #:version version) +                  ;; Single import +                  (npm-binary->guix-package package-name #:version version)) +         ((or #f '()) +          (leave (G_ "failed to download meta-data for package '~a@~a'~%") +                 package-name version)) +         (('package etc ...) `(package ,@etc)) +         ((? list? sexps) +          (map (match-lambda +                 ((and ('package ('name name) ('version version) . rest) pkg) +                  `(define-public ,(name+version->symbol name version) +                     ,pkg)) +                 (_ #f)) +               sexps)))) +      (() +       (leave (G_ "too few arguments~%"))) +      ((many ...) +       (leave (G_ "too many arguments~%")))))) diff --git a/tests/npm-binary.scm b/tests/npm-binary.scm new file mode 100755 index 0000000000..cf85e572b3 --- /dev/null +++ b/tests/npm-binary.scm @@ -0,0 +1,146 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2024 Jelle Licht <jlicht@fsfe.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 (test-npm-binary) +  #:use-module ((gcrypt hash) +                #:select ((sha256 . gcrypt-sha256))) +  #:use-module (guix import npm-binary) +  #:use-module (guix base32) +  #:use-module (guix tests) +  #:use-module (srfi srfi-64) +  #:use-module (ice-9 iconv) +  #:use-module (ice-9 match) +  #:export (run-test)) + +(define foo-json +  "{ +  \"name\": \"foo\", +  \"dist-tags\": { +    \"latest\": \"1.2.3\", +    \"next\": \"2.0.1-beta4\" +  }, +  \"description\": \"General purpose utilities to foo your bars\", +  \"homepage\": \"https://github.com/quartz/foo\", +  \"repository\": \"quartz/foo\", +  \"versions\": { +    \"1.2.3\": { +      \"name\": \"foo\", +      \"description\": \"General purpose utilities to foo your bars\", +      \"version\": \"1.2.3\", +      \"author\": \"Jelle Licht <jlicht@fsfe.org>\", +      \"devDependencies\": { +        \"node-megabuilder\": \"^0.0.2\" +      }, +      \"dependencies\": { +        \"bar\": \"^0.1.0\" +      }, +      \"repository\": { +        \"url\": \"quartz/foo\" +      }, +      \"homepage\": \"https://github.com/quartz/foo\", +      \"license\": \"MIT\", +      \"dist\": { +        \"tarball\": \"https://registry.npmjs.org/foo/-/foo-1.2.3.tgz\" +      } +    } +  } +}") + +(define bar-json +  "{ +  \"name\": \"bar\", +  \"dist-tags\": { +    \"latest\": \"0.1.2\" +  }, +  \"description\": \"Core module in FooBar\", +  \"homepage\": \"https://github.com/quartz/bar\", +  \"repository\": \"quartz/bar\", +  \"versions\": { +    \"0.1.2\": { +      \"name\": \"bar\", +      \"description\": \"Core module in FooBar\", +      \"version\": \"0.1.2\", +      \"author\": \"Jelle Licht <jlicht@fsfe.org>\", +      \"repository\": { +        \"url\": \"quartz/bar\" +      }, +      \"homepage\": \"https://github.com/quartz/bar\", +      \"license\": \"MIT\", +      \"dist\": { +        \"tarball\": \"https://registry.npmjs.org/bar/-/bar-0.1.2.tgz\" +      } +    } +  } +}") + +(define test-source-hash +  "") + +(define test-source +  "Empty file\n") + +(define have-guile-semver? +  (false-if-exception (resolve-interface '(semver)))) + +(test-begin "npm") + +(unless have-guile-semver? (test-skip 1)) +(test-assert "npm-binary->guix-package" +  (mock ((guix http-client) http-fetch +         (lambda* (url #:rest _) +           (match url +             ("https://registry.npmjs.org/foo" +              (values (open-input-string foo-json) +                      (string-length foo-json))) +             ("https://registry.npmjs.org/bar" +              (values (open-input-string bar-json) +                      (string-length bar-json))) +             ("https://registry.npmjs.org/foo/-/foo-1.2.3.tgz" +              (set! test-source-hash +                    (bytevector->nix-base32-string +                     (gcrypt-sha256 (string->bytevector test-source "utf-8")))) +              (values (open-input-string test-source) +                      (string-length test-source)))))) +        (match (npm-binary->guix-package "foo") +          (`(package +              (name "node-foo") +              (version "1.2.3") +              (source (origin +                        (method url-fetch) +                        (uri "https://registry.npmjs.org/foo/-/foo-1.2.3.tgz") +                        (sha256 +                         (base32 +                          ,test-source-hash)))) +              (build-system node-build-system) +              (arguments +               (list #:tests? #f +                     #:phases +                     (gexp (modify-phases %standard-phases +                             (delete 'build) +                             (add-after 'patch-dependencies 'delete-dev-dependencies +                               (lambda _ +                                 (delete-dependencies '("node-megabuilder")))))))) +              (inputs (list node-bar-0.1.2)) +              (home-page "https://github.com/quartz/foo") +              (synopsis "General purpose utilities to foo your bars") +              (description "General purpose utilities to foo your bars") +              (license license:expat)) +           #t) +          (x +           (pk 'fail x #f))))) + +(test-end "npm") | 
