build-system: Add 'clojure-build-system'.

* guix/build-system/clojure.scm, guix/build/clojure-build-system.scm: New
files.
* guix/build/clojure-utils.scm (@*, @@*): New macros.
(%source-dirs, %test-dirs, %compile-dir, %main-class, %omit-source?,
%aot-include, %aot-exclude, %tests?, %test-include, %test-exclude,
%clojure-regex): New variables.
(package-name->jar-names, canonicalize-relative-path, find-files*,
file-sans-extension, relative-path->clojure-lib-string, find-clojure-libs,
compiled-from?, include-list\exclude-list, eval-with-clojure, create-jar):
New procedures.
* Makefile.am (MODULES): Add them.
* doc/guix.texi (Build Systems): Document 'clojure-build-system'.
This commit is contained in:
Alex Vong 2018-11-19 12:07:44 +01:00 committed by Danny Milosavljevic
parent 53f316ab90
commit df730f6757
No known key found for this signature in database
GPG Key ID: E71A35542C30BAA5
4 changed files with 565 additions and 2 deletions

View File

@ -53,6 +53,7 @@ Copyright @copyright{} 2018 Pierre-Antoine Rouby@*
Copyright @copyright{} 2018 Gábor Boskovits@* Copyright @copyright{} 2018 Gábor Boskovits@*
Copyright @copyright{} 2018 Florian Pelz@* Copyright @copyright{} 2018 Florian Pelz@*
Copyright @copyright{} 2018 Laura Lazzati@* Copyright @copyright{} 2018 Laura Lazzati@*
Copyright @copyright{} 2018 Alex Vong@*
Permission is granted to copy, distribute and/or modify this document Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3 or under the terms of the GNU Free Documentation License, Version 1.3 or
@ -4395,6 +4396,63 @@ The @code{install} phase installs the binaries, and it also installs the
source code and @file{Cargo.toml} file. source code and @file{Cargo.toml} file.
@end defvr @end defvr
@cindex Clojure (programming language)
@cindex simple Clojure build system
@defvr {Scheme Variable} clojure-build-system
This variable is exported by @code{(guix build-system clojure)}. It implements
a simple build procedure for @uref{https://clojure.org/, Clojure} packages
using plain old @code{compile} in Clojure. Cross-compilation is not supported
yet.
It adds @code{clojure}, @code{icedtea} and @code{zip} to the set of inputs.
Different packages can be specified with the @code{#:clojure}, @code{#:jdk} and
@code{#:zip} parameters, respectively.
A list of source directories, test directories and jar names can be specified
with the @code{#:source-dirs}, @code{#:test-dirs} and @code{#:jar-names}
parameters, respectively. Compile directory and main class can be specified
with the @code{#:compile-dir} and @code{#:main-class} parameters, respectively.
Other parameters are documented below.
This build system is an extension of @var{ant-build-system}, but with the
following phases changed:
@table @code
@item build
This phase calls @code{compile} in Clojure to compile source files and runs
@command{jar} to create jars from both source files and compiled files
according to the include list and exclude list specified in
@code{#:aot-include} and @code{#:aot-exclude}, respectively. The exclude list
has priority over the include list. These lists consist of symbols
representing Clojure libraries or the special keyword @code{#:all} representing
all Clojure libraries found under the source directories. The parameter
@code{#:omit-source?} decides if source should be included into the jars.
@item check
This phase runs tests according to the include list and exclude list specified
in @code{#:test-include} and @code{#:test-exclude}, respectively. Their
meanings are analogous to that of @code{#:aot-include} and
@code{#:aot-exclude}, except that the special keyword @code{#:all} now
stands for all Clojure libraries found under the test directories. The
parameter @code{#:tests?} decides if tests should be run.
@item install
This phase installs all jars built previously.
@end table
Apart from the above, this build system also contains an additional phase:
@table @code
@item install-doc
This phase installs all top-level files with base name matching
@var{%doc-regex}. A different regex can be specified with the
@code{#:doc-regex} parameter. All files (recursively) inside the documentation
directories specified in @code{#:doc-dirs} are installed as well.
@end table
@end defvr
@defvr {Scheme Variable} cmake-build-system @defvr {Scheme Variable} cmake-build-system
This variable is exported by @code{(guix build-system cmake)}. It This variable is exported by @code{(guix build-system cmake)}. It
implements the build procedure for packages using the implements the build procedure for packages using the

View File

@ -0,0 +1,195 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Alex Vong <alexvong1995@gmail.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-system clojure)
#:use-module (guix build clojure-utils)
#:use-module (guix build-system)
#:use-module (guix build-system ant)
#:use-module ((guix build-system gnu)
#:select (standard-packages)
#:prefix gnu:)
#:use-module (guix derivations)
#:use-module (guix packages)
#:use-module ((guix search-paths)
#:select
((search-path-specification->sexp . search-path-spec->sexp)))
#:use-module (guix utils)
#:use-module (ice-9 match)
#:export (%clojure-build-system-modules
clojure-build
clojure-build-system))
;; Commentary:
;;
;; Standard build procedure for Clojure packages.
;;
;; Code:
(define-with-docs %clojure-build-system-modules
"Build-side modules imported and used by default."
`((guix build clojure-build-system)
(guix build clojure-utils)
(guix build guile-build-system)
,@%ant-build-system-modules))
(define-with-docs %default-clojure
"The default Clojure package."
(delay (@* (gnu packages lisp) clojure)))
(define-with-docs %default-jdk
"The default JDK package."
(delay (@* (gnu packages java) icedtea)))
(define-with-docs %default-zip
"The default ZIP package."
(delay (@* (gnu packages compression) zip)))
(define* (lower name
#:key
source target
inputs native-inputs
(clojure (force %default-clojure))
(jdk (force %default-jdk))
(zip (force %default-zip))
outputs system
#:allow-other-keys
#:rest arguments)
"Return a bag for NAME."
(let ((private-keywords '(#:source #:target
#:inputs #:native-inputs
#:clojure #:jdk #:zip)))
(if target
(error "No cross-compilation for clojure-build-system yet: LOWER"
target) ; FIXME
(bag (name name)
(system system)
(host-inputs `(,@(if source
`(("source" ,source))
'())
,@inputs
,@(gnu:standard-packages)))
(build-inputs `(("clojure" ,clojure)
("jdk" ,jdk "jdk")
("zip" ,zip)
,@native-inputs))
(outputs outputs)
(build clojure-build)
(arguments (strip-keyword-arguments private-keywords
arguments))))))
(define-with-docs source->output-path
"Convert source input to output path."
(match-lambda
(((? derivation? source))
(derivation->output-path source))
((source)
source)
(source
source)))
(define-with-docs maybe-guile->guile
"Find the right guile."
(match-lambda
((and maybe-guile (? package?))
maybe-guile)
(#f ; default
(@* (gnu packages commencement) guile-final))))
(define* (clojure-build store name inputs
#:key
(source-dirs `',%source-dirs)
(test-dirs `',%test-dirs)
(compile-dir %compile-dir)
(jar-names `',(package-name->jar-names name))
(main-class %main-class)
(omit-source? %omit-source?)
(aot-include `',%aot-include)
(aot-exclude `',%aot-exclude)
doc-dirs ; no sensible default
(doc-regex %doc-regex)
(tests? %tests?)
(test-include `',%test-include)
(test-exclude `',%test-exclude)
(phases '(@ (guix build clojure-build-system)
%standard-phases))
(outputs '("out"))
(search-paths '())
(system (%current-system))
(guile #f)
(imported-modules %clojure-build-system-modules)
(modules %clojure-build-system-modules))
"Build SOURCE with INPUTS."
(let ((builder `(begin
(use-modules ,@modules)
(clojure-build #:name ,name
#:source ,(source->output-path
(assoc-ref inputs "source"))
#:source-dirs ,source-dirs
#:test-dirs ,test-dirs
#:compile-dir ,compile-dir
#:jar-names ,jar-names
#:main-class ,main-class
#:omit-source? ,omit-source?
#:aot-include ,aot-include
#:aot-exclude ,aot-exclude
#:doc-dirs ,doc-dirs
#:doc-regex ,doc-regex
#:tests? ,tests?
#:test-include ,test-include
#:test-exclude ,test-exclude
#:phases ,phases
#:outputs %outputs
#:search-paths ',(map search-path-spec->sexp
search-paths)
#:system ,system
#:inputs %build-inputs)))
(guile-for-build (package-derivation store
(maybe-guile->guile guile)
system
#:graft? #f)))
(build-expression->derivation store name builder
#:inputs inputs
#:system system
#:modules imported-modules
#:outputs outputs
#:guile-for-build guile-for-build)))
(define clojure-build-system
(build-system
(name 'clojure)
(description "Simple Clojure build system using plain old 'compile'")
(lower lower)))
;;; clojure.scm ends here

View File

@ -0,0 +1,110 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Alex Vong <alexvong1995@gmail.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 clojure-build-system)
#:use-module ((guix build ant-build-system)
#:select ((%standard-phases . %standard-phases@ant)
ant-build))
#:use-module (guix build clojure-utils)
#:use-module (guix build java-utils)
#:use-module (guix build utils)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-26)
#:export (%standard-phases
clojure-build))
;; Commentary:
;;
;; Builder-side code of the standard build procedure for Clojure packages.
;;
;; Code:
(define* (build #:key
source-dirs compile-dir
jar-names main-class omit-source?
aot-include aot-exclude
#:allow-other-keys)
"Standard 'build' phase for clojure-build-system."
(let* ((libs (append-map find-clojure-libs source-dirs))
(libs* (include-list\exclude-list aot-include
aot-exclude
#:all-list libs)))
(mkdir-p compile-dir)
(eval-with-clojure `(run! compile ',libs*)
source-dirs)
(let ((source-dir-files-alist (map (lambda (dir)
(cons dir (find-files* dir)))
source-dirs))
;; workaround transitive compilation in Clojure
(classes (filter (lambda (class)
(any (cut compiled-from? class <>)
libs*))
(find-files* compile-dir))))
(for-each (cut create-jar <> (cons (cons compile-dir classes)
(if omit-source?
'()
source-dir-files-alist))
#:main-class main-class)
jar-names)
#t)))
(define* (check #:key
test-dirs
jar-names
tests? test-include test-exclude
#:allow-other-keys)
"Standard 'check' phase for clojure-build-system. Note that TEST-EXCLUDE has
priority over TEST-INCLUDE."
(if tests?
(let* ((libs (append-map find-clojure-libs test-dirs))
(libs* (include-list\exclude-list test-include
test-exclude
#:all-list libs)))
(for-each (lambda (jar)
(eval-with-clojure `(do (apply require
'(clojure.test ,@libs*))
(apply clojure.test/run-tests
',libs*))
(cons jar test-dirs)))
jar-names)))
#t)
(define-with-docs install
"Standard 'install' phase for clojure-build-system."
(install-jars "./"))
(define-with-docs %standard-phases
"Standard build phases for clojure-build-system."
(modify-phases %standard-phases@ant
(replace 'build build)
(replace 'check check)
(replace 'install install)
(add-after 'install-license-files 'install-doc install-doc)))
(define* (clojure-build #:key
inputs
(phases %standard-phases)
#:allow-other-keys
#:rest args)
"Build the given Clojure package, applying all of PHASES in order."
(apply ant-build
#:inputs inputs
#:phases phases
args))
;;; clojure-build-system.scm ends here

View File

@ -19,12 +19,48 @@
(define-module (guix build clojure-utils) (define-module (guix build clojure-utils)
#:use-module (guix build utils) #:use-module (guix build utils)
#:use-module (ice-9 ftw) #:use-module (ice-9 ftw)
#:use-module (ice-9 match)
#:use-module (ice-9 regex) #:use-module (ice-9 regex)
#:use-module (srfi srfi-1) #:use-module (srfi srfi-1)
#:use-module (srfi srfi-8)
#:use-module (srfi srfi-26) #:use-module (srfi srfi-26)
#:export (%clojure-regex #:export (@*
@@*
define-with-docs define-with-docs
install-doc))
%doc-regex
install-doc
%source-dirs
%test-dirs
%compile-dir
package-name->jar-names
%main-class
%omit-source?
%aot-include
%aot-exclude
%tests?
%test-include
%test-exclude
%clojure-regex
canonicalize-relative-path
find-files*
file-sans-extension
relative-path->clojure-lib-string
find-clojure-libs
compiled-from?
include-list\exclude-list
eval-with-clojure
create-jar))
(define-syntax-rule (@* module name)
"Like (@ MODULE NAME), but resolves at run time."
(module-ref (resolve-interface 'module) 'name))
(define-syntax-rule (@@* module name)
"Like (@@ MODULE NAME), but resolves at run time."
(module-ref (resolve-module 'module) 'name))
(define-syntax-rule (define-with-docs name docs val) (define-syntax-rule (define-with-docs name docs val)
"Create top-level variable named NAME with doc string DOCS and value VAL." "Create top-level variable named NAME with doc string DOCS and value VAL."
@ -63,3 +99,167 @@ DOC-REGEX can be compiled or uncompiled."
(for-each (cut copy-recursively <> dest-dir) (for-each (cut copy-recursively <> dest-dir)
doc-dirs) doc-dirs)
#t)) #t))
(define-with-docs %source-dirs
"A default list of source directories."
'("src/"))
(define-with-docs %test-dirs
"A default list of test directories."
'("test/"))
(define-with-docs %compile-dir
"Default directory for holding class files."
"classes/")
(define (package-name->jar-names name)
"Given NAME, a package name like \"foo-0.9.1b\",
return the list of default jar names: (\"foo-0.9.1b.jar\" \"foo.jar\")."
(map (cut string-append <> ".jar")
(list name
(receive (base-name _)
(package-name->name+version name)
base-name))))
(define-with-docs %main-class
"Default name for main class. It should be a symbol or #f."
#f)
(define-with-docs %omit-source?
"Include source in jars by default."
#f)
(define-with-docs %aot-include
"A default list of symbols deciding what to compile. Note that the exclude
list has priority over the include list. The special keyword #:all represents
all libraries found under the source directories."
'(#:all))
(define-with-docs %aot-exclude
"A default list of symbols deciding what not to compile.
See the doc string of '%aot-include' for more details."
'())
(define-with-docs %tests?
"Enable tests by default."
#t)
(define-with-docs %test-include
"A default list of symbols deciding what tests to include. Note that the
exclude list has priority over the include list. The special keyword #:all
represents all tests found under the test directories."
'(#:all))
(define-with-docs %test-exclude
"A default list of symbols deciding what tests to exclude.
See the doc string of '%test-include' for more details."
'())
(define-with-docs %clojure-regex
"Default regex for matching the base name of clojure source files."
"\\.cljc?$")
(define-with-docs canonicalize-relative-path
"Like 'canonicalize-path', but for relative paths.
Canonicalizations requiring the path to exist are omitted."
(let ((remove.. (lambda (ls)
(fold-right (match-lambda*
(((and comp (not "..")) (".." comps ...))
comps)
((comp (comps ...))
(cons comp comps)))
'()
ls))))
(compose (match-lambda
(() ".")
(ls (string-join ls "/")))
remove..
(cut remove (cut member <> '("" ".")) <>)
(cut string-split <> #\/))))
(define (find-files* base-dir . args)
"Similar to 'find-files', but with BASE-DIR stripped and result
canonicalized."
(map canonicalize-relative-path
(with-directory-excursion base-dir
(apply find-files "./" args))))
;;; FIXME: should be moved to (guix build utils)
(define-with-docs file-sans-extension
"Strip extension from path, if any."
(@@ (guix build guile-build-system)
file-sans-extension))
(define (relative-path->clojure-lib-string path)
"Convert PATH to a clojure library string."
(string-map (match-lambda
(#\/ #\.)
(#\_ #\-)
(chr chr))
(file-sans-extension path)))
(define* (find-clojure-libs base-dir
#:key (clojure-regex %clojure-regex))
"Return the list of clojure libraries found under BASE-DIR.
CLOJURE-REGEX can be compiled or uncompiled."
(map (compose string->symbol
relative-path->clojure-lib-string)
(find-files* base-dir clojure-regex)))
(define (compiled-from? class lib)
"Given class file CLASS and clojure library symbol LIB, decide if CLASS
results from compiling LIB."
(string-prefix? (symbol->string lib)
(relative-path->clojure-lib-string class)))
(define* (include-list\exclude-list include-list exclude-list
#:key all-list)
"Given INCLUDE-LIST and EXCLUDE-LIST, replace all occurences of #:all by
slicing ALL-LIST into them and compute their list difference."
(define (replace-#:all ls all-ls)
(append-map (match-lambda
(#:all all-ls)
(x (list x)))
ls))
(let ((include-list* (replace-#:all include-list all-list))
(exclude-list* (replace-#:all exclude-list all-list)))
(lset-difference equal? include-list* exclude-list*)))
(define (eval-with-clojure expr extra-paths)
"Evaluate EXPR with clojure.
EXPR must be a s-expression writable by guile and readable by clojure.
For examples, '(require '[clojure.string]) will not work,
because the guile writer converts brackets to parentheses.
EXTRA-PATHS is a list of paths which will be appended to $CLASSPATH."
(let* ((classpath (getenv "CLASSPATH"))
(classpath* (string-join (cons classpath extra-paths) ":")))
(invoke "java"
"-classpath" classpath*
"clojure.main"
"--eval" (object->string expr))))
(define* (create-jar output-jar dir-files-alist
#:key
(verbose? #t)
(compress? #f)
(main-class %main-class))
"Given DIR-FILES-ALIST, an alist of the form: ((DIR . FILES) ...)
Create jar named OUTPUT-JAR from FILES with DIR stripped."
(let ((grouped-options (string-append "c"
(if verbose? "v" "")
"f"
(if compress? "" "0")
(if main-class "e" ""))))
(apply invoke `("jar"
,grouped-options
,output-jar
,@(if main-class (list (symbol->string main-class)) '())
,@(append-map (match-lambda
((dir . files)
(append-map (lambda (file)
`("-C" ,dir ,file))
files)))
dir-files-alist)))))