diff mbox series

[bug#66801,v3,01/14] build-system: Add mix-build-system.

Message ID 67c324d191a9698aed8d9887260cb0ef2bc031df.1700088189.git.phfrohring@deeplinks.com
State New
Headers show
Series [bug#66801,v3,01/14] build-system: Add mix-build-system. | expand

Commit Message

Pierre-Henry Fröhring Nov. 15, 2023, 10:51 p.m. UTC
* guix/build-system/mix.scm,
* guix/build/mix-build-system.scm: New modules.

Change-Id: I2cbf6c963a530e73420da0eb17ffaf92827451bf
---
 guix/build-system/mix.scm       | 181 ++++++++++++++++++++++++++++
 guix/build/mix-build-system.scm | 205 ++++++++++++++++++++++++++++++++
 2 files changed, 386 insertions(+)
 create mode 100644 guix/build-system/mix.scm
 create mode 100644 guix/build/mix-build-system.scm


base-commit: a0d337e79c87d7c38c79d0291974f490cb137a52

Comments

Liliana Marie Prikler Nov. 16, 2023, 2:05 a.m. UTC | #1
Am Mittwoch, dem 15.11.2023 um 23:51 +0100 schrieb Pierre-Henry
Fröhring:
> * guix/build-system/mix.scm,
> * guix/build/mix-build-system.scm: New modules.
> 
> Change-Id: I2cbf6c963a530e73420da0eb17ffaf92827451bf
> ---
>  guix/build-system/mix.scm       | 181 ++++++++++++++++++++++++++++
>  guix/build/mix-build-system.scm | 205
> ++++++++++++++++++++++++++++++++
>  2 files changed, 386 insertions(+)
>  create mode 100644 guix/build-system/mix.scm
>  create mode 100644 guix/build/mix-build-system.scm
> 
> diff --git a/guix/build-system/mix.scm b/guix/build-system/mix.scm
> new file mode 100644
> index 00000000..ae80679b
> --- /dev/null
> +++ b/guix/build-system/mix.scm
> @@ -0,0 +1,181 @@
> +;;; GNU Guix --- Functional package management for GNU
> +;;; Copyright © 2023 Pierre-Henry Fröhring
> <phfrohring@deeplinks.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/>.
> +
> +;; Commentary:
> +;;
> +;; Standard build procedure for Elixir packages using 'mix'.  This
> is
> +;; implemented as an extension of 'gnu-build-system'.
> +;;
> +;; Code:
> +
> +(define-module (guix build-system mix)
> +  #:use-module (guix build mix-build-system)
> +  #:use-module (guix build-system gnu)
> +  #:use-module (guix build-system)
> +  #:use-module (guix gexp)
> +  #:use-module (guix monads)
> +  #:use-module (guix packages)
> +  #:use-module (guix search-paths)
> +  #:use-module (guix store)
> +  #:use-module (guix utils)
> +  #:use-module (ice-9 match)
> +  #:use-module (srfi srfi-1)
> +  #:use-module (srfi srfi-26)
> +  #:export (mix-build-system hexpm-uri))
> +
> +;; Lazily resolve the bindings to avoid circular dependencies.
> +(define (default-glibc-utf8-locales)
> +  (let* ((base (resolve-interface '(gnu packages base))))
> +    (module-ref base 'glibc-utf8-locales)))
> +
> +(define (default-elixir-hex)
> +  (let ((elixir (resolve-interface '(gnu packages elixir))))
> +    (module-ref elixir 'elixir-hex)))
> +
> +(define (default-rebar3)
> +  (let ((erlang (resolve-interface '(gnu packages erlang))))
> +    (module-ref erlang 'rebar3)))
> +
> +(define (default-elixir)
> +  "Return the default Elixir package."
> +  (let ((elixir (resolve-interface '(gnu packages elixir))))
> +    (module-ref elixir 'elixir)))
> +
> +(define (hexpm-uri name version)
> +  "Return the URI where to fetch the sources of a Hex package NAME
> at VERSION.
> +NAME is the name of the package which should look like: elixir-pkg-
> name-X.Y.Z
> +See: https://github.com/hexpm/specifications/blob/main/endpoints.md"
> +  ((compose
> +    (cut string-append "https://repo.hex.pm/tarballs/" <> "-"
> version ".tar")
> +    (cut string-replace-substring <> "-" "_")
> +    strip-elixir-prefix)
> +   name))
> +
> +;; A number of environment variables specific to the Mix build
> system are
> +;; reflected here.  They are documented at
> +;;
> https://hexdocs.pm/mix/1.15.7/Mix.html#module-environment-variables. 
> Other
> +;; parameters located in mix.exs are defined at
> +;;
> https://hexdocs.pm/mix/1.15.7/Mix.Project.html#module-configuration
> +(define* (mix-build name
> +                    inputs
> +                    #:key
> +                    source
> +                    (tests? #t)
> +                    (mix-path #f) ;See MIX_PATH.
> +                    (mix-exs "mix.exs") ;See MIX_EXS.
> +                    (build-per-environment #t) ;See
> :build_per_environment.
> +                    (phases '%standard-phases)
> +                    (outputs '("out"))
> +                    (search-paths '())
> +                    (system (%current-system))
> +                    (guile #f)
> +                    (imported-modules `((guix build mix-build-
> system)
> +                                        ,@%gnu-build-system-
> modules))
> +                    (modules '((guix build mix-build-system)
> +                               (guix build utils))))
> +  "Build SOURCE using Elixir, and with INPUTS."
> +
> +  ;; Check the documentation of :build_per_environment here:
> +  ;;
> https://hexdocs.pm/mix/1.15.7/Mix.Project.html#module-configuration A
> nd
> +  ;; "Environments" here:
> +  ;; https://hexdocs.pm/mix/1.15.7/Mix.html#module-environments
> +  (define mix-environments
> +    (if build-per-environment
> +        `("prod" ,@(if tests? '("test") '()))
> +        '("shared")))
> +
> +  (define builder
> +    (with-imported-modules imported-modules
> +      #~(begin
> +
> +          (use-modules #$@(sexp->gexp modules))
> +
> +          #$(with-build-variables inputs outputs
> +              #~(mix-build #:name #$name
> +                           #:source #+source
> +                           #:system #$system
> +                           #:tests? #$tests?
> +                           #:mix-path #$mix-path
> +                           #:mix-exs #$mix-exs
> +                           #:mix-environments '#$mix-environments
> +                           #:build-per-environment #$build-per-
> environment
> +                           #: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)))
> +    (gexp->derivation name
> +                      builder
> +                      #:system system
> +                      #:graft? #f       ;consistent with 'gnu-build'
> +                      #:target #f
> +                      #:guile-for-build guile)))
> +
> +(define* (lower name
> +                #:key
> +                (elixir (default-elixir))
> +                (elixir-hex (default-elixir-hex))
> +                (glibc-utf8-locales (default-glibc-utf8-locales))
> +                (inputs '())
> +                (native-inputs '())
> +                (propagated-inputs '())
> +                (rebar3 (default-rebar3))
> +                (tests? #t)
> +                outputs
> +                source
> +                system
> +                target
> +                #:allow-other-keys #:rest arguments)
> +  "Return a bag for NAME."
> +  (let ((private-keywords
> +         '(#:inputs #:native-inputs
> +           #:outputs #:system #:target
> +           #:elixir #:elixir-hex #:glibc-utf8-locales
> +           #:rebar3 #:erlang))
> +        (build-inputs
> +         `(,@(standard-packages)
> +           ("glibc-utf8-locales" ,glibc-utf8-locales)
> +           ("erlang" ,(lookup-package-input elixir "erlang"))
> +           ("rebar3" ,rebar3)
> +           ("elixir" ,elixir)
> +           ("elixir-hex" ,elixir-hex)
> +           ,@inputs
> +           ,@native-inputs)))
> +  (bag (name name)
> +       (system system)
> +       (build-inputs build-inputs)
> +       (host-inputs (if target inputs '()))
> +       (outputs outputs)
> +       (build mix-build)
> +       (arguments (strip-keyword-arguments private-keywords
> arguments)))))
> +
> +(define mix-build-system
> +  (build-system (name 'mix)
> +                (description "The standard Mix build system")
> +                (lower lower)))
> +
> +;;; mix.scm ends here
> diff --git a/guix/build/mix-build-system.scm b/guix/build/mix-build-
> system.scm
> new file mode 100644
> index 00000000..0a1fcb5c
> --- /dev/null
> +++ b/guix/build/mix-build-system.scm
> @@ -0,0 +1,205 @@
> +;;; GNU Guix --- Functional package management for GNU
> +;;; Copyright © 2023 Pierre-Henry Fröhring
> <phfrohring@deeplinks.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/>.
> +
> +;; Commentary:
> +;;
> +;; Code:
> +
> +(define-module (guix build mix-build-system)
> +  #:use-module ((guix build gnu-build-system) #:prefix gnu:)
> +  #:use-module (guix build utils)
> +  #:use-module (ice-9 ftw)
> +  #:use-module (ice-9 match)
> +  #:use-module (ice-9 regex)
> +  #:use-module (ice-9 string-fun)
> +  #:use-module (srfi srfi-1)
> +  #:use-module (srfi srfi-26)
> +  #:export (mix-build
> +            strip-elixir-prefix
> +            %standard-phases))
> +
> +(define (elixir-version elixir)
> +  "Return an X.Y string where X and Y are respectively the major and
> minor version number of ELIXIR.
> +Example: /gnu/store/…-elixir-1.14.0 → 1.14"
> +  ((compose
> +    (cut string-join <> ".")
> +    (cut take <> 2)
> +    (cut string-split <> #\.)
> +    last)
> +   (string-split elixir #\-)))
I don't think we need to be overly cute here.  The let-binding version
from python-build-system is less surprising to the uninitiated reader.

See also strip-store-file-name and package-name->name+version.

> +(define (elixir-libdir elixir path)
> +  "Return the path where all libraries for a specified ELIXIR
> version are installed."
> +  (string-append path "/lib/elixir/" (elixir-version elixir)))
You probably want to swap path and elixir and perhaps also find a way
to implicitly parameterize the latter so as to make it optional. 
> +
> +(define (erlang-libdir path)
> +  "Return the path of the directory where libraries of an Erlang
> package are
> +installed in the store."
> +  (string-append path "/lib/erlang/lib"))
> +
> +(define (install-dir inputs outputs)
> +  "Return the path of the current output's Elixir library.
> +Example: /gnu/store/…/lib/elixir/1.14"
> +  (elixir-libdir (assoc-ref inputs "elixir")
> +                 (assoc-ref outputs "out")))
> +
> +(define (strip-prefix prefix name)
> +  "Return NAME without the prefix PREFIX."
> +  (if (string-prefix? prefix name)
> +      (string-drop name (string-length prefix))
> +      name))
You might want to consider 
  (define* (strip-prefix name #:optional (prefix "elixir-")
     …)

> +(define (mix-build-dir mix-env)
> +  "Return the directory where build artifacts are to be installed
> according to
> +en environment MIX-ENV in the current directory."
> +  (string-append "_build/" mix-env "/lib"))
> +
> +(define* (unpack #:key source mix-path #: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)
> +    (when (file-exists? "contents.tar.gz")
> +      (invoke "tar" "xvf" "contents.tar.gz"))))
> +
> +(define (list-directories dir)
> +  "List absolute paths of directories directly under the directory
> DIR."
> +  (map (cut string-append dir "/" <>)
> +       (scandir dir (lambda (filename)
> +                      (and (not (member filename '("." "..")))
> +                           (directory-exists? (string-append dir "/"
> filename)))))))
> +
> +(define* (configure #:key inputs mix-path mix-exs #:allow-other-
> keys)
> +  "Set environment variables.
> +See:
> https://hexdocs.pm/mix/1.15.7/Mix.html#module-environment-variables"
> +  (setenv "LC_ALL" "en_US.UTF-8")
> +  (setenv "MIX_HOME" (getcwd))
> +  (setenv "MIX_ARCHIVES" "archives")
> +  (setenv "MIX_BUILD_ROOT" "_build")
> +  (setenv "MIX_DEPS_PATH" "deps")
> +  (setenv "MIX_PATH" (or mix-path ""))
> +  (setenv "MIX_REBAR3" (string-append (assoc-ref inputs "rebar3")
> "/bin/rebar3"))
> +  (setenv "MIX_EXS" mix-exs))
This does not appear to be a configure phase in the traditional sense
of the wording.  Instead, it should be a post 'set-paths' 'set-mix-env'
imho.

> +(define* (install-hex #:key name inputs outputs #:allow-other-keys)
> +  "Install Hex."
> +  (let ((hex-archive-path (string-append (getenv "MIX_ARCHIVES")
> "/hex")))
> +    (mkdir-p hex-archive-path)
> +    (symlink (car (list-directories (elixir-libdir (assoc-ref inputs
> "elixir")
> +                                                   (assoc-ref inputs
> "elixir-hex"))))
> +             (string-append hex-archive-path "/hex"))))
Why do we need this?  It looks like we'll be pasting the same (native?)
input all over the store, which imho would be bad.

> +(define* (install-dependencies #:key
> +                               name
> +                               inputs
> +                               outputs
> +                               tests?
> +                               build-per-environment
> +                               (native-inputs '())
> +                               mix-environments
> +                               #:allow-other-keys
> +                               #:rest rest)
> +  "Install dependencies."
> +  (define (install-lib lib dir)
> +    (let ((lib-name (last (string-split lib #\/))))
> +      (symlink lib (string-append dir "/" lib-name))))
> +
> +  (define (install-input mix-env input)
> +    (let ((dir (mix-build-dir mix-env)))
> +      (mkdir-p dir)
> +      (match input
> +        ((_ . path)
> +         ((compose
> +           (cut for-each (cut install-lib <> dir) <>)
> +           (cut append-map list-directories <>)
> +           (cut filter directory-exists? <>))
> +          (list (elixir-libdir (assoc-ref inputs "elixir") path)
> +                (erlang-libdir path)))))))
I think you're at the wrong layer of abstraction here.

  (match input 
    ((_ . prefix)
     (begin 
      (install-subdirectories (elixir-libdir path))
      (install-subdirectories (erlang-libdir path)))))

where (install-subdirectories PATH) is basically
  (when (directory-exists? PATH)
    (for-each (cute install-lib <> (mix-build-dir mix-env))
              (list-directories PATH)))

> +  (define (install-inputs mix-env)
> +    (for-each (cut install-input mix-env <>)
> +              (append inputs native-inputs)))
Installing native inputs: probably a bad idea (inhibits cross-
compilation).

> +  (for-each install-inputs mix-environments))
> +
> +(define* (build #:key mix-environments #:allow-other-keys)
> +  "Builds the Mix project."
> +  (for-each (lambda (mix-env)
> +              (setenv "MIX_ENV" mix-env)
> +              (invoke "mix" "compile" "--no-deps-check"))
> +            mix-environments))
> +
> +(define* (check #:key (tests? #t) #:allow-other-keys)
> +  "Test the Mix project."
> +  (if tests?
> +      (invoke "mix" "test" "--no-deps-check")
> +      (format #t "tests? = ~a~%" tests?)))
> +
> +(define* (remove-mix-dirs . _)
> +  "Remove all .mix/ directories.
> +We do not want to copy them to the installation directory."
> +  (for-each delete-file-recursively
> +            (find-files "." (file-name-predicate "\\.mix$")
> #:directories? #t)))
> +
> +(define (library-name pkg-name)
> +  "Return the library name deduced from PKG-NAME.
> +A package should be named: elixir-lib-name-X.Y.Z from which the
> library name
> +lib_name is deduced."
> +  ((compose
> +    (cut string-join <> "_")
> +    (cut drop-right <> 1)
> +    (cut string-split <> #\-))
> +   (strip-elixir-prefix pkg-name)))
Consider defining (package-name-version->elixir-name) analogous to
(package-name-version->erlang-name) in rebar-build-system.  Also allow
overriding it through a build system argument.  The thing you currently
have will blow up with git-version.

Cheers
Pierre-Henry Fröhring Nov. 16, 2023, 1:01 p.m. UTC | #2
Do you mind if I paste the conversation using the following org format?

* Comment
** lilyp
> +(define (elixir-version elixir)
> +  "Return an X.Y string where X and Y are respectively the major and
> minor version number of ELIXIR.
> +Example: /gnu/store/…-elixir-1.14.0 → 1.14"
> +  ((compose
> +    (cut string-join <> ".")
> +    (cut take <> 2)
> +    (cut string-split <> #\.)
> +    last)
> +   (string-split elixir #\-)))

I don't think we need to be overly cute here.  The let-binding version
from python-build-system is less surprising to the uninitiated reader.

See also strip-store-file-name and package-name->name+version.


** phf
Maybe:
#+begin_src scheme
(define (elixir-version elixir)
  "Return an X.Y string where X and Y are respectively the major and minor
version number of ELIXIR.
Example: /gnu/store/…-elixir-1.14.0 → 1.14"
  (receive (_ version) (package-name->name+version (strip-store-file-name
elixir))
    (let* ((components  (string-split version #\.))
           (major+minor (take components 2)))
      (string-join major+minor "."))))
#+end_src

or:
#+begin_src scheme
(define (elixir-version elixir)
  "Return an X.Y string where X and Y are respectively the major and minor
version number of ELIXIR.
Example: /gnu/store/…-elixir-1.14.0 → 1.14"
  (let* ((version     (last (string-split elixir #\-)))
         (components  (string-split version #\.))
         (major+minor (take components 2)))
    (string-join major+minor ".")))
#+end_src

or: just inline the code as it is used just once. See
[[id:76abe0e4-a0e2-4176-bdc0-9ff241e8ba42][next comment]].


* Comment
:PROPERTIES:
:ID:       76abe0e4-a0e2-4176-bdc0-9ff241e8ba42
:END:

** lilyp
> +(define (elixir-libdir elixir path)
> +  "Return the path where all libraries for a specified ELIXIR
> version are installed."
> +  (string-append path "/lib/elixir/" (elixir-version elixir)))

You probably want to swap path and elixir and perhaps also find a way
to implicitly parameterize the latter so as to make it optional.


** phf
Is this what you mean?
#+begin_src scheme
;; The Elixir version is constant as soon as it is computable from the
current
;; execution. It is a X.Y string where X and Y are respectively the major
and
;; minor version number of the Elixir used in the build.
(define elixir-version (make-parameter "X.Y"))

(define* (elixir-libdir path #:optional (version (elixir-version)))
  "Return the path where all libraries for a specified ELIXIR version are
installed."
  (string-append path "/lib/elixir/" version))

(define* (configure #:key inputs mix-path mix-exs #:allow-other-keys)
  …
  (elixir-version
   (receive (_ version) (package-name->name+version (strip-store-file-name
(assoc-ref inputs "elixir")))
     (let* ((components  (string-split version #\.))
            (major+minor (take components 2)))
       (string-join major+minor ".")))))
#+end_src


* Comment
** lilyp
> +(define* (configure #:key inputs mix-path mix-exs #:allow-other-
> keys)
> +  "Set environment variables.
> +See:
> https://hexdocs.pm/mix/1.15.7/Mix.html#module-environment-variables"
> +  (setenv "LC_ALL" "en_US.UTF-8")
> +  (setenv "MIX_HOME" (getcwd))
> +  (setenv "MIX_ARCHIVES" "archives")
> +  (setenv "MIX_BUILD_ROOT" "_build")
> +  (setenv "MIX_DEPS_PATH" "deps")
> +  (setenv "MIX_PATH" (or mix-path ""))
> +  (setenv "MIX_REBAR3" (string-append (assoc-ref inputs "rebar3")
> "/bin/rebar3"))
> +  (setenv "MIX_EXS" mix-exs))

This does not appear to be a configure phase in the traditional sense
of the wording.  Instead, it should be a post 'set-paths' 'set-mix-env'
imho.


** phf
After ~install-locale~ since ~(setenv "LC_ALL" "en_US.UTF-8")~ is called in
this
phase.
#+begin_src scheme
(define %standard-phases
  (modify-phases gnu:%standard-phases
    …
    (delete 'configure)
    (add-after 'install-locale 'set-mix-env set-mix-env)
    (replace 'unpack unpack)
    …))
#+end_src


* Comment
** lilyp
> +(define* (install-hex #:key name inputs outputs #:allow-other-keys)
> +  "Install Hex."
> +  (let ((hex-archive-path (string-append (getenv "MIX_ARCHIVES")
> "/hex")))
> +    (mkdir-p hex-archive-path)
> +    (symlink (car (list-directories (elixir-libdir (assoc-ref inputs
> "elixir")
> +                                                   (assoc-ref inputs
> "elixir-hex"))))
> +             (string-append hex-archive-path "/hex"))))

Why do we need this?  It looks like we'll be pasting the same (native?)
input all over the store, which imho would be bad.


** phf
Without ~Hex~, you get this error:
#+begin_example
starting phase `build'
Could not find Hex, which is needed to build dependency :ex_doc
Shall I install Hex? (if running non-interactively, use "mix local.hex
--force") [Yn]
#+end_example
This message is called from ~handle_rebar_not_found~ in
~lib/mix/lib/mix/tasks/deps.compile.ex~ ; which is called from ~rebar_cmd~
because
a ~manager~ (~Hex~) could not be found. Hex must be present => install-hex
must be
executed.

I thought that this should not be a problem since Hex is needed at build
time,
and just symlinked. Maybe should it be copied instead?


* Comment
** lilyp
> +  (define (install-input mix-env input)
> +    (let ((dir (mix-build-dir mix-env)))
> +      (mkdir-p dir)
> +      (match input
> +        ((_ . path)
> +         ((compose
> +           (cut for-each (cut install-lib <> dir) <>)
> +           (cut append-map list-directories <>)
> +           (cut filter directory-exists? <>))
> +          (list (elixir-libdir (assoc-ref inputs "elixir") path)
> +                (erlang-libdir path)))))))

I think you're at the wrong layer of abstraction here.

  (match input
    ((_ . prefix)
     (begin
      (install-subdirectories (elixir-libdir path))
      (install-subdirectories (erlang-libdir path)))))

where (install-subdirectories PATH) is basically
  (when (directory-exists? PATH)
    (for-each (cute install-lib <> (mix-build-dir mix-env))
              (list-directories PATH)))


** phf
Does this work?
#+begin_src scheme
(define (install-lib lib dir)
    (let ((lib-name (last (string-split lib #\/))))
      (symlink lib (string-append dir "/" lib-name))))

  (define (install-subdirectories mix-env path)
    (let ((build-dir (mix-build-dir mix-env)))
      (mkdir-p build-dir)
      (when (directory-exists? path)
        (for-each (cute install-lib <> build-dir)
                  (list-directories path)))))

  (define (install-input mix-env input)
    (match input
      ((_ . path)
       (begin
         (install-subdirectories mix-env (elixir-libdir path))
         (install-subdirectories mix-env (erlang-libdir path))))))
#+end_src


* Comment
** lilyp
> +  (define (install-inputs mix-env)
> +    (for-each (cut install-input mix-env <>)
> +              (append inputs native-inputs)))

Installing native inputs: probably a bad idea (inhibits cross-
compilation).


** phf
A slight variant that does not link things when it should not:
#+begin_src scheme
(define (install-inputs mix-env)
    (for-each (cut install-input mix-env <>)
              (cond
                ((string=? mix-env "prod") inputs)
                ((member mix-env '("shared" "test")) (append inputs
native-inputs))
                (else (error (format #f "Unexpected Mix env: ~a~%"
mix-env))))))
#+end_src

I did not consider cross-compilation yet. The following might be wrong be
here
we go. I far as I understand, Erlang applications are largely platform
independent. Once the code is compiled to BEAM bytecode, it can run on any
platform that has the Erlang VM installed. If an Erlang library uses native
extensions, then cross-compilation might be required. For a build to succeed
in a given environment (one of "prod", "test", "shared"), the BEAM files of
all dependencies should be present on the build machine. So, all
dependencies
must be installed


* Comment
** lilyp
> +(define (library-name pkg-name)
> +  "Return the library name deduced from PKG-NAME.
> +A package should be named: elixir-lib-name-X.Y.Z from which the
> library name
> +lib_name is deduced."
> +  ((compose
> +    (cut string-join <> "_")
> +    (cut drop-right <> 1)
> +    (cut string-split <> #\-))
> +   (strip-elixir-prefix pkg-name)))

Consider defining (package-name-version->elixir-name) analogous to
(package-name-version->erlang-name) in rebar-build-system.  Also allow
overriding it through a build system argument.  The thing you currently
have will blow up with git-version.


** phf
Faily close to the ~rebar-build-system~ version.
#+begin_src scheme
(define (package-name-version->elixir-name name+ver)
  "Convert the Guix package NAME-VER to the corresponding Elixir
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 (string-prefix? "elixir-" name-)
          (string-drop name- (string-length "elixir-"))
          name-)
      #\-)
     "_")))
#+end_src
Liliana Marie Prikler Nov. 16, 2023, 3:11 p.m. UTC | #3
Am Donnerstag, dem 16.11.2023 um 14:01 +0100 schrieb Pierre-Henry
Fröhring:
> Do you mind if I paste the conversation using the following org
> format?
I do mind you pasting it in HTML format :P

> * Comment
> ** lilyp
> > +(define (elixir-version elixir)
> > +  "Return an X.Y string where X and Y are respectively the major
> > and
> > minor version number of ELIXIR.
> > +Example: /gnu/store/…-elixir-1.14.0 → 1.14"
> > +  ((compose
> > +    (cut string-join <> ".")
> > +    (cut take <> 2)
> > +    (cut string-split <> #\.)
> > +    last)
> > +   (string-split elixir #\-)))
> 
> I don't think we need to be overly cute here.  The let-binding
> version from python-build-system is less surprising to the
> uninitiated reader.
> 
> See also strip-store-file-name and package-name->name+version.
> 
> 
> ** phf
> Maybe:
> #+begin_src scheme
> (define (elixir-version elixir)
>   "Return an X.Y string where X and Y are respectively the major and
> minor version number of ELIXIR.
> Example: /gnu/store/…-elixir-1.14.0 → 1.14"
>   (receive (_ version) (package-name->name+version (strip-store-file-
> name elixir))
>     (let* ((components  (string-split version #\.))
>            (major+minor (take components 2)))
>       (string-join major+minor "."))))
> #+end_src
> 
> or:
> #+begin_src scheme
> (define (elixir-version elixir)
>   "Return an X.Y string where X and Y are respectively the major and
> minor version number of ELIXIR.
> Example: /gnu/store/…-elixir-1.14.0 → 1.14"
>   (let* ((version     (last (string-split elixir #\-)))
>          (components  (string-split version #\.))
>          (major+minor (take components 2)))
>     (string-join major+minor ".")))
> #+end_src
> 
> or: just inline the code as it is used just once. See [[id:76abe0e4-
> a0e2-4176-bdc0-9ff241e8ba42][next comment]].
Note that you can use SRFI-71 let and let* to bind multiple values at
once.  So you can write the first one without receive.

> 
> * Comment
> :PROPERTIES:
> :ID:       76abe0e4-a0e2-4176-bdc0-9ff241e8ba42
> :END:
> 
> ** lilyp
> > +(define (elixir-libdir elixir path)
> > +  "Return the path where all libraries for a specified ELIXIR
> > version are installed."
> > +  (string-append path "/lib/elixir/" (elixir-version elixir)))
> 
> You probably want to swap path and elixir and perhaps also find a way
> to implicitly parameterize the latter so as to make it optional.
> 
> 
> ** phf
> Is this what you mean?
> #+begin_src scheme
> ;; The Elixir version is constant as soon as it is computable from
> the current
> ;; execution. It is a X.Y string where X and Y are respectively the
> major and
> ;; minor version number of the Elixir used in the build.
> (define elixir-version (make-parameter "X.Y"))
> 
> (define* (elixir-libdir path #:optional (version (elixir-version)))
>   "Return the path where all libraries for a specified ELIXIR version
> are installed."
>   (string-append path "/lib/elixir/" version))
> 
> (define* (configure #:key inputs mix-path mix-exs #:allow-other-keys)
>   …
>   (elixir-version
>    (receive (_ version) (package-name->name+version (strip-store-
> file-name (assoc-ref inputs "elixir")))
>      (let* ((components  (string-split version #\.))
>             (major+minor (take components 2)))
>        (string-join major+minor ".")))))
> #+end_src
I'd use %elixir-version and perhaps make it a fluent rather than a
parameter (not quite sure whether parameters get reset when a function
goes out of scope).

> * Comment
> ** lilyp
> > +(define* (configure #:key inputs mix-path mix-exs #:allow-other-
> > keys)
> > +  "Set environment variables.
> > +See:
> > https://hexdocs.pm/mix/1.15.7/Mix.html#module-environment-variables
> > "
> > +  (setenv "LC_ALL" "en_US.UTF-8")
> > +  (setenv "MIX_HOME" (getcwd))
> > +  (setenv "MIX_ARCHIVES" "archives")
> > +  (setenv "MIX_BUILD_ROOT" "_build")
> > +  (setenv "MIX_DEPS_PATH" "deps")
> > +  (setenv "MIX_PATH" (or mix-path ""))
> > +  (setenv "MIX_REBAR3" (string-append (assoc-ref inputs "rebar3")
> > "/bin/rebar3"))
> > +  (setenv "MIX_EXS" mix-exs))
> 
> This does not appear to be a configure phase in the traditional sense
> of the wording.  Instead, it should be a post 'set-paths' 'set-mix-
> env' imho.
> 
> 
> ** phf
> After ~install-locale~ since ~(setenv "LC_ALL" "en_US.UTF-8")~ is
> called in this phase.
> #+begin_src scheme
> (define %standard-phases
>   (modify-phases gnu:%standard-phases
>     …
>     (delete 'configure)
>     (add-after 'install-locale 'set-mix-env set-mix-env)
>     (replace 'unpack unpack)
>     …))
> #+end_src
Good point: you shouldn't be setting LC_ALL anyway, that's already done
by install-locale.

> * Comment
> ** lilyp
> > +(define* (install-hex #:key name inputs outputs #:allow-other-
> > keys)
> > +  "Install Hex."
> > +  (let ((hex-archive-path (string-append (getenv "MIX_ARCHIVES")
> > "/hex")))
> > +    (mkdir-p hex-archive-path)
> > +    (symlink (car (list-directories (elixir-libdir (assoc-ref
> > inputs
> > "elixir")
> > +                                                   (assoc-ref
> > inputs
> > "elixir-hex"))))
> > +             (string-append hex-archive-path "/hex"))))
> 
> Why do we need this?  It looks like we'll be pasting the same
> (native?) input all over the store, which imho would be bad.
> 
> 
> ** phf
> Without ~Hex~, you get this error:
> #+begin_example
> starting phase `build'
> Could not find Hex, which is needed to build dependency :ex_doc
> Shall I install Hex? (if running non-interactively, use "mix
> local.hex --force") [Yn]
> #+end_example
> This message is called from ~handle_rebar_not_found~ in
> ~lib/mix/lib/mix/tasks/deps.compile.ex~ ; which is called from
> ~rebar_cmd~ because
> a ~manager~ (~Hex~) could not be found. Hex must be present =>
> install-hex must be executed.
> 
> I thought that this should not be a problem since Hex is needed at
> build time, and just symlinked.  Maybe should it be copied instead?
Is hex not an (implicit) native-input in your build system?  Anything
that keeps it from functioning is a packaging bug imho.

> * Comment
> ** lilyp
> > +  (define (install-input mix-env input)
> > +    (let ((dir (mix-build-dir mix-env)))
> > +      (mkdir-p dir)
> > +      (match input
> > +        ((_ . path)
> > +         ((compose
> > +           (cut for-each (cut install-lib <> dir) <>)
> > +           (cut append-map list-directories <>)
> > +           (cut filter directory-exists? <>))
> > +          (list (elixir-libdir (assoc-ref inputs "elixir") path)
> > +                (erlang-libdir path)))))))
> 
> I think you're at the wrong layer of abstraction here.
> 
>   (match input
>     ((_ . prefix)
>      (begin
>       (install-subdirectories (elixir-libdir path))
>       (install-subdirectories (erlang-libdir path)))))
> 
> where (install-subdirectories PATH) is basically
>   (when (directory-exists? PATH)
>     (for-each (cute install-lib <> (mix-build-dir mix-env))
>               (list-directories PATH)))
> 
> 
> ** phf
> Does this work?
> #+begin_src scheme
> (define (install-lib lib dir)
>     (let ((lib-name (last (string-split lib #\/))))
>       (symlink lib (string-append dir "/" lib-name))))
> 
>   (define (install-subdirectories mix-env path)
>     (let ((build-dir (mix-build-dir mix-env)))
>       (mkdir-p build-dir)
>       (when (directory-exists? path)
>         (for-each (cute install-lib <> build-dir)
>                   (list-directories path)))))
Maybe move the mkdir-p into the when or at the start of install-lib.


>   (define (install-input mix-env input)
>     (match input
>       ((_ . path)
>        (begin
>          (install-subdirectories mix-env (elixir-libdir path))
>          (install-subdirectories mix-env (erlang-libdir path))))))
> #+end_src
> 
> 
> * Comment
> ** lilyp
> > +  (define (install-inputs mix-env)
> > +    (for-each (cut install-input mix-env <>)
> > +              (append inputs native-inputs)))
> 
> Installing native inputs: probably a bad idea (inhibits cross-
> compilation).
> 
> 
> ** phf
> A slight variant that does not link things when it should not:
> #+begin_src scheme
> (define (install-inputs mix-env)
>     (for-each (cut install-input mix-env <>)
>               (cond
>                 ((string=? mix-env "prod") inputs)
>                 ((member mix-env '("shared" "test")) (append inputs
> native-inputs))
>                 (else (error (format #f "Unexpected Mix env: ~a~%"
> mix-env))))))
> #+end_src
> 
> I did not consider cross-compilation yet. The following might be
> wrong be here we go. I far as I understand, Erlang applications are
> largely platform independent. Once the code is compiled to BEAM
> bytecode, it can run on any platform that has the Erlang VM
> installed.  If an Erlang library uses native extensions, then cross-
> compilation might be required.  For a build to succeed
> in a given environment (one of "prod", "test", "shared"), the BEAM
> files of all dependencies should be present on the build machine. So,
> all dependencies must be installed
Not an expert on elixir, but that sounds borked.  You might get around
this with propagated-inputs maybe?  That being said, native-inputs
shouldn't blow up a build if missing.

> * Comment
> ** lilyp
> > +(define (library-name pkg-name)
> > +  "Return the library name deduced from PKG-NAME.
> > +A package should be named: elixir-lib-name-X.Y.Z from which the
> > library name
> > +lib_name is deduced."
> > +  ((compose
> > +    (cut string-join <> "_")
> > +    (cut drop-right <> 1)
> > +    (cut string-split <> #\-))
> > +   (strip-elixir-prefix pkg-name)))
> 
> Consider defining (package-name-version->elixir-name) analogous to
> (package-name-version->erlang-name) in rebar-build-system.  Also
> allow overriding it through a build system argument.  The thing you
> currently have will blow up with git-version.
> 
> 
> ** phf
> Faily close to the ~rebar-build-system~ version.
> #+begin_src scheme
> (define (package-name-version->elixir-name name+ver)
>   "Convert the Guix package NAME-VER to the corresponding Elixir
> 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 (string-prefix? "elixir-" name-)
>           (string-drop name- (string-length "elixir-"))
>           name-)
>       #\-)
>      "_")))
> #+end_src
Looks okay.
Pierre-Henry Fröhring Nov. 16, 2023, 6:12 p.m. UTC | #4
I should really configure mu4e or something like that.
I'm curious to know if you have a pointer to an efficient setup for working
through emails like this.
Below the last three comments:

* WAITING Comment
:PROPERTIES:
:ID:       76abe0e4-a0e2-4176-bdc0-9ff241e8ba42
:END:

** lilyp
> +(define (elixir-libdir elixir path)
> +  "Return the path where all libraries for a specified ELIXIR
> version are installed."
> +  (string-append path "/lib/elixir/" (elixir-version elixir)))

You probably want to swap path and elixir and perhaps also find a way
to implicitly parameterize the latter so as to make it optional.


** phf
Is this what you mean?
#+begin_src scheme
;; The Elixir version is constant as soon as it is computable from the
current
;; execution. It is a X.Y string where X and Y are respectively the major
and
;; minor version number of the Elixir used in the build.
(define elixir-version (make-parameter "X.Y"))

(define* (elixir-libdir path #:optional (version (elixir-version)))
  "Return the path where all libraries for a specified ELIXIR version are
installed."
  (string-append path "/lib/elixir/" version))

(define* (configure #:key inputs mix-path mix-exs #:allow-other-keys)
  …
  (elixir-version
   (receive (_ version) (package-name->name+version (strip-store-file-name
(assoc-ref inputs "elixir")))
     (let* ((components  (string-split version #\.))
            (major+minor (take components 2)))
       (string-join major+minor ".")))))
#+end_src


** lilyp
I'd use %elixir-version and perhaps make it a fluent rather than a parameter
(not quite sure whether parameters get reset when a function goes out of
scope).


** phf
Parameters do not get reset when a function goes out of scope:
#+begin_src scheme
(define x (make-parameter 1))
(define (set-x-to-2) (x 2))
(format #t "~a~%" (x))
(set-x-to-2)
(format #t "~a~%" (x))
#+end_src

#+begin_example
1
2
#+end_example

The [[
https://www.gnu.org/software/guile/manual/html_node/Parameters.html][documentation]]
seems to indicate that it's an appropriate replacement for a
global variable.


* WAITING Comment
** lilyp
Is hex not an (implicit) native-input in your build system?  Anything that
keeps it from functioning is a packaging bug imho.


** phf
If ~mix~ finds ~Hex~ under ~$MIX_ARCHIVES/hex/hex~, then ~mix compile~ does
not emit
the message above. I'm not sure how could this be changed. I've tried to set
~MIX_PATH~ to ~/gnu/store/…-elixir-hex-2.0.5/lib/elixir/1.14~ without
success. So,
this is the reason why ~install-hex~ phase installs Hex like it does.


* WAITING Comment
** lilyp
> +  (define (install-inputs mix-env)
> +    (for-each (cut install-input mix-env <>)
> +              (append inputs native-inputs)))

Installing native inputs: probably a bad idea (inhibits cross-
compilation).


** phf
A slight variant that does not link things when it should not:
#+begin_src scheme
(define (install-inputs mix-env)
    (for-each (cut install-input mix-env <>)
              (cond
                ((string=? mix-env "prod") inputs)
                ((member mix-env '("shared" "test")) (append inputs
native-inputs))
                (else (error (format #f "Unexpected Mix env: ~a~%"
mix-env))))))
#+end_src

I did not consider cross-compilation yet. The following might be wrong be
here
we go. I far as I understand, Erlang applications are largely platform
independent. Once the code is compiled to BEAM bytecode, it can run on any
platform that has the Erlang VM installed. If an Erlang library uses native
extensions, then cross-compilation might be required. For a build to succeed
in a given environment (one of "prod", "test", "shared"), the BEAM files of
all dependencies should be present on the build machine. So, all
dependencies
must be installed


** lilyp
Not an expert on elixir, but that sounds borked.


** phf
Yes. Did not have time to look into it as of now.


** lilyp
You might get around this with propagated-inputs maybe?  That being said,
native-inputs shouldn't blow up a build if missing.


** phf
If ~native-inputs~ are missing, it's fine. But wait, maybe there is a
misunderstanding here. Please, check the reasoning: in a cross-compilation
context, we have two machines A and B, and:
- native-inputs: dependencies that must be built on A for A ;
- inputs: dependencies that must be built on A for B ;
- propagated-inputs: like inputs but installed in the profile.


If installing Elixir (like Python) gathers all libraries in the profile
with a
variable like ~GUIX_ERL_LIBS~, then, it would be enough to list
dependencies (in
packages) in ~propagated-inputs~ instead of ~inputs~ and make them
available to
Elixir through ~ERL_LIBS~ (like ~GUIX_PYTHONPATH~ and ~PYTHONPATH~). As a
consequence, the ~install-dependencies~ phase would be reduced to
~ERL_LIBS=$GUIX_ERL_LIBS~.


Is this an answer to: "You might get around this with propagated-inputs
maybe?"
Liliana Marie Prikler Nov. 16, 2023, 7:34 p.m. UTC | #5
Am Donnerstag, dem 16.11.2023 um 19:12 +0100 schrieb Pierre-Henry
Fröhring:
> I should really configure mu4e or something like that.
> I'm curious to know if you have a pointer to an efficient setup for
> working through emails like this.
> Below the last three comments:
> 
> * WAITING Comment
> :PROPERTIES:
> :ID:       76abe0e4-a0e2-4176-bdc0-9ff241e8ba42
> :END:
> 
> ** lilyp
> > +(define (elixir-libdir elixir path)
> > +  "Return the path where all libraries for a specified ELIXIR
> > version are installed."
> > +  (string-append path "/lib/elixir/" (elixir-version elixir)))
> 
> You probably want to swap path and elixir and perhaps also find a way
> to implicitly parameterize the latter so as to make it optional.
> 
> 
> ** phf
> Is this what you mean?
> #+begin_src scheme
> ;; The Elixir version is constant as soon as it is computable from
> the current
> ;; execution. It is a X.Y string where X and Y are respectively the
> major and
> ;; minor version number of the Elixir used in the build.
> (define elixir-version (make-parameter "X.Y"))
> 
> (define* (elixir-libdir path #:optional (version (elixir-version)))
>   "Return the path where all libraries for a specified ELIXIR version
> are installed."
>   (string-append path "/lib/elixir/" version))
> 
> (define* (configure #:key inputs mix-path mix-exs #:allow-other-keys)
>   …
>   (elixir-version
>    (receive (_ version) (package-name->name+version (strip-store-
> file-name (assoc-ref inputs "elixir")))
>      (let* ((components  (string-split version #\.))
>             (major+minor (take components 2)))
>        (string-join major+minor ".")))))
> #+end_src
> 
> 
> ** lilyp
> I'd use %elixir-version.
> 
> 
> ** phf
> Parameters do not get reset when a function goes out of scope:
> #+begin_src scheme
> (define x (make-parameter 1))
> (define (set-x-to-2) (x 2))
> (format #t "~a~%" (x))
> (set-x-to-2)
> (format #t "~a~%" (x))
> #+end_src
> 
> #+begin_example
> 1
> 2
> #+end_example
> 
> The documentation seems to indicate that it's an appropriate
> replacement for a global variable.
It also uses the special parameterize thing to scope values.  That
being said, if parameters work for you, then parameters are fine.

> 
> * WAITING Comment** lilyp
> Is hex not an (implicit) native-input in your build system?  Anything
> that keeps it from functioning is a packaging bug imho.
> 
> 
> ** phf
> If ~mix~ finds ~Hex~ under ~$MIX_ARCHIVES/hex/hex~, then ~mix
> compile~ does not emit the message above. I'm not sure how could this
> be changed. I've tried to set ~MIX_PATH~ to ~/gnu/store/…-elixir-hex-
> 2.0.5/lib/elixir/1.14~ without
> success. So, this is the reason why ~install-hex~ phase installs Hex
> like it does.
Look into mix and how it invokes hex.  There's hardcoding potential,
I'm sure of it.

> * WAITING Comment
> ** lilyp
> > +  (define (install-inputs mix-env)
> > +    (for-each (cut install-input mix-env <>)
> > +              (append inputs native-inputs)))
> 
> Installing native inputs: probably a bad idea (inhibits cross-
> compilation).
> 
> 
> ** phf
> A slight variant that does not link things when it should not:
> #+begin_src scheme
> (define (install-inputs mix-env)
>     (for-each (cut install-input mix-env <>)
>               (cond
>                 ((string=? mix-env "prod") inputs)
>                 ((member mix-env '("shared" "test")) (append inputs
> native-inputs))
>                 (else (error (format #f "Unexpected Mix env: ~a~%"
> mix-env))))))
> #+end_src
> 
> I did not consider cross-compilation yet. The following might be
> wrong be here we go. I far as I understand, Erlang applications are
> largely platform independent. Once the code is compiled to BEAM
> bytecode, it can run on any platform that has the Erlang VM
> installed. If an Erlang library uses native extensions, then cross-
> compilation might be required. For a build to succeed
> in a given environment (one of "prod", "test", "shared"), the BEAM
> files of all dependencies should be present on the build machine. So,
> all dependencies must be installed
> 
> 
> ** lilyp
> Not an expert on elixir, but that sounds borked.
> 
> 
> ** phf
> Yes. Did not have time to look into it as of now.
> 
> 
> ** lilyp
> You might get around this with propagated-inputs maybe?  That being
> said, native-inputs shouldn't blow up a build if missing.
> 
> 
> ** phf
> If ~native-inputs~ are missing, it's fine. But wait, maybe there is a
> misunderstanding here. Please, check the reasoning: in a cross-
> compilation context, we have two machines A and B, and:
> - native-inputs: dependencies that must be built on A for A ;
> - inputs: dependencies that must be built on A for B ;
> - propagated-inputs: like inputs but installed in the profile.
> 
> 
> If installing Elixir (like Python) gathers all libraries in the
> profile with a variable like ~GUIX_ERL_LIBS~, then, it would be
> enough to list dependencies (in packages) in ~propagated-inputs~
> instead of ~inputs~ and make them available to Elixir through
> ~ERL_LIBS~ (like ~GUIX_PYTHONPATH~ and ~PYTHONPATH~).
> As a consequence, the ~install-dependencies~ phase would be reduced
> to ~ERL_LIBS=$GUIX_ERL_LIBS~.
> 
> Is this an answer to: "You might get around this with propagated-
> inputs maybe?"
If environment variables work, that is clearly to be preferred over any
other magic we've considered so far.  My hunch is that rebar-build-
system was written this way because environment variables didn't work,
however.  Same with Rust and Node, which are kinda yucky in this
regard.

Other than that, yes, (ab)using propagated-inputs like that is the way
to go when there's no smarter alternative available.

Cheers
Pierre-Henry Fröhring Nov. 17, 2023, 7:36 a.m. UTC | #6
* WAITING Comment
** lilyp
> +(define* (install-hex #:key name inputs outputs #:allow-other-keys)
> +  "Install Hex."
> +  (let ((hex-archive-path (string-append (getenv "MIX_ARCHIVES")
> "/hex")))
> +    (mkdir-p hex-archive-path)
> +    (symlink (car (list-directories (elixir-libdir (assoc-ref inputs
> "elixir")
> +                                                   (assoc-ref inputs
> "elixir-hex"))))
> +             (string-append hex-archive-path "/hex"))))

Why do we need this?  It looks like we'll be pasting the same (native?)
input all over the store, which imho would be bad.


** phf
Without ~Hex~, you get this error:
#+begin_example
starting phase `build'
Could not find Hex, which is needed to build dependency :ex_doc
Shall I install Hex? (if running non-interactively, use "mix local.hex
--force") [Yn]
#+end_example
This message is called from ~handle_rebar_not_found~ in
~lib/mix/lib/mix/tasks/deps.compile.ex~ ; which is called from
~rebar_cmd~ because
a ~manager~ (~Hex~) could not be found. Hex must be present =>
install-hex must be
executed.

I thought that this should not be a problem since Hex is needed at build time,
and just symlinked. Maybe should it be copied instead?


** lilyp
Is hex not an (implicit) native-input in your build system?  Anything that
keeps it from functioning is a packaging bug imho.


** phf
If ~mix~ finds ~Hex~ under ~$MIX_ARCHIVES/hex/hex~, then ~mix compile~
does not emit
the message above. I'm not sure how could this be changed. I've tried to set
~MIX_PATH~ to ~/gnu/store/…-elixir-hex-2.0.5/lib/elixir/1.14~ without
success. So,
this is the reason why ~install-hex~ phase installs Hex like it does.


** lilyp
Look into mix and how it invokes hex.  There's hardcoding potential, I'm sure
of it.


** phf
:PROPERTIES:
:ID:       d9ffbbfa-3cd3-4a44-9acb-04a0627b90d7
:END:

- Observation: ~mix compile~ ⇒ "Could not find Hex, which is needed to
build dependency :ex_doc".
- We have the source
  - guix build --source
- We found from where this message is emitted.
  - lib/mix/lib/mix/hex.ex
- We now why the message was emitted.
  - Code.ensure_loaded?(Hex) failed.
  - https://hexdocs.pm/elixir/1.14.0/Code.html#ensure_loaded/1

Well, adding the store path of Hex to ~ERL_LIBS~ solves the problem. The
~install-hex~ phase can be deleted in favor of manipulating ~ERL_LIBS~ as
suggested with propagated inputs
[[id:d7cd6e3d-9802-499f-a157-7698aca942d4][below]].


* WAITING Comment
:PROPERTIES:
:ID:       d7cd6e3d-9802-499f-a157-7698aca942d4
:END:

** lilyp
> +  (define (install-inputs mix-env)
> +    (for-each (cut install-input mix-env <>)
> +              (append inputs native-inputs)))

Installing native inputs: probably a bad idea (inhibits cross-
compilation).


** phf
A slight variant that does not link things when it should not:
#+begin_src scheme
(define (install-inputs mix-env)
    (for-each (cut install-input mix-env <>)
              (cond
                ((string=? mix-env "prod") inputs)
                ((member mix-env '("shared" "test")) (append inputs
native-inputs))
                (else (error (format #f "Unexpected Mix env: ~a~%" mix-env))))))
#+end_src

I did not consider cross-compilation yet. The following might be wrong be here
we go. As far as I understand, Erlang applications are largely platform
independent. Once the code is compiled to BEAM bytecode, it can run on any
platform that has the Erlang VM installed. If an Erlang library uses native
extensions, then cross-compilation might be required. For a build to succeed
in a given environment (one of "prod", "test", "shared"), the BEAM files of
all dependencies should be present on the build machine. So, all dependencies
must be installed


** lilyp
Not an expert on elixir, but that sounds borked.


** phf
Yes. Did not have time to look into it as of now.


** lilyp
You might get around this with propagated-inputs maybe?  That being said,
native-inputs shouldn't blow up a build if missing.


** phf
If ~native-inputs~ are missing, it's fine. But wait, maybe there is a
misunderstanding here. Please, check the reasoning: in a cross-compilation
context, we have two machines A and B, and:
- native-inputs: dependencies that must be built on A for A ;
- inputs: dependencies that must be built on A for B ;
- propagated-inputs: like inputs but installed in the profile.


If installing Elixir (like Python) gathers all libraries in the profile with a
variable like ~GUIX_ERL_LIBS~, then, it would be enough to list dependencies (in
packages) in ~propagated-inputs~ instead of ~inputs~ and make them available to
Elixir through ~ERL_LIBS~ (like ~GUIX_PYTHONPATH~ and ~PYTHONPATH~). As a
consequence, the ~install-dependencies~ phase would be reduced to
~ERL_LIBS=$GUIX_ERL_LIBS~.


Is this an answer to: "You might get around this with propagated-inputs
maybe?"


** lilyp
If environment variables work, that is clearly to be preferred over any other
magic we've considered so far.  My hunch is that rebar-build- system was
written this way because environment variables didn't work, however.  Same
with Rust and Node, which are kinda yucky in this regard.

Other than that, yes, (ab)using propagated-inputs like that is the way to go
when there's no smarter alternative available.


** phf
As hinted [[id:d9ffbbfa-3cd3-4a44-9acb-04a0627b90d7][above]], abusing
propagated inputs works up to the ~elixir-machete~
package. Essentially, it is enough to add the code below to the
~elixir~ package:
#+begin_src scheme
(native-search-paths
 (list (search-path-specification
        (variable "GUIX_ERL_LIBS")
        (files (list "lib/erlang/lib"
                     (string-append "lib/elixir/" (version-major+minor
version)))))))
#+end_src


~install-dependencies~ has been deleted altogether for a single line added to
~set-mix-env~.
#+begin_src scheme
(setenv "ERL_LIBS" (getenv "GUIX_ERL_LIBS"))
#+end_src

So, if Erlang and Elixir dependencies are added to propagated inputs, then it
seems to work as expected.
diff mbox series

Patch

diff --git a/guix/build-system/mix.scm b/guix/build-system/mix.scm
new file mode 100644
index 00000000..ae80679b
--- /dev/null
+++ b/guix/build-system/mix.scm
@@ -0,0 +1,181 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Pierre-Henry Fröhring <phfrohring@deeplinks.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/>.
+
+;; Commentary:
+;;
+;; Standard build procedure for Elixir packages using 'mix'.  This is
+;; implemented as an extension of 'gnu-build-system'.
+;;
+;; Code:
+
+(define-module (guix build-system mix)
+  #:use-module (guix build mix-build-system)
+  #:use-module (guix build-system gnu)
+  #:use-module (guix build-system)
+  #:use-module (guix gexp)
+  #:use-module (guix monads)
+  #:use-module (guix packages)
+  #:use-module (guix search-paths)
+  #:use-module (guix store)
+  #:use-module (guix utils)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:export (mix-build-system hexpm-uri))
+
+;; Lazily resolve the bindings to avoid circular dependencies.
+(define (default-glibc-utf8-locales)
+  (let* ((base (resolve-interface '(gnu packages base))))
+    (module-ref base 'glibc-utf8-locales)))
+
+(define (default-elixir-hex)
+  (let ((elixir (resolve-interface '(gnu packages elixir))))
+    (module-ref elixir 'elixir-hex)))
+
+(define (default-rebar3)
+  (let ((erlang (resolve-interface '(gnu packages erlang))))
+    (module-ref erlang 'rebar3)))
+
+(define (default-elixir)
+  "Return the default Elixir package."
+  (let ((elixir (resolve-interface '(gnu packages elixir))))
+    (module-ref elixir 'elixir)))
+
+(define (hexpm-uri name version)
+  "Return the URI where to fetch the sources of a Hex package NAME at VERSION.
+NAME is the name of the package which should look like: elixir-pkg-name-X.Y.Z
+See: https://github.com/hexpm/specifications/blob/main/endpoints.md"
+  ((compose
+    (cut string-append "https://repo.hex.pm/tarballs/" <> "-" version ".tar")
+    (cut string-replace-substring <> "-" "_")
+    strip-elixir-prefix)
+   name))
+
+;; A number of environment variables specific to the Mix build system are
+;; reflected here.  They are documented at
+;; https://hexdocs.pm/mix/1.15.7/Mix.html#module-environment-variables.  Other
+;; parameters located in mix.exs are defined at
+;; https://hexdocs.pm/mix/1.15.7/Mix.Project.html#module-configuration
+(define* (mix-build name
+                    inputs
+                    #:key
+                    source
+                    (tests? #t)
+                    (mix-path #f) ;See MIX_PATH.
+                    (mix-exs "mix.exs") ;See MIX_EXS.
+                    (build-per-environment #t) ;See :build_per_environment.
+                    (phases '%standard-phases)
+                    (outputs '("out"))
+                    (search-paths '())
+                    (system (%current-system))
+                    (guile #f)
+                    (imported-modules `((guix build mix-build-system)
+                                        ,@%gnu-build-system-modules))
+                    (modules '((guix build mix-build-system)
+                               (guix build utils))))
+  "Build SOURCE using Elixir, and with INPUTS."
+
+  ;; Check the documentation of :build_per_environment here:
+  ;; https://hexdocs.pm/mix/1.15.7/Mix.Project.html#module-configuration And
+  ;; "Environments" here:
+  ;; https://hexdocs.pm/mix/1.15.7/Mix.html#module-environments
+  (define mix-environments
+    (if build-per-environment
+        `("prod" ,@(if tests? '("test") '()))
+        '("shared")))
+
+  (define builder
+    (with-imported-modules imported-modules
+      #~(begin
+
+          (use-modules #$@(sexp->gexp modules))
+
+          #$(with-build-variables inputs outputs
+              #~(mix-build #:name #$name
+                           #:source #+source
+                           #:system #$system
+                           #:tests? #$tests?
+                           #:mix-path #$mix-path
+                           #:mix-exs #$mix-exs
+                           #:mix-environments '#$mix-environments
+                           #:build-per-environment #$build-per-environment
+                           #: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)))
+    (gexp->derivation name
+                      builder
+                      #:system system
+                      #:graft? #f       ;consistent with 'gnu-build'
+                      #:target #f
+                      #:guile-for-build guile)))
+
+(define* (lower name
+                #:key
+                (elixir (default-elixir))
+                (elixir-hex (default-elixir-hex))
+                (glibc-utf8-locales (default-glibc-utf8-locales))
+                (inputs '())
+                (native-inputs '())
+                (propagated-inputs '())
+                (rebar3 (default-rebar3))
+                (tests? #t)
+                outputs
+                source
+                system
+                target
+                #:allow-other-keys #:rest arguments)
+  "Return a bag for NAME."
+  (let ((private-keywords
+         '(#:inputs #:native-inputs
+           #:outputs #:system #:target
+           #:elixir #:elixir-hex #:glibc-utf8-locales
+           #:rebar3 #:erlang))
+        (build-inputs
+         `(,@(standard-packages)
+           ("glibc-utf8-locales" ,glibc-utf8-locales)
+           ("erlang" ,(lookup-package-input elixir "erlang"))
+           ("rebar3" ,rebar3)
+           ("elixir" ,elixir)
+           ("elixir-hex" ,elixir-hex)
+           ,@inputs
+           ,@native-inputs)))
+  (bag (name name)
+       (system system)
+       (build-inputs build-inputs)
+       (host-inputs (if target inputs '()))
+       (outputs outputs)
+       (build mix-build)
+       (arguments (strip-keyword-arguments private-keywords arguments)))))
+
+(define mix-build-system
+  (build-system (name 'mix)
+                (description "The standard Mix build system")
+                (lower lower)))
+
+;;; mix.scm ends here
diff --git a/guix/build/mix-build-system.scm b/guix/build/mix-build-system.scm
new file mode 100644
index 00000000..0a1fcb5c
--- /dev/null
+++ b/guix/build/mix-build-system.scm
@@ -0,0 +1,205 @@ 
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Pierre-Henry Fröhring <phfrohring@deeplinks.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/>.
+
+;; Commentary:
+;;
+;; Code:
+
+(define-module (guix build mix-build-system)
+  #:use-module ((guix build gnu-build-system) #:prefix gnu:)
+  #:use-module (guix build utils)
+  #:use-module (ice-9 ftw)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 regex)
+  #:use-module (ice-9 string-fun)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:export (mix-build
+            strip-elixir-prefix
+            %standard-phases))
+
+(define (elixir-version elixir)
+  "Return an X.Y string where X and Y are respectively the major and minor version number of ELIXIR.
+Example: /gnu/store/…-elixir-1.14.0 → 1.14"
+  ((compose
+    (cut string-join <> ".")
+    (cut take <> 2)
+    (cut string-split <> #\.)
+    last)
+   (string-split elixir #\-)))
+
+(define (elixir-libdir elixir path)
+  "Return the path where all libraries for a specified ELIXIR version are installed."
+  (string-append path "/lib/elixir/" (elixir-version elixir)))
+
+(define (erlang-libdir path)
+  "Return the path of the directory where libraries of an Erlang package are
+installed in the store."
+  (string-append path "/lib/erlang/lib"))
+
+(define (install-dir inputs outputs)
+  "Return the path of the current output's Elixir library.
+Example: /gnu/store/…/lib/elixir/1.14"
+  (elixir-libdir (assoc-ref inputs "elixir")
+                 (assoc-ref outputs "out")))
+
+(define (strip-prefix prefix name)
+  "Return NAME without the prefix PREFIX."
+  (if (string-prefix? prefix name)
+      (string-drop name (string-length prefix))
+      name))
+
+(define (strip-elixir-prefix name)
+  "Strip elixir- from NAME."
+  (strip-prefix "elixir-" name))
+
+(define (mix-build-dir mix-env)
+  "Return the directory where build artifacts are to be installed according to
+en environment MIX-ENV in the current directory."
+  (string-append "_build/" mix-env "/lib"))
+
+(define* (unpack #:key source mix-path #: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)
+    (when (file-exists? "contents.tar.gz")
+      (invoke "tar" "xvf" "contents.tar.gz"))))
+
+(define (list-directories dir)
+  "List absolute paths of directories directly under the directory DIR."
+  (map (cut string-append dir "/" <>)
+       (scandir dir (lambda (filename)
+                      (and (not (member filename '("." "..")))
+                           (directory-exists? (string-append dir "/" filename)))))))
+
+(define* (configure #:key inputs mix-path mix-exs #:allow-other-keys)
+  "Set environment variables.
+See: https://hexdocs.pm/mix/1.15.7/Mix.html#module-environment-variables"
+  (setenv "LC_ALL" "en_US.UTF-8")
+  (setenv "MIX_HOME" (getcwd))
+  (setenv "MIX_ARCHIVES" "archives")
+  (setenv "MIX_BUILD_ROOT" "_build")
+  (setenv "MIX_DEPS_PATH" "deps")
+  (setenv "MIX_PATH" (or mix-path ""))
+  (setenv "MIX_REBAR3" (string-append (assoc-ref inputs "rebar3") "/bin/rebar3"))
+  (setenv "MIX_EXS" mix-exs))
+
+(define* (install-hex #:key name inputs outputs #:allow-other-keys)
+  "Install Hex."
+  (let ((hex-archive-path (string-append (getenv "MIX_ARCHIVES") "/hex")))
+    (mkdir-p hex-archive-path)
+    (symlink (car (list-directories (elixir-libdir (assoc-ref inputs "elixir")
+                                                   (assoc-ref inputs "elixir-hex"))))
+             (string-append hex-archive-path "/hex"))))
+
+(define* (install-dependencies #:key
+                               name
+                               inputs
+                               outputs
+                               tests?
+                               build-per-environment
+                               (native-inputs '())
+                               mix-environments
+                               #:allow-other-keys
+                               #:rest rest)
+  "Install dependencies."
+  (define (install-lib lib dir)
+    (let ((lib-name (last (string-split lib #\/))))
+      (symlink lib (string-append dir "/" lib-name))))
+
+  (define (install-input mix-env input)
+    (let ((dir (mix-build-dir mix-env)))
+      (mkdir-p dir)
+      (match input
+        ((_ . path)
+         ((compose
+           (cut for-each (cut install-lib <> dir) <>)
+           (cut append-map list-directories <>)
+           (cut filter directory-exists? <>))
+          (list (elixir-libdir (assoc-ref inputs "elixir") path)
+                (erlang-libdir path)))))))
+
+  (define (install-inputs mix-env)
+    (for-each (cut install-input mix-env <>)
+              (append inputs native-inputs)))
+
+  (for-each install-inputs mix-environments))
+
+(define* (build #:key mix-environments #:allow-other-keys)
+  "Builds the Mix project."
+  (for-each (lambda (mix-env)
+              (setenv "MIX_ENV" mix-env)
+              (invoke "mix" "compile" "--no-deps-check"))
+            mix-environments))
+
+(define* (check #:key (tests? #t) #:allow-other-keys)
+  "Test the Mix project."
+  (if tests?
+      (invoke "mix" "test" "--no-deps-check")
+      (format #t "tests? = ~a~%" tests?)))
+
+(define* (remove-mix-dirs . _)
+  "Remove all .mix/ directories.
+We do not want to copy them to the installation directory."
+  (for-each delete-file-recursively
+            (find-files "." (file-name-predicate "\\.mix$") #:directories? #t)))
+
+(define (library-name pkg-name)
+  "Return the library name deduced from PKG-NAME.
+A package should be named: elixir-lib-name-X.Y.Z from which the library name
+lib_name is deduced."
+  ((compose
+    (cut string-join <> "_")
+    (cut drop-right <> 1)
+    (cut string-split <> #\-))
+   (strip-elixir-prefix pkg-name)))
+
+(define* (install #:key
+                  inputs
+                  outputs
+                  name
+                  build-per-environment
+                  #:allow-other-keys)
+  "Install build artifacts in the store."
+  (let* ((lib-name (library-name name))
+         (lib-dir (string-append (install-dir inputs outputs) "/" lib-name)))
+    (mkdir-p lib-dir)
+    (copy-recursively (string-append (mix-build-dir (if build-per-environment "prod" "shared")) "/" lib-name)
+                      lib-dir
+                      #:follow-symlinks? #t)))
+
+(define %standard-phases
+  (modify-phases gnu:%standard-phases
+    (delete 'bootstrap)
+    (replace 'configure configure)
+    (replace 'unpack unpack)
+    (add-after 'patch-generated-file-shebangs 'install-hex install-hex)
+    (add-after 'install-hex 'install-dependencies install-dependencies)
+    (replace 'build build)
+    (replace 'check check)
+    (add-before 'install 'remove-mix-dirs remove-mix-dirs)
+    (replace 'install install)))
+
+(define* (mix-build #:key inputs (phases %standard-phases)
+                    #:allow-other-keys #:rest args)
+  "Build the given Mix package, applying all of PHASES in order."
+  (apply gnu:gnu-build #:inputs inputs #:phases phases args))
+
+;;; mix-build-system.scm ends here