aefcfdd0ae
* gnu/packages/patches/elm-offline-package-registry.patch: New file. * gnu/local.mk (dist_patch_DATA): Add it. * gnu/packages/elm.scm (elm): Use it. * guix/build-system/elm.scm, guix/build/elm-build-system.scm, tests/elm.scm: New files. * Makefile.scm (MODULES, SCM_TESTS): Add them. * doc/guix.texi (Build Systems): Document 'elm-build-system'. * doc/contributing.texi (Elm Packages): New section. Document naming conventions and utilities. Signed-off-by: Ludovic Courtès <ludo@gnu.org>
381 lines
17 KiB
Scheme
381 lines
17 KiB
Scheme
;;; GNU Guix --- Functional package management for GNU
|
|
;;; Copyright © 2022 Philip McGrath <philip@philipmcgrath.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 build elm-build-system)
|
|
#:use-module ((guix build gnu-build-system) #:prefix gnu:)
|
|
#:use-module (guix build utils)
|
|
#:use-module (guix build json)
|
|
#:use-module (guix build union)
|
|
#:use-module (ice-9 ftw)
|
|
#:use-module (ice-9 rdelim)
|
|
#:use-module (ice-9 regex)
|
|
#:use-module (ice-9 match)
|
|
#:use-module (ice-9 popen)
|
|
#:use-module (ice-9 vlist)
|
|
#:use-module (srfi srfi-1)
|
|
#:use-module (srfi srfi-26)
|
|
#:use-module (srfi srfi-71)
|
|
#:export (%standard-phases
|
|
patch-application-dependencies
|
|
patch-json-string-escapes
|
|
read-offline-registry->vhash
|
|
elm-build))
|
|
|
|
;;; Commentary:
|
|
;;;
|
|
;;; Elm draws a sharp distinction between "projects" with `{"type":"package"}`
|
|
;;; vs. `{"type":"application"}` in the "elm.json" file: see
|
|
;;; <https://github.com/elm/compiler/blob/master/docs/elm.json/package.md> and
|
|
;;; <https://github.com/elm/compiler/blob/master/docs/elm.json/application.md>.
|
|
;;; For now, `elm-build-system` is designed for "package"s: packaging
|
|
;;; "application"s requires ad-hoc replacements for some phases---but see
|
|
;;; `patch-application-dependencies`, which helps to work around a known issue
|
|
;;; discussed below. It would be nice to add more streamlined support for
|
|
;;; "application"s one we have more experience building them in Guix. For
|
|
;;; example, we could incorporate the `uglifyjs` advice from
|
|
;;; <https://github.com/elm/compiler/blob/master/hints/optimize.md>.
|
|
;;;
|
|
;;; We want building an Elm "package" to produce:
|
|
;;;
|
|
;;; - a "docs.json" file with extracted documentation; and
|
|
;;;
|
|
;;; - an "artifacts.dat" file with compilation results for use in building
|
|
;;; "package"s and "application"s.
|
|
;;;
|
|
;;; Unfortunately, there isn't an entry point to the Elm compiler that builds
|
|
;;; those files directly. Building with `elm make` does something different,
|
|
;;; more oriented toward development, testing, and building "application"s.
|
|
;;; We work around this limitation by staging the "package" we're building as
|
|
;;; though it were already installed in ELM_HOME, generating a trivial Elm
|
|
;;; "application" that depends on the "package", and building the
|
|
;;; "application", which causes the files for the "package" to be built.
|
|
;;;
|
|
;;; Much of the ceremony involved is to avoid using `elm` in ways that would
|
|
;;; make it try to do network IO beyond the bare minimum functionality for
|
|
;;; which we've patched a replacement into our `elm`. On the other hand, we
|
|
;;; get to take advantage of the very regular structure required of Elm
|
|
;;; packages.
|
|
;;;
|
|
;;; *Known issue:* Elm itself supports multiple versions of "package"s
|
|
;;; coexisting simultaneously under ELM_HOME, but we do not support this yet.
|
|
;;; Sometimes, parallel versions coexisting causes `elm` to try to write to
|
|
;;; built "artifacts.dat" files. For now, two workarounds are possible:
|
|
;;;
|
|
;;; - Use `patch-application-dependencies` to rewrite an "application"'s
|
|
;;; "elm.json" file to refer to the versions of its inputs actually
|
|
;;; packaged in Guix.
|
|
;;;
|
|
;;; - Use a Guix package transformation to rewrite your "application"'s
|
|
;;; dependencies recursively, so that only one version of each Elm
|
|
;;; "package" is included in your "application"'s build environment.
|
|
;;;
|
|
;;; Patching `elm` more extensively---perhaps adding an `elm guix`
|
|
;;; subcommand`---might let us address these issues more directly.
|
|
;;;
|
|
;;; Code:
|
|
;;;
|
|
|
|
(define %essential-elm-packages
|
|
;; elm/json isn't essential in a fundamental sense,
|
|
;; but it's required for a {"type":"application"},
|
|
;; which we are generating to trigger the build
|
|
'("elm/core" "elm/json"))
|
|
|
|
(define* (target-elm-version #:optional elm)
|
|
"Return the version of ELM or whichever 'elm' is in $PATH.
|
|
Return #false if it cannot be determined."
|
|
(let* ((pipe (open-pipe* OPEN_READ
|
|
(or elm "elm")
|
|
"--version"))
|
|
(line (read-line pipe)))
|
|
(and (zero? (close-pipe pipe))
|
|
(string? line)
|
|
line)))
|
|
|
|
(define* (prepare-elm-home #:key native-inputs inputs #:allow-other-keys)
|
|
"Set the ELM_HOME environment variable and populate the indicated directory
|
|
with the union of the Elm \"package\" inputs. Also, set GUIX_ELM_VERSION to
|
|
the version of the Elm compiler in use."
|
|
(let* ((elm (search-input-file (or native-inputs inputs) "/bin/elm"))
|
|
(elm-version (target-elm-version elm)))
|
|
(setenv "GUIX_ELM_VERSION" elm-version)
|
|
(mkdir "../elm-home")
|
|
(with-directory-excursion "../elm-home"
|
|
(union-build elm-version
|
|
(search-path-as-list
|
|
(list (string-append "share/elm/" elm-version))
|
|
(map cdr inputs))
|
|
#:create-all-directories? #t)
|
|
(setenv "ELM_HOME" (getcwd)))))
|
|
|
|
(define* (stage #:key native-inputs inputs #:allow-other-keys)
|
|
"Extract the installable files from the Elm \"package\" into a staging
|
|
directory and link it into the ELM_HOME tree. Also, set GUIX_ELM_PKG_NAME and
|
|
GUIX_ELM_PKG_VERSION to the name and version, respectively, of the Elm package
|
|
being built, as defined in its \"elm.json\" file."
|
|
(let* ((elm-version (getenv "GUIX_ELM_VERSION"))
|
|
(elm-home (getenv "ELM_HOME"))
|
|
(info (match (call-with-input-file "elm.json" read-json)
|
|
(('@ . alist) alist)))
|
|
(name (assoc-ref info "name"))
|
|
(version (assoc-ref info "version"))
|
|
(rel-dir (string-append elm-version "/packages/" name "/" version))
|
|
(staged-dir (string-append elm-home "/../staged/" rel-dir)))
|
|
(setenv "GUIX_ELM_PKG_NAME" name)
|
|
(setenv "GUIX_ELM_PKG_VERSION" version)
|
|
(mkdir-p staged-dir)
|
|
(mkdir-p (string-append elm-home "/" (dirname rel-dir)))
|
|
(symlink staged-dir
|
|
(string-append elm-home "/" rel-dir))
|
|
(copy-recursively "src" (string-append staged-dir "/src"))
|
|
(install-file "elm.json" staged-dir)
|
|
(install-file "README.md" staged-dir)
|
|
(when (file-exists? "LICENSE")
|
|
(install-file "LICENSE" staged-dir))))
|
|
|
|
(define (patch-json-string-escapes file)
|
|
"Work around a bug in the Elm compiler's JSON parser by attempting to
|
|
replace REVERSE-SOLIDUS--SOLIDUS escape sequences in FILE with unescaped
|
|
SOLIDUS characters."
|
|
;; https://github.com/elm/compiler/issues/2255
|
|
(substitute* file
|
|
(("\\\\/")
|
|
"/")))
|
|
|
|
(define (directory-list dir)
|
|
"Like DIRECTORY-LIST from 'racket/base': lists the contents of DIR, not
|
|
including the special \".\" and \"..\" entries."
|
|
(scandir dir (lambda (f)
|
|
(not (member f '("." ".."))))))
|
|
|
|
(define* (make-offline-registry-file #:key inputs #:allow-other-keys)
|
|
"Generate an \"offline-package-registry.json\" file and set
|
|
GUIX_ELM_OFFLINE_REGISTRY_FILE to its path, cooperating with a patch to `elm`
|
|
to avoid attempting to download a list of all published Elm package names and
|
|
versions from the internet."
|
|
(let* ((elm-home (getenv "ELM_HOME"))
|
|
(elm-version (getenv "GUIX_ELM_VERSION"))
|
|
(registry-file
|
|
(string-append elm-home "/../offline-package-registry.json"))
|
|
(registry-alist
|
|
;; here, we don't need to look up entries, so we build the
|
|
;; alist directly, rather than using a vhash
|
|
(with-directory-excursion
|
|
(string-append elm-home "/" elm-version "/packages")
|
|
(append-map (lambda (org)
|
|
(with-directory-excursion org
|
|
(map (lambda (repo)
|
|
(cons (string-append org "/" repo)
|
|
(directory-list repo)))
|
|
(directory-list "."))))
|
|
(directory-list ".")))))
|
|
(call-with-output-file registry-file
|
|
(lambda (out)
|
|
(write-json `(@ ,@registry-alist) out)))
|
|
(patch-json-string-escapes registry-file)
|
|
(setenv "GUIX_ELM_OFFLINE_REGISTRY_FILE" registry-file)))
|
|
|
|
(define (read-offline-registry->vhash)
|
|
"Return a vhash mapping Elm \"package\" names to lists of available version
|
|
strings."
|
|
(alist->vhash
|
|
(match (call-with-input-file (getenv "GUIX_ELM_OFFLINE_REGISTRY_FILE")
|
|
read-json)
|
|
(('@ . alist) alist))))
|
|
|
|
(define (find-indirect-dependencies registry-vhash root-pkg root-version)
|
|
"Return the recursive dependencies of ROOT-PKG, an Elm \"package\" name, at
|
|
version ROOT-VERSION as an alist mapping Elm \"package\" names to (single)
|
|
versions. The resulting alist will not include entries for
|
|
%ESSENTIAL-ELM-PACKAGES or for ROOT-PKG itself. The REGISTRY-VHASH is used in
|
|
conjunction with the ELM_HOME environment variable to find dependencies."
|
|
(with-directory-excursion
|
|
(string-append (getenv "ELM_HOME")
|
|
"/" (getenv "GUIX_ELM_VERSION")
|
|
"/packages")
|
|
(define (get-dependencies pkg version acc)
|
|
(let* ((elm-json-alist
|
|
(match (call-with-input-file
|
|
(string-append pkg "/" version "/elm.json")
|
|
read-json)
|
|
(('@ . alist) alist)))
|
|
(deps-alist
|
|
(match (assoc-ref elm-json-alist "dependencies")
|
|
(('@ . alist) alist)))
|
|
(deps-names
|
|
(filter-map (match-lambda
|
|
((name . range)
|
|
(and (not (member name %essential-elm-packages))
|
|
name)))
|
|
deps-alist)))
|
|
(fold register-dependency acc deps-names)))
|
|
(define (register-dependency pkg acc)
|
|
;; Using vhash-cons unconditionally would add duplicate entries,
|
|
;; which would then cause problems when we must emit JSON.
|
|
;; Plus, we can avoid needlessly duplicating work.
|
|
(if (vhash-assoc pkg acc)
|
|
acc
|
|
(match (vhash-assoc pkg registry-vhash)
|
|
((_ version . _)
|
|
;; in the rare case that multiple versions are present,
|
|
;; just picking an arbitrary one seems to work well enough for now
|
|
(get-dependencies pkg version (vhash-cons pkg version acc))))))
|
|
(vlist->list
|
|
(get-dependencies root-pkg root-version vlist-null))))
|
|
|
|
(define* (patch-application-dependencies #:key inputs #:allow-other-keys)
|
|
"Rewrites the \"elm.json\" file in the working directory---which must be of
|
|
`\"type\":\"application\"`, not `\"type\":\"package\"`---to refer to the
|
|
dependency versions actually provided via Guix. The
|
|
GUIX_ELM_OFFLINE_REGISTRY_FILE environment variable is used to find available
|
|
versions."
|
|
(let* ((registry-vhash (read-offline-registry->vhash))
|
|
(rewrite-dep-version
|
|
(match-lambda
|
|
((name . _)
|
|
(cons name (match (vhash-assoc name registry-vhash)
|
|
((_ version) ;; no dot
|
|
version))))))
|
|
(rewrite-direct/indirect
|
|
(match-lambda
|
|
;; a little checking to avoid confusing misuse with "package"
|
|
;; project dependencies, which have a different shape
|
|
(((and key (or "direct" "indirect"))
|
|
'@ . alist)
|
|
`(,key @ ,@(map rewrite-dep-version alist)))))
|
|
(rewrite-json-section
|
|
(match-lambda
|
|
(((and key (or "dependencies" "test-dependencies"))
|
|
'@ . alist)
|
|
`(,key @ ,@(map rewrite-direct/indirect alist)))
|
|
((k . v)
|
|
(cons k v))))
|
|
(rewrite-elm-json
|
|
(match-lambda
|
|
(('@ . alist)
|
|
`(@ ,@(map rewrite-json-section alist))))))
|
|
(with-atomic-file-replacement "elm.json"
|
|
(lambda (in out)
|
|
(write-json (rewrite-elm-json (read-json in))
|
|
out)))
|
|
(patch-json-string-escapes "elm.json")))
|
|
|
|
(define* (configure #:key native-inputs inputs #:allow-other-keys)
|
|
"Generate a trivial Elm \"application\" with a direct dependency on the Elm
|
|
\"package\" currently being built."
|
|
(let* ((info (match (call-with-input-file "elm.json" read-json)
|
|
(('@ . alist) alist)))
|
|
(name (getenv "GUIX_ELM_PKG_NAME"))
|
|
(version (getenv "GUIX_ELM_PKG_VERSION"))
|
|
(elm-home (getenv "ELM_HOME"))
|
|
(registry-vhash (read-offline-registry->vhash))
|
|
(app-dir (string-append elm-home "/../fake-app")))
|
|
(mkdir-p (string-append app-dir "/src"))
|
|
(with-directory-excursion app-dir
|
|
(call-with-output-file "elm.json"
|
|
(lambda (out)
|
|
(write-json
|
|
`(@ ("type" . "application")
|
|
("source-directories" "src") ;; intentionally no dot
|
|
("elm-version" . ,(getenv "GUIX_ELM_VERSION"))
|
|
("dependencies"
|
|
@ ("direct"
|
|
@ ,@(map (lambda (pkg)
|
|
(match (vhash-assoc pkg registry-vhash)
|
|
((_ pkg-version . _)
|
|
(cons pkg
|
|
(if (equal? pkg name)
|
|
version
|
|
pkg-version)))))
|
|
(if (member name %essential-elm-packages)
|
|
%essential-elm-packages
|
|
(cons name %essential-elm-packages))))
|
|
("indirect"
|
|
@ ,@(if (member name %essential-elm-packages)
|
|
'()
|
|
(find-indirect-dependencies registry-vhash
|
|
name
|
|
version))))
|
|
("test-dependencies"
|
|
@ ("direct" @)
|
|
("indirect" @)))
|
|
out)))
|
|
(patch-json-string-escapes "elm.json")
|
|
(with-output-to-file "src/Main.elm"
|
|
;; the most trivial possible elm program
|
|
(lambda ()
|
|
(display "module Main exposing (..)
|
|
main : Program () () ()
|
|
main = Platform.worker
|
|
{ init = \\_ -> ( (), Cmd.none )
|
|
, update = \\_ -> \\_ -> ( (), Cmd.none )
|
|
, subscriptions = \\_ -> Sub.none }"))))))
|
|
|
|
(define* (build #:key native-inputs inputs #:allow-other-keys)
|
|
"Run `elm make` to build the Elm \"application\" generated by CONFIGURE."
|
|
(with-directory-excursion (string-append (getenv "ELM_HOME") "/../fake-app")
|
|
(invoke (search-input-file (or native-inputs inputs) "/bin/elm")
|
|
"make"
|
|
"src/Main.elm")))
|
|
|
|
(define* (check #:key tests? #:allow-other-keys)
|
|
"Does nothing, because the `elm-test` executable has not yet been packaged
|
|
for Guix."
|
|
(when tests?
|
|
(display "elm-test has not yet been packaged for Guix\n")))
|
|
|
|
(define* (install #:key outputs #:allow-other-keys)
|
|
"Installs the contents of the directory generated by STAGE, including any
|
|
files added by BUILD, to the Guix package output."
|
|
(copy-recursively
|
|
(string-append (getenv "ELM_HOME") "/../staged")
|
|
(string-append (assoc-ref outputs "out") "/share/elm")))
|
|
|
|
(define* (validate-compiled #:key outputs #:allow-other-keys)
|
|
"Checks that the files \"artifacts.dat\" and \"docs.json\" have been
|
|
installed."
|
|
(let ((base (string-append "/share/elm/"
|
|
(getenv "GUIX_ELM_VERSION")
|
|
"/packages/"
|
|
(getenv "GUIX_ELM_PKG_NAME")
|
|
"/"
|
|
(getenv "GUIX_ELM_PKG_VERSION")))
|
|
(expected '("artifacts.dat" "docs.json")))
|
|
(for-each (lambda (name)
|
|
(search-input-file outputs (string-append base "/" name)))
|
|
expected)))
|
|
|
|
(define %standard-phases
|
|
(modify-phases gnu:%standard-phases
|
|
(add-after 'unpack 'prepare-elm-home prepare-elm-home)
|
|
(delete 'bootstrap)
|
|
(add-after 'patch-source-shebangs 'stage stage)
|
|
(add-after 'stage 'make-offline-registry-file make-offline-registry-file)
|
|
(replace 'configure configure)
|
|
(delete 'patch-generated-file-shebangs)
|
|
(replace 'build build)
|
|
(replace 'check check)
|
|
(replace 'install install)
|
|
(add-before 'validate-documentation-location 'validate-compiled
|
|
validate-compiled)))
|
|
|
|
(define* (elm-build #:key inputs (phases %standard-phases)
|
|
#:allow-other-keys #:rest args)
|
|
"Builds the given Elm project, applying all of the PHASES in order."
|
|
(apply gnu:gnu-build #:inputs inputs #:phases phases args))
|