Subject: [bug#54796] [PATCH v4 02/22] build-system: Add 'rebar3-build-system'.

From: Hartmut Goebel <[email-masked]>
Date: Sun, 10 Apr 2022 20:57:00 +0200

* guix/build-system/rebar3.scm, guix/build/rebar3-build-system.scm: New files.
* Makefile.am (MODULES): Add them.
---
 Makefile.am                        |   2 +
 doc/guix.texi                      |  41 ++++++++
 guix/build-system/rebar3.scm       | 143 ++++++++++++++++++++++++++++
 guix/build/rebar3-build-system.scm | 147 +++++++++++++++++++++++++++++
 4 files changed, 333 insertions(+)
 create mode 100644 guix/build-system/rebar3.scm
 create mode 100644 guix/build/rebar3-build-system.scm

diff --git a/Makefile.am b/Makefile.am
index 46481dac2c..e2321d1098 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -164,6 +164,7 @@ MODULES =					\
   guix/build-system/waf.scm			\
   guix/build-system/r.scm			\
   guix/build-system/rakudo.scm			\
+  guix/build-system/rebar3.scm			\
   guix/build-system/ruby.scm			\
   guix/build-system/scons.scm			\
   guix/build-system/texlive.scm			\
@@ -217,6 +218,7 @@ MODULES =					\
   guix/build/r-build-system.scm			\
   guix/build/renpy-build-system.scm			\
   guix/build/rakudo-build-system.scm		\
+  guix/build/rebar3-build-system.scm		\
   guix/build/ruby-build-system.scm		\
   guix/build/scons-build-system.scm		\
   guix/build/texlive-build-system.scm		\
diff --git a/doc/guix.texi b/doc/guix.texi
index 6c17b26d70..518fabf48d 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -8717,6 +8717,47 @@ with @code{#:zef} or removed by passing @code{#f} to the
 @code{with-zef?} parameter.
 @end defvr
 
+@defvr {Scheme Variable} rebar3-build-system
+This variable is exported by @code{(guix build-system rebar3)}.  It
+implements a build procedure around @code{rebar3}.
+
+It adds both @code{rebar3} and the @code{erlang} to the set of inputs.
+Different packages can be specified with the @code{#:rebar} and
+@code{#:erlang} parameters, respectively.
+
+This build system is based on @code{gnu-build-system}, but with the
+following phases changed:
+
+@table @code
+
+@item unpack
+This phase, after unpacking the source like the @code{gnu-build-system}
+does, checks for a file @code{contents.tar.gz} at the top-level of the
+source.  If this file exists, it will be unpacked, too.  This eases
+handling of package hosted by @i{hex.pm}.
+
+@item bootstrap
+@item configure
+There are no @code{bootstrap} and @code{configure} phase because erlang
+packages typically don’t need to be configured.
+
+@item build
+This phase runs @code{rebar3 compile}
+with the flags listed in @code{#:rebar-flags}.
+
+@item check
+Unless @code{#:tests? #f} is passed,
+this phase runs @code{rebar3 eunit},
+or some other target specified with @code{#:test-target},
+with the flags listed in @code{#:rebar-flags},
+
+@item install
+This installs the files created in the @i{default} profile, or some
+other profile specified with @code{#:install-profile}.
+
+@end table
+@end defvr
+
 @defvr {Scheme Variable} texlive-build-system
 This variable is exported by @code{(guix build-system texlive)}.  It is
 used to build TeX packages in batch mode with a specified engine.  The
diff --git a/guix/build-system/rebar3.scm b/guix/build-system/rebar3.scm
new file mode 100644
index 0000000000..753dcf0989
--- /dev/null
+++ b/guix/build-system/rebar3.scm
@@ -0,0 +1,143 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2016 Ricardo Wurmus <[email-masked]>
+;;; Copyright © 2020 Hartmut Goebel <[email-masked]>
+;;;
+;;; 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 rebar3)
+  #:use-module (guix store)
+  #:use-module (guix utils)
+  #:use-module (guix gexp)
+  #:use-module (guix packages)
+  #:use-module (guix monads)
+  #:use-module (guix search-paths)
+  #:use-module (guix build-system)
+  #:use-module (guix build-system gnu)
+  #:use-module ((guix hexpm-download) #:select (hexpm-uri) #:prefix dl:)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-26)
+  #:export (hexpm-uri
+            %rebar3-build-system-modules
+            rebar3-build
+            rebar3-build-system))
+
+;;
+;; Standard build procedure for Erlang packages using Rebar3.
+;;
+
+(define hexpm-uri dl:hexpm-uri)
+
+(define %rebar3-build-system-modules
+  ;; Build-side modules imported by default.
+  `((guix build rebar3-build-system)
+    ,@%gnu-build-system-modules))
+
+(define (default-rebar3)
+  "Return the default Rebar3 package."
+  ;; Lazily resolve the binding to avoid a circular dependency.
+  (let ((erlang-mod (resolve-interface '(gnu packages erlang))))
+    (module-ref erlang-mod 'rebar3)))
+
+(define (default-erlang)
+  "Return the default Erlang package."
+  ;; Lazily resolve the binding to avoid a circular dependency.
+  (let ((erlang-mod (resolve-interface '(gnu packages erlang))))
+    (module-ref erlang-mod 'erlang)))
+
+(define* (lower name
+                #:key source inputs native-inputs outputs system target
+                (rebar (default-rebar3))
+                (erlang (default-erlang))
+                #:allow-other-keys
+                #:rest arguments)
+  "Return a bag for NAME from the given arguments."
+  (define private-keywords
+    '(#:target #:rebar #:erlang #:inputs #:native-inputs))
+
+  (and (not target)                               ;XXX: no cross-compilation
+       (bag
+         (name name)
+         (system system)
+         (host-inputs `(,@(if source
+                              `(("source" ,source))
+                              '())
+                        ,@inputs))
+         (build-inputs `(("rebar" ,rebar)
+                         ("erlang" ,erlang) ;; for escriptize
+                         ,@native-inputs
+                         ;; Keep the standard inputs of 'gnu-build-system'.
+                         ,@(standard-packages)))
+         (outputs outputs)
+         (build rebar3-build)
+         (arguments (strip-keyword-arguments private-keywords arguments)))))
+
+(define* (rebar3-build name inputs
+                       #:key
+                       guile source
+                       (rebar-flags ''("skip_deps=true" "-vv"))
+                       (tests? #t)
+                       (test-target "eunit")
+                       ;; TODO: install-name  ; default: based on guix package name
+                       (install-profile "default")
+                       (phases '(@ (guix build rebar3-build-system)
+                                   %standard-phases))
+                       (outputs '("out"))
+                       (search-paths '())
+                       (native-search-paths '())
+                       (system (%current-system))
+                       (imported-modules %rebar3-build-system-modules)
+                       (modules '((guix build rebar3-build-system)
+                                  (guix build utils))))
+  "Build SOURCE with INPUTS."
+
+  (define builder
+    (with-imported-modules imported-modules
+      #~(begin
+          (use-modules #$@(sexp->gexp modules))
+
+          #$(with-build-variables inputs outputs
+              #~(rebar3-build #:source #+source
+                      #:system #$system
+                      #:name #$name
+                      #:rebar-flags #$rebar-flags
+                      #:tests? #$tests?
+                      #:test-target #$test-target
+                      ;; TODO: #:install-name #$install-name
+                      #:install-profile #$install-profile
+                      #:phases #$(if (pair? phases)
+                                     (sexp->gexp phases)
+                                     phases)
+                      #:outputs %outputs
+                      #:search-paths '#$(sexp->gexp
+                                         (map search-path-specification->sexp
+                                              search-paths))
+                      #:inputs %build-inputs)))))
+
+  (mlet %store-monad ((guile (package->derivation (or guile (default-guile))
+                                                  system #:graft? #f)))
+    ;; Note: Always pass #:graft? #f.  Without it, ALLOWED-REFERENCES &
+    ;; co. would be interpreted as referring to grafted packages.
+    (gexp->derivation name builder
+                      #:system system
+                      #:target #f
+                      #:graft? #f
+                      #:guile-for-build guile)))
+
+(define rebar3-build-system
+  (build-system
+    (name 'rebar3)
+    (description "The standard Rebar3 build system")
+    (lower lower)))
diff --git a/guix/build/rebar3-build-system.scm b/guix/build/rebar3-build-system.scm
new file mode 100644
index 0000000000..ae18538ed3
--- /dev/null
+++ b/guix/build/rebar3-build-system.scm
@@ -0,0 +1,147 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2016, 2018 Ricardo Wurmus <[email-masked]>
+;;; Copyright © 2019 Björn Höfling <[email-masked]>
+;;; Copyright © 2020, 2022 Hartmut Goebel <[email-masked]>
+;;;
+;;; 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 rebar3-build-system)
+  #:use-module ((guix build gnu-build-system) #:prefix gnu:)
+  #:use-module ((guix build utils) #:hide (delete))
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 ftw)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:export (rebar3-build
+            %standard-phases))
+
+;;
+;; Builder-side code of the standard build procedure for Erlang packages using
+;; rebar3.
+;;
+;; TODO: Think about whether bindir ("ebin"), libdir ("priv") and includedir
+;; "(include") need to be configurable
+
+(define %erlang-libdir "/lib/erlang/lib")
+
+(define* (erlang-depends #:key inputs #:allow-other-keys)
+  (define input-directories
+    (match inputs
+      (((_ . dir) ...)
+       dir)))
+  (mkdir-p "_checkouts")
+
+  (for-each
+   (lambda (input-dir)
+     (let ((elibdir (string-append input-dir %erlang-libdir)))
+       (when (directory-exists? elibdir)
+         (for-each
+          (lambda (dirname)
+            (let ((dest (string-append elibdir "/" dirname))
+                  (link (string-append "_checkouts/" dirname)))
+              (when (not (file-exists? link))
+                ;; RETHINK: Maybe better copy and make writable to avoid some
+                ;; error messages e.g. when using with rebar3-git-vsn.
+                (symlink dest link))))
+          (list-directories elibdir)))))
+   input-directories))
+
+(define* (unpack #:key source #:allow-other-keys)
+  "Unpack SOURCE in the working directory, and change directory within the
+source.  When SOURCE is a directory, copy it in a sub-directory of the current
+working directory."
+  (let ((gnu-unpack (assoc-ref gnu:%standard-phases 'unpack)))
+    (gnu-unpack #:source source)
+    ;; Packages from hex.pm typically have a contents.tar.gz containing the
+    ;; actual source. If this tar file exists, extract it.
+    (when (file-exists? "contents.tar.gz")
+      (invoke "tar" "xvf" "contents.tar.gz"))))
+
+(define* (build #:key (rebar-flags '()) #:allow-other-keys)
+  (apply invoke `("rebar3" "compile" ,@rebar-flags)))
+
+(define* (check #:key target (rebar-flags '()) (tests? (not target))
+                (test-target "eunit")
+                #:allow-other-keys)
+  (if tests?
+      (apply invoke `("rebar3" ,test-target ,@rebar-flags))
+      (format #t "test suite not run~%")))
+
+(define (erlang-package? name)
+  "Check if NAME correspond to the name of an Erlang package."
+  (string-prefix? "erlang-" name))
+
+(define (package-name-version->erlang-name name+ver)
+  "Convert the Guix package NAME-VER to the corresponding Erlang name-version
+format.  Essentially drop the prefix used in Guix and replace dashes by
+underscores."
+  (let* ((name- (package-name->name+version name+ver)))
+    (string-join
+     (string-split
+      (if (erlang-package? name-)  ; checks for "erlang-" prefix
+          (string-drop name- (string-length "erlang-"))
+          name-)
+      #\-)
+     "_")))
+
+(define (list-directories directory)
+  "Return file names of the sub-directory of DIRECTORY."
+  (scandir directory
+           (lambda (file)
+             (and (not (member file '("." "..")))
+                  (file-is-directory? (string-append directory "/" file))))))
+
+(define* (install #:key name outputs
+                  (install-name (package-name-version->erlang-name name))
+                  (install-profile "default") ; build profile outputs to install
+                  #:allow-other-keys)
+  (let* ((out (assoc-ref outputs "out"))
+         (pkg-dir (string-append out %erlang-libdir "/" install-name)))
+    (let ((bin-dir (string-append "_build/" install-profile "/bin"))
+          (lib-dir (string-append "_build/" install-profile "/lib")))
+      ;; install _build/PROFILE/bin
+      (when (file-exists? bin-dir)
+        (copy-recursively bin-dir out #:follow-symlinks? #t))
+      ;; install _build/PROFILE/lib/*/{ebin,include,priv}
+      (for-each
+       (lambda (*)
+         (for-each
+          (lambda (dirname)
+            (let ((src-dir (string-append lib-dir "/" * "/" dirname))
+                  (dst-dir (string-append pkg-dir "/" dirname)))
+              (when (file-exists? src-dir)
+                (copy-recursively src-dir dst-dir #:follow-symlinks? #t))
+              (false-if-exception
+               (delete-file (string-append dst-dir "/.gitignore")))))
+          '("ebin" "include" "priv")))
+       (list-directories lib-dir))
+      (false-if-exception
+       (delete-file (string-append pkg-dir "/priv/Run-eunit-loop.expect"))))))
+
+(define %standard-phases
+  (modify-phases gnu:%standard-phases
+    (replace 'unpack unpack)
+    (delete 'bootstrap)
+    (delete 'configure)
+    (add-before 'build 'erlang-depends erlang-depends)
+    (replace 'build build)
+    (replace 'check check)
+    (replace 'install install)))
+
+(define* (rebar3-build #:key inputs (phases %standard-phases)
+                       #:allow-other-keys #:rest args)
+  "Build the given Erlang package, applying all of PHASES in order."
+  (apply gnu:gnu-build #:inputs inputs #:phases phases args))
-- 
2.30.2