2016-08-22 23:23:55 -04:00
|
|
|
|
;;; GNU Guix --- Functional package management for GNU
|
|
|
|
|
;;; Copyright © 2015 David Thompson <davet@gnu.org>
|
2021-03-30 01:27:31 -04:00
|
|
|
|
;;; Copyright © 2016, 2020 Jelle Licht <jlicht@fsfe.org>
|
|
|
|
|
;;; Copyright © 2019, 2021 Timothy Sample <samplet@ngyro.com>
|
2022-01-08 03:41:51 -05:00
|
|
|
|
;;; Copyright © 2021, 2022 Philip McGrath <philip@philipmcgrath.com>
|
|
|
|
|
;;; Copyright © 2022 Liliana Marie Prikler <liliana.prikler@gmail.com>
|
2016-08-22 23:23:55 -04:00
|
|
|
|
;;;
|
|
|
|
|
;;; 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 build node-build-system)
|
|
|
|
|
#:use-module ((guix build gnu-build-system) #:prefix gnu:)
|
|
|
|
|
#:use-module (guix build utils)
|
2021-03-30 01:27:31 -04:00
|
|
|
|
#:use-module (guix build json)
|
|
|
|
|
#:use-module (ice-9 ftw)
|
2022-01-08 03:41:53 -05:00
|
|
|
|
#:use-module (ice-9 regex)
|
2016-08-22 23:23:55 -04:00
|
|
|
|
#:use-module (ice-9 match)
|
|
|
|
|
#:use-module (srfi srfi-1)
|
2022-01-08 03:41:51 -05:00
|
|
|
|
#:use-module (srfi srfi-71)
|
2016-08-22 23:23:55 -04:00
|
|
|
|
#:export (%standard-phases
|
2022-01-08 03:41:51 -05:00
|
|
|
|
with-atomic-json-file-replacement
|
2022-01-08 03:41:53 -05:00
|
|
|
|
delete-dependencies
|
2016-08-22 23:23:55 -04:00
|
|
|
|
node-build))
|
|
|
|
|
|
2022-01-08 03:41:51 -05:00
|
|
|
|
(define (with-atomic-json-file-replacement file proc)
|
|
|
|
|
"Like 'with-atomic-file-replacement', but PROC is called with a single
|
|
|
|
|
argument---the result of parsing FILE's contents as json---and should a value
|
|
|
|
|
to be written as json to the replacement FILE."
|
|
|
|
|
(with-atomic-file-replacement file
|
|
|
|
|
(lambda (in out)
|
|
|
|
|
(write-json (proc (read-json in)) out))))
|
|
|
|
|
|
|
|
|
|
(define* (assoc-ref* alist key #:optional default)
|
|
|
|
|
"Like assoc-ref, but return DEFAULT instead of #f if no value exists."
|
|
|
|
|
(match (assoc key alist)
|
|
|
|
|
(#f default)
|
|
|
|
|
((_ . value) value)))
|
|
|
|
|
|
|
|
|
|
(define* (jsobject-ref obj key #:optional default)
|
|
|
|
|
(match obj
|
|
|
|
|
(('@ . alist) (assoc-ref* alist key default))))
|
|
|
|
|
|
|
|
|
|
(define* (alist-pop alist key #:optional (= equal?))
|
|
|
|
|
"Return two values, the first pair in ALIST with key KEY, and the other
|
|
|
|
|
elements. Equality calls are made as (= KEY ALISTCAR)."
|
|
|
|
|
(define (found? pair)
|
|
|
|
|
(= key (car pair)))
|
|
|
|
|
|
|
|
|
|
(let ((before after (break found? alist)))
|
|
|
|
|
(if (pair? after)
|
|
|
|
|
(values (car after) (append before (cdr after)))
|
|
|
|
|
(values #f before))))
|
|
|
|
|
|
|
|
|
|
(define* (alist-update alist key proc #:optional default (= equal?))
|
|
|
|
|
"Return an association list like ALIST, but with KEY mapped to the result of
|
|
|
|
|
PROC applied to the first value found under the comparison (= KEY ALISTCAR).
|
|
|
|
|
If no such value exists, use DEFAULT instead.
|
|
|
|
|
Unlike acons, this removes the previous association of KEY (assuming it is
|
|
|
|
|
unique), but the result may still share storage with ALIST."
|
|
|
|
|
(let ((pair rest (alist-pop alist key =)))
|
|
|
|
|
(acons key
|
|
|
|
|
(proc (if (pair? pair)
|
|
|
|
|
(cdr pair)
|
|
|
|
|
default))
|
|
|
|
|
rest)))
|
|
|
|
|
|
|
|
|
|
(define (jsobject-update* js . updates)
|
|
|
|
|
"Return a json object like JS, but with all UPDATES applied. Each update is
|
|
|
|
|
a list (KEY PROC [DEFAULT]), so that KEY is mapped to the result of PROC
|
|
|
|
|
applied to the value to which KEY is mapped in JS. If no such mapping exists,
|
|
|
|
|
PROC is instead applied to DEFAULT, or to '#f' is no DEFAULT is specified.
|
|
|
|
|
The update takes place from left to right, so later UPDATERs will receive the
|
|
|
|
|
values returned by earlier UPDATERs for the same KEY."
|
|
|
|
|
(match js
|
|
|
|
|
(('@ . alist)
|
|
|
|
|
(let loop ((alist alist)
|
|
|
|
|
(updates updates))
|
|
|
|
|
(match updates
|
|
|
|
|
(() (cons '@ alist))
|
|
|
|
|
(((key proc) . updates)
|
|
|
|
|
(loop (alist-update alist key proc #f equal?) updates))
|
|
|
|
|
(((key proc default) . updates)
|
|
|
|
|
(loop (alist-update alist key proc default equal?) updates)))))))
|
|
|
|
|
|
|
|
|
|
(define (jsobject-union combine seed . objects)
|
|
|
|
|
"Merge OBJECTS into SEED by applying (COMBINE KEY VAL0 VAL), where VAL0
|
|
|
|
|
is the value found in the (possibly updated) SEED and VAL is the new value
|
|
|
|
|
found in one of the OBJECTS."
|
|
|
|
|
(match seed
|
|
|
|
|
(('@ . aseed)
|
|
|
|
|
(match objects
|
|
|
|
|
(() seed)
|
|
|
|
|
((('@ . alists) ...)
|
|
|
|
|
(cons
|
|
|
|
|
'@
|
|
|
|
|
(fold (lambda (alist aseed)
|
|
|
|
|
(if (null? aseed) alist
|
|
|
|
|
(fold
|
|
|
|
|
(match-lambda*
|
|
|
|
|
(((k . v) aseed)
|
|
|
|
|
(let ((pair tail (alist-pop alist k)))
|
|
|
|
|
(match pair
|
|
|
|
|
(#f (acons k v aseed))
|
|
|
|
|
((_ . v0) (acons k (combine k v0 v) aseed))))))
|
|
|
|
|
aseed
|
|
|
|
|
alist)))
|
|
|
|
|
aseed
|
|
|
|
|
alists)))))))
|
|
|
|
|
|
|
|
|
|
;; Possibly useful helper functions:
|
|
|
|
|
;; (define (newest key val0 val) val)
|
|
|
|
|
;; (define (unkeyed->keyed proc) (lambda (_key val0 val) (proc val0 val)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;;
|
|
|
|
|
;;; Phases.
|
|
|
|
|
;;;
|
2016-08-22 23:23:55 -04:00
|
|
|
|
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(define (set-home . _)
|
|
|
|
|
(with-directory-excursion ".."
|
|
|
|
|
(let loop ((i 0))
|
|
|
|
|
(let ((dir (string-append "npm-home-" (number->string i))))
|
|
|
|
|
(if (directory-exists? dir)
|
|
|
|
|
(loop (1+ i))
|
|
|
|
|
(begin
|
|
|
|
|
(mkdir dir)
|
|
|
|
|
(setenv "HOME" (string-append (getcwd) "/" dir))
|
|
|
|
|
(format #t "set HOME to ~s~%" (getenv "HOME")))))))
|
|
|
|
|
#t)
|
2016-08-22 23:23:55 -04:00
|
|
|
|
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(define (module-name module)
|
|
|
|
|
(let* ((package.json (string-append module "/package.json"))
|
|
|
|
|
(package-meta (call-with-input-file package.json read-json)))
|
2022-01-08 03:41:51 -05:00
|
|
|
|
(jsobject-ref package-meta "name")))
|
2021-03-30 01:27:31 -04:00
|
|
|
|
|
|
|
|
|
(define (index-modules input-paths)
|
|
|
|
|
(define (list-modules directory)
|
|
|
|
|
(append-map (lambda (x)
|
|
|
|
|
(if (string-prefix? "@" x)
|
|
|
|
|
(list-modules (string-append directory "/" x))
|
|
|
|
|
(list (string-append directory "/" x))))
|
|
|
|
|
(filter (lambda (x)
|
|
|
|
|
(not (member x '("." ".."))))
|
|
|
|
|
(or (scandir directory) '()))))
|
|
|
|
|
(let ((index (make-hash-table (* 2 (length input-paths)))))
|
|
|
|
|
(for-each (lambda (dir)
|
|
|
|
|
(let ((nm (string-append dir "/lib/node_modules")))
|
|
|
|
|
(for-each (lambda (module)
|
|
|
|
|
(hash-set! index (module-name module) module))
|
|
|
|
|
(list-modules nm))))
|
|
|
|
|
input-paths)
|
|
|
|
|
index))
|
|
|
|
|
|
|
|
|
|
(define* (patch-dependencies #:key inputs #:allow-other-keys)
|
|
|
|
|
|
|
|
|
|
(define index (index-modules (map cdr inputs)))
|
|
|
|
|
|
2022-01-08 03:41:51 -05:00
|
|
|
|
(define resolve-dependencies
|
|
|
|
|
(match-lambda
|
|
|
|
|
(('@ . alist)
|
|
|
|
|
(cons '@ (map (match-lambda
|
|
|
|
|
((key . value)
|
|
|
|
|
(cons key (hash-ref index key value))))
|
|
|
|
|
alist)))))
|
2021-03-30 01:27:31 -04:00
|
|
|
|
|
2022-01-08 03:41:51 -05:00
|
|
|
|
(with-atomic-json-file-replacement "package.json"
|
|
|
|
|
(lambda (pkg-meta)
|
|
|
|
|
(jsobject-update*
|
|
|
|
|
pkg-meta
|
|
|
|
|
`("devDependencies" ,resolve-dependencies (@))
|
|
|
|
|
`("dependencies" ,(lambda (deps)
|
|
|
|
|
(resolve-dependencies
|
|
|
|
|
(jsobject-union
|
|
|
|
|
(lambda (k a b) b)
|
|
|
|
|
(jsobject-ref pkg-meta "peerDependencies" '(@))
|
|
|
|
|
deps)))
|
|
|
|
|
(@)))))
|
2016-08-22 23:23:55 -04:00
|
|
|
|
#t)
|
|
|
|
|
|
2022-01-08 03:41:53 -05:00
|
|
|
|
(define (delete-dependencies absent)
|
|
|
|
|
"Rewrite 'package.json' to allow the build to proceed without packages
|
|
|
|
|
listed in ABSENT, a list of strings naming npm packages.
|
|
|
|
|
|
|
|
|
|
To prevent the deleted dependencies from being reintroduced, use this function
|
|
|
|
|
only after the 'patch-dependencies' phase."
|
|
|
|
|
(define delete-from-jsobject
|
|
|
|
|
(match-lambda
|
|
|
|
|
(('@ . alist)
|
|
|
|
|
(cons '@ (filter (match-lambda
|
|
|
|
|
((k . _)
|
|
|
|
|
(not (member k absent))))
|
|
|
|
|
alist)))))
|
|
|
|
|
|
|
|
|
|
(with-atomic-json-file-replacement "package.json"
|
|
|
|
|
(lambda (pkg-meta)
|
|
|
|
|
(jsobject-update*
|
|
|
|
|
pkg-meta
|
|
|
|
|
`("devDependencies" ,delete-from-jsobject (@))
|
|
|
|
|
`("dependencies" ,delete-from-jsobject (@))))))
|
|
|
|
|
|
2022-01-08 03:41:49 -05:00
|
|
|
|
(define* (delete-lockfiles #:key inputs #:allow-other-keys)
|
|
|
|
|
"Delete 'package-lock.json', 'yarn.lock', and 'npm-shrinkwrap.json', if they
|
|
|
|
|
exist."
|
|
|
|
|
(for-each (lambda (pth)
|
|
|
|
|
(when (file-exists? pth)
|
|
|
|
|
(delete-file pth)))
|
|
|
|
|
'("package-lock.json"
|
|
|
|
|
"yarn.lock"
|
|
|
|
|
"npm-shrinkwrap.json"))
|
|
|
|
|
#t)
|
|
|
|
|
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(define* (configure #:key outputs inputs #:allow-other-keys)
|
|
|
|
|
(let ((npm (string-append (assoc-ref inputs "node") "/bin/npm")))
|
|
|
|
|
(invoke npm "--offline" "--ignore-scripts" "install")
|
2016-08-22 23:23:55 -04:00
|
|
|
|
#t))
|
|
|
|
|
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(define* (build #:key inputs #:allow-other-keys)
|
|
|
|
|
(let ((package-meta (call-with-input-file "package.json" read-json)))
|
2022-01-08 03:41:51 -05:00
|
|
|
|
(if (jsobject-ref (jsobject-ref package-meta "scripts" '(@)) "build" #f)
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(let ((npm (string-append (assoc-ref inputs "node") "/bin/npm")))
|
|
|
|
|
(invoke npm "run" "build"))
|
|
|
|
|
(format #t "there is no build script to run~%"))
|
|
|
|
|
#t))
|
2016-08-22 23:23:55 -04:00
|
|
|
|
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(define* (check #:key tests? inputs #:allow-other-keys)
|
2016-08-22 23:23:55 -04:00
|
|
|
|
"Run 'npm test' if TESTS?"
|
|
|
|
|
(if tests?
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(let ((npm (string-append (assoc-ref inputs "node") "/bin/npm")))
|
|
|
|
|
(invoke npm "test"))
|
|
|
|
|
(format #t "test suite not run~%"))
|
2016-08-22 23:23:55 -04:00
|
|
|
|
#t)
|
|
|
|
|
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(define* (repack #:key inputs #:allow-other-keys)
|
2021-06-17 08:11:19 -04:00
|
|
|
|
(invoke "tar"
|
|
|
|
|
;; Add options suggested by https://reproducible-builds.org/docs/archives/
|
|
|
|
|
"--sort=name"
|
|
|
|
|
(string-append "--mtime=@" (getenv "SOURCE_DATE_EPOCH"))
|
|
|
|
|
"--owner=0"
|
|
|
|
|
"--group=0"
|
|
|
|
|
"--numeric-owner"
|
|
|
|
|
"-czf" "../package.tgz" ".")
|
2021-03-30 01:27:31 -04:00
|
|
|
|
#t)
|
2016-08-22 23:23:55 -04:00
|
|
|
|
|
|
|
|
|
(define* (install #:key outputs inputs #:allow-other-keys)
|
2021-03-30 01:27:31 -04:00
|
|
|
|
"Install the node module to the output store item."
|
|
|
|
|
(let ((out (assoc-ref outputs "out"))
|
|
|
|
|
(npm (string-append (assoc-ref inputs "node") "/bin/npm")))
|
|
|
|
|
(invoke npm "--prefix" out
|
|
|
|
|
"--global"
|
|
|
|
|
"--offline"
|
|
|
|
|
"--loglevel" "info"
|
|
|
|
|
"--production"
|
|
|
|
|
"install" "../package.tgz")
|
2016-08-22 23:23:55 -04:00
|
|
|
|
#t))
|
|
|
|
|
|
2022-01-08 03:41:52 -05:00
|
|
|
|
(define* (avoid-node-gyp-rebuild #:key outputs #:allow-other-keys)
|
|
|
|
|
"Adjust the installed 'package.json' to remove an 'install' script that
|
|
|
|
|
would try to run 'node-gyp rebuild'."
|
|
|
|
|
;; We want to take advantage of `npm install`'s automatic support for
|
|
|
|
|
;; building native addons with node-gyp: in particular, it helps us avoid
|
|
|
|
|
;; hard-coding the specifics of how npm's internal copy of node-gyp is
|
|
|
|
|
;; currently packaged. However, the mechanism by which the automatic support
|
|
|
|
|
;; is implemented causes problems for us.
|
|
|
|
|
;;
|
|
|
|
|
;; If a package contains a 'binding.gyp' file and does not define an
|
|
|
|
|
;; 'install' or 'preinstall' script, 'npm install' runs a default install
|
|
|
|
|
;; script consisting of 'node-gyp rebuild'. In our 'install' phase, this
|
|
|
|
|
;; implicit 'install' script, if it is applicable, is explicitly added to
|
|
|
|
|
;; the "package.json" file. However, if another Guix package were to use a
|
|
|
|
|
;; Node.js package with such an 'install' script, the dependent package's
|
|
|
|
|
;; build process would fail, because 'node-gyp rebuild' would try to write
|
|
|
|
|
;; to the store.
|
|
|
|
|
;;
|
|
|
|
|
;; Here, if the installed "package.json" defines scripts.install as
|
|
|
|
|
;; "node-gyp rebuild", we replace it with a no-op. Importantly, deleting the
|
|
|
|
|
;; install script definition would not be enough, because the default
|
|
|
|
|
;; install script would cause the same problem.
|
|
|
|
|
;;
|
|
|
|
|
;; For further details, see:
|
|
|
|
|
;; - https://docs.npmjs.com/cli/v8/configuring-npm/package-json#default-values
|
|
|
|
|
;; - https://docs.npmjs.com/cli/v8/using-npm/scripts#best-practices
|
|
|
|
|
(define installed-package.json
|
|
|
|
|
(search-input-file outputs (string-append "/lib/node_modules/"
|
|
|
|
|
(module-name ".")
|
|
|
|
|
"/package.json")))
|
|
|
|
|
;; We don't want to use an atomic replacement here, because we often don't
|
|
|
|
|
;; even need to overwrite this file. Therefore, let's use some helpers
|
|
|
|
|
;; that we'd otherwise not need.
|
|
|
|
|
(define pkg-meta
|
|
|
|
|
(call-with-input-file installed-package.json read-json))
|
|
|
|
|
(define scripts
|
|
|
|
|
(jsobject-ref pkg-meta "scripts" '(@)))
|
|
|
|
|
(define (jsobject-set js key val)
|
|
|
|
|
(jsobject-update* js (list key (const val))))
|
|
|
|
|
|
|
|
|
|
(when (equal? "node-gyp rebuild" (jsobject-ref scripts "install" #f))
|
|
|
|
|
(call-with-output-file installed-package.json
|
|
|
|
|
(lambda (out)
|
|
|
|
|
(write-json
|
|
|
|
|
(jsobject-set pkg-meta
|
|
|
|
|
"scripts"
|
|
|
|
|
(jsobject-set scripts
|
|
|
|
|
"install"
|
|
|
|
|
"echo Guix: avoiding node-gyp rebuild"))
|
|
|
|
|
out)))))
|
|
|
|
|
|
2016-08-22 23:23:55 -04:00
|
|
|
|
(define %standard-phases
|
|
|
|
|
(modify-phases gnu:%standard-phases
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(add-after 'unpack 'set-home set-home)
|
|
|
|
|
(add-before 'configure 'patch-dependencies patch-dependencies)
|
2022-01-08 03:41:49 -05:00
|
|
|
|
(add-after 'patch-dependencies 'delete-lockfiles delete-lockfiles)
|
2016-08-22 23:23:55 -04:00
|
|
|
|
(replace 'configure configure)
|
|
|
|
|
(replace 'build build)
|
2021-03-30 01:27:31 -04:00
|
|
|
|
(replace 'check check)
|
|
|
|
|
(add-before 'install 'repack repack)
|
2022-01-08 03:41:52 -05:00
|
|
|
|
(replace 'install install)
|
|
|
|
|
(add-after 'install 'avoid-node-gyp-rebuild avoid-node-gyp-rebuild)))
|
2016-08-22 23:23:55 -04:00
|
|
|
|
|
|
|
|
|
(define* (node-build #:key inputs (phases %standard-phases)
|
|
|
|
|
#:allow-other-keys #:rest args)
|
|
|
|
|
(apply gnu:gnu-build #:inputs inputs #:phases phases args))
|