unofficial mirror of guix-devel@gnu.org 
 help / color / mirror / code / Atom feed
From: swedebugia@riseup.net
To: guix-devel <guix-devel@gnu.org>
Subject: Help wanted with recursive npm import returning #f
Date: Tue, 04 Dec 2018 13:43:54 -0800	[thread overview]
Message-ID: <e2f4fb4b43f0f544c2e8833c14992e49@riseup.net> (raw)

[-- Attachment #1: Type: text/plain, Size: 1345 bytes --]

Hi

Introduction
------------
Inspired by Ricardos commit here I rewrote most of the npm importer.

Added memoization, receive, stream->list, values and rewrote the tarball
fetcher to use only npm-uri and tarballs from the registry. Additionally
I implemented handling of scoped packages (e.g. @babel/core).

It contains less lines of code than Jelles importer.

The single import works and is a lot faster and more reliable than
before when fuzzy matching on github was used. See it in action:
http://paste.debian.net/1054384/

Caveats:
1) we don't know if the registry-tarballs are reproducible.
2) filename is the same as the upstream tarball -> we should convert it
to guix-name.
3) we have to download the tarball because sha256 is not among the
hashes computed by npm. (I emailed npm@npmjs.org to ask for them to
compute it for all their tarballs :) )

Help wanted
-----------

There is a bug which only affects the recursive importer. I tried hard
finding it but I'm in way over my head and my guile-foo does not seem to
cut it with this one. :) 

For recursive output it downloads but outputs #f in the end instead of
the sexps. See example output: http://paste.debian.net/1054383/

Trying to debug via the REPL I met this:
scheme@(guile-user) [1]> (npm-recursive-import "async")
$3 = #<stream ? ...>

Any ideas? 

-- 
Cheers 
Swedebugia

[-- Attachment #2: npm.scm --]
[-- Type: text/plain, Size: 12786 bytes --]

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015 David Thompson <davet@gnu.org>
;;; Copyright © 2016 Jelle Licht <jlicht@fsfe.org>
;;; Copyright © 2018 swedebugia <swedebugia@riseup.net>
;;;
;;; 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)
  #:use-module (ice-9 binary-ports)
  #:use-module (ice-9 control)
  #:use-module (ice-9 match)
  #:use-module (ice-9 receive)
  #:use-module (ice-9 regex)
  #:use-module (ice-9 rdelim)
  #:use-module (ice-9 vlist)
  #:use-module (gcrypt hash)
  #:use-module (gnu packages)
  #:use-module (guix base32)
  #:use-module (guix build git)
  #:use-module (guix build-system node)
  #:use-module ((guix download) #:prefix download:)
;;  #:use-module (guix import github)
  #:use-module (guix import json)
  #:use-module (guix import utils)
  #:use-module ((guix licenses) #:select (expat))
  #:use-module (guix packages)
  #:use-module (guix serialization)
  #:use-module (guix upstream)
  #:use-module (guix utils)
  #:use-module (json)
  #:use-module (rnrs bytevectors)
  #:use-module (rnrs io ports)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-11)
  #:use-module (srfi srfi-9 gnu)
  #:use-module (srfi srfi-9)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (web uri)
  #:export (npm->guix-package
            npm-recursive-import
            ;; For debugging in the REPL:
            npm-fetch
            list-requirements
            maybe-inputs))
;;;
;;; Comment:
;;; This is the npm importer. 
;;; Native-inputs are not considered by the importer at this stage because the
;;; code is adapted from the pypi importer and because they are for the most
;;; part not needed to build or use the package.

;;; We should compute and include the native-inputs and include them
;;; commented out. To be able to do this we need something (a new
;;; syntax-rule?) to pass comments in the returned sexp like this:
;;; (native-inputs)
;;; `((
;;; ;;("input" ,input)
;;; ))

;;; We should add a flag to the command line to enable import of
;;; devdependencies aka. native inputs if the user desires.
;;; Perhaps a flag to indicate max levels of recursiveness is also useful to
;;; avoid ending up with 100+ records with one command.

;;;
;;; Code
;;;

(define *REGISTRY*  "https://registry.npmjs.org/")

(define (npm-fetch name)
  "Return metadata from the npm registry for package NAME."
  (json-fetch-alist (string-append *REGISTRY* name)))

(define (npm-tarball alist version)
  "Return the *REGISTRY* tarball url for version VERSION of ALIST"
  (let* ((v (assoc-ref* alist "versions" version))
         (d (assoc-ref* v "dist")))
    (assoc-ref* d "tarball")))

;; TODO use this to check the tarball
(define (npm-tarball-sha512 alist version)
  "Return the *REGISTRY* sha512sum for version VERSION of ALIST or #f if not
found"
  (let* ((v (assoc-ref* alist "versions" version))
         (d (assoc-ref* v "dist")))
    (assoc-ref* d "integrity")))

(define (npm-latest-release alist)
  "Return a string with the latest released version from
ALIST. E.g. '2.1.0'"
  (assoc-ref* alist "dist-tags" "latest"))

(define (npm-package? package)
  "Return true if PACKAGE is an npm package."
  (string-prefix? "node-" (package-name package)))

(define (node->package-name name)
  "Given the NAME of a package on npmjs, return a Guix-compliant name for the
package. We remove the '@' and keep the '/' in scoped
packages. E.g. @mocha/test -> node-mocha/test"
  (cond ((and (string-prefix? "@" name)
              (string-prefix? "node-" name))
         (snake-case (string-drop name 1)))
        ((string-prefix? "@" name)
         (string-append "node-" (snake-case (string-drop name 1))))
        ((string-prefix? "node-" name)
         (snake-case name))
        (else       
         (string-append "node-" (snake-case name)))))

;; (define (blacklisted? name)
;;   "Check if the string name is blacklisted. RETURN #t if yes, else #f."
;;   ;; Split the string to enable ut so blacklist scoped packages like
;;   ;; @babel/core and packages like eslint-popup without having to type in
;;   ;; every single combination.
;;   (if (or
;;        ;; Catch @babel/core
;;        (member (car (string-split name (char-set #\- #\/))) blacklist)
;;        (member (car (string-split name (char-set #\/))) blacklist)
;;        ;; Catch eslint-plugin
;;        (member (car (string-split name (char-set #\-))) blacklist)
;;        (member name blacklist))
;;       #t #f))

(define (extract-npm-dependencies dependencies)
  "Returns a list of dependencies according to the npm naming scheme, from the
npm list of dependencies DEPENDENCIES."
  (if (not dependencies)
      '()
      (map car dependencies)))

;; Needed when adding versioning of package inputs to maybe-inputs.
(define (sanitize-npm-version version)
  "Return version without prefix."
  ;;FIXME sanitize other common prefixes
  (cond ((string-prefix? "^" version)
	 (string-drop version 1))
	((string-prefix? "~" version)
	 (string-drop version 1))
	;; Does this work when version="*"?
	((string-prefix? "*" version)
	 (string-drop version 1))
	((string-ci? "*" version)
	 ;; Return version =""
	 (string-drop version 1))
	(else
	 version)))

(define* (maybe-inputs package-inputs #:optional blacklist native)
  "Given a list of PACKAGE-INPUTS, tries to generate the 'inputs' field of a
package definition. BLACKLIST and NATIVE are booleans and optional."
  ;; TODO add versions to avoid cyclic deps.
  (match package-inputs
    ;; clause1 pat=the empty list
    (()
     ;; body
     '())
    ;; clause2 pat=package-inputs zero or more
    ((package-inputs ...)
     ;;body
     `((inputs
        (,'quasiquote ,package-inputs))))))

(define (list-requirements package-alist)
  "Return a list of dependencies after blacklisting."
  (let* ((name (assoc-ref package-alist "name"))
         (version (npm-latest-release package-alist))
         (curr (assoc-ref* package-alist "versions" version))
         (dependencies (assoc-ref curr "dependencies")))
    ;; Only work with inputs for now.
    (extract-npm-dependencies dependencies)))
     
(define (compute-inputs package-alist)
  "Given the PACKAGE-ALIST of an already downloaded TARBALL, return a list of
name/variable pairs describing the required inputs of this package.  Also
return the unaltered list of upstream dependency names."
  (let ((dependencies
         (remove (cut string=? "argparse" <>)
                 (list-requirements package-alist))))
    (values (sort
             (map (lambda (input)
                    (let ((guix-name (node->package-name input)))
                      (list guix-name (list 'unquote (string->symbol guix-name)))))
                  dependencies)
             (lambda args
               (match args
                 (((a _ ...) (b _ ...))
                  (string-ci<? a b)))))
            dependencies)))

(define (make-npm-sexp name version home-page description
                       dependencies dev-dependencies license
		       source-url package-alist)
  "Return the `package' s-expression for a Node package with the given string NAME,
string VERSION, string HOME-PAGE, string DESCRIPTION, alist DEPENDENCIES,
alist DEV-DEPENDENCIES, list LICENSES and string SOURCE-URL. The alists
contain: npm-name . version"
  (call-with-temporary-output-file
   (lambda (temp port)
     (and (url-fetch source-url temp)
          (receive (input-package-names upstream-dependency-names)
	      (compute-inputs package-alist)
	    (values
             (let ((name (string-downcase name))
                   (guixname (node->package-name name))
                   )
               ;; Name package guix-name-version, e.g. node-async-0.8.0
               `((define-public ,(string->symbol (string-append guixname "-" version))
                   (package
                     (name ,guixname)
                     (version ,version)
                     (source (origin
                               (method url-fetch)
                               (uri (npm-uri ,name version))
                               (sha256
				(base32
				 ,(guix-hash-url temp)))))
                     (build-system node-build-system)
                     ,@(maybe-inputs input-package-names)
                     (synopsis ,description) ; no synopsis field in package.json files
                     (description ,description)
                     (home-page ,home-page)
                     (license ,license)))))
             upstream-dependency-names))))))

(define (extract-license package-json)
  (let ((license-entry (assoc-ref package-json "license"))
        (license-legacy (assoc-ref package-json "licenses")))
    (cond
     ((string? license-entry)
      (spdx-string->license license-entry))
     ((list? license-entry)
      (spdx-string->license (assoc-ref license-entry "type")))
     ((string? license-legacy)
      (spdx-string->license license-legacy))
     ((and license-legacy (positive? (length license-legacy)))
      `(list ,@(map
                (lambda (l) (spdx-string->license (assoc-ref l "type")))
                license-legacy)))
     (else
      #f))))

(define npm->guix-package
  (memoize
   (lambda* (package-name)
     "Fetch the metadata for PACKAGE-NAME from registry.npmjs.com and return the
 `package' s-expression corresponding to that package, or #f on failure."
     (let ((package (npm-fetch package-name)))
       (and package
            ;; TODO catch errors here and leave with error message.
            (let* ((name (assoc-ref package "name"))
                   (version (npm-latest-release package))
                   (curr (assoc-ref* package "versions" version))
                   (dependencies (assoc-ref curr "dependencies"))
                   (dev-dependencies (assoc-ref curr "devDependencies"))
                   (description (assoc-ref package "description"))
                   (home-page (assoc-ref package "homepage"))
                   (license (extract-license curr))
                   (source-url (npm-tarball package version)))
              (make-npm-sexp name version home-page description
                             dependencies dev-dependencies license source-url
                             ;; Pass the whole alist on to compute-inputs from
                             ;; it in the next step.
                             package)))))))

(define (npm-recursive-import package-name)
  (recursive-import package-name #f
                    #:repo->guix-package (lambda (name repo)
                                           (npm->guix-package name))
                    #:guix-name node->package-name))

(define (guix-package->npm-name package)
  "Given a npm PACKAGE return the name of the package on PyPI."
  ;; TODO - needed for the updater

  ;; Inspiration from pypi
  ;; (define (url->pypi-name url)
  ;;   (hyphen-package-name->name+version
  ;;    (basename (file-sans-extension url))))

  ;; (match (and=> (package-source package) origin-uri)
  ;;   ((? string? url)
  ;;    (url->pypi-name url))
  ;;   ((lst ...)
  ;;    (any url->pypi-name lst))
  ;;   (#f #f))

  ;; From Jelle
  ;; (define (package->upstream-name package)
  ;;   "Return the upstream name of the PACKAGE."
  ;;   (let* ((properties (package-properties package))
  ;;          (upstream-name (and=> properties
  ;;                                (cut assoc-ref <> 'upstream-name))))
  ;;     (if upstream-name
  ;;         upstream-name
  ;;         #f))) ;; TODO: Use proper heuristics with package name and what-not
  )


(define (latest-release package)
  "Return an <upstream-source> for the latest release of PACKAGE."

  (define upstream-name
    (package-name package))

  (define meta
    (npm-fetch upstream-name))

  (and meta
       (let ((version (npm-latest-release meta)))
         (upstream-source
          (package (package-name package))
          (version version)
          (urls (npm-tarball meta version))))))

(define %npm-updater
  (upstream-updater
   (name 'npm)
   (description "Updater for Node Package Manager packages")
   (pred npm-package?)
   (latest latest-release)))

             reply	other threads:[~2018-12-04 21:44 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-12-04 21:43 swedebugia [this message]
2018-12-04 22:07 ` Help wanted with recursive npm import returning #f Jelle Licht
2018-12-04 22:11   ` Jelle Licht
2018-12-04 23:34   ` swedebugia

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://guix.gnu.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=e2f4fb4b43f0f544c2e8833c14992e49@riseup.net \
    --to=swedebugia@riseup.net \
    --cc=guix-devel@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/guix.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).