diff options
author | Nicolas Graves <ngraves@ngraves.fr> | 2023-11-02 16:16:50 +0100 |
---|---|---|
committer | Ludovic Courtès <ludo@gnu.org> | 2023-12-18 23:16:55 +0100 |
commit | 9dab75879140d0f6ca0a25e1f2e3c5b912090d7c (patch) | |
tree | efbca87e1d0b267e1cc56973393fec01335072b6 | |
parent | e8fd78d54e058008028420d87aa434983032fa9c (diff) |
build-system: Add ‘composer-build-system’.
* guix/build-system/composer.scm: New file.
* guix/build/composer-build-system.scm: New file.
* gnu/packages/aux-files/findclass.php: New file.
* Makefile.am: Add them.
* doc/guix.texi (Build Systems): Document it.
Co-authored-by: Julien Lepiller <julien@lepiller.eu>
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Change-Id: Ie6a05b42ff04d3ad774a0a20278a77e4820bb8f6
-rw-r--r-- | Makefile.am | 3 | ||||
-rw-r--r-- | doc/guix.texi | 14 | ||||
-rw-r--r-- | gnu/packages/aux-files/findclass.php | 125 | ||||
-rw-r--r-- | guix/build-system/composer.scm | 165 | ||||
-rw-r--r-- | guix/build/composer-build-system.scm | 301 |
5 files changed, 608 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am index ee0f5ba4fc..b64dcaa77c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -154,6 +154,7 @@ MODULES = \ guix/build-system/clojure.scm \ guix/build-system/cmake.scm \ guix/build-system/copy.scm \ + guix/build-system/composer.scm \ guix/build-system/dub.scm \ guix/build-system/dune.scm \ guix/build-system/elm.scm \ @@ -212,6 +213,7 @@ MODULES = \ guix/build/cargo-utils.scm \ guix/build/chicken-build-system.scm \ guix/build/cmake-build-system.scm \ + guix/build/composer-build-system.scm \ guix/build/dub-build-system.scm \ guix/build/dune-build-system.scm \ guix/build/elm-build-system.scm \ @@ -420,6 +422,7 @@ dist_noinst_DATA = \ AUX_FILES = \ gnu/packages/aux-files/chromium/master-preferences.json \ gnu/packages/aux-files/emacs/guix-emacs.el \ + gnu/packages/aux-files/findclass.php \ gnu/packages/aux-files/guix.vim \ gnu/packages/aux-files/linux-libre/6.6-arm.conf \ gnu/packages/aux-files/linux-libre/6.6-arm64.conf \ diff --git a/doc/guix.texi b/doc/guix.texi index ffd8ae331d..b742a3d5b2 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -9598,6 +9598,20 @@ debugging information''), which roughly means that code is compiled with @code{-O2 -g}, as is the case for Autoconf-based packages by default. @end defvar +@defvar composer-build-system +This variable is exported by @code{(guix build-system composer)}. It +implements the build procedure for packages using +@url{https://getcomposer.org/, Composer}, the PHP package manager. + +It automatically adds the @code{php} package to the set of inputs. Which +package is used can be specified with the @code{#:php} parameter. + +The @code{#:test-target} parameter is used to control which script is run +for the tests. By default, the @code{test} script is run if it exists. If +the script does not exist, the build system will run @code{phpunit} from the +source directory, assuming there is a @file{phpunit.xml} file. +@end defvar + @defvar dune-build-system This variable is exported by @code{(guix build-system dune)}. It supports builds of packages using @uref{https://dune.build/, Dune}, a build diff --git a/gnu/packages/aux-files/findclass.php b/gnu/packages/aux-files/findclass.php new file mode 100644 index 0000000000..d0b250c8e1 --- /dev/null +++ b/gnu/packages/aux-files/findclass.php @@ -0,0 +1,125 @@ +<?php +/** + * The content of this file is copied from composer's src/Composer/Autoload/ClassMapGenerator.php + * the findClasses method was extracted, to prevent using any dependency. + * + * Composer (and thus this file) is distributed under the expat license, and + * ClassMapGenerator.php also contains this notice: + * + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * This file is copied from the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * To the extent to wich it makes sense, as the author of the extract: + * Copyright © 2020 Julien Lepiller <julien@lepiller.eu> + */ + +/** + * Extract the classes in the given file + * + * @param string $path The file to check + * @throws \RuntimeException + * @return array The found classes + */ +function findClasses($path) +{ + $extraTypes = PHP_VERSION_ID < 50400 ? '' : '|trait'; + if (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.3', '>=')) { + $extraTypes .= '|enum'; + } + // Use @ here instead of Silencer to actively suppress 'unhelpful' output + // @link https://github.com/composer/composer/pull/4886 + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + $message = 'File at "%s" does not exist, check your classmap definitions'; + } elseif (!is_readable($path)) { + $message = 'File at "%s" is not readable, check its permissions'; + } elseif ('' === trim(file_get_contents($path))) { + // The input file was really empty and thus contains no classes + return array(); + } else { + $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; + } + $error = error_get_last(); + if (isset($error['message'])) { + $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; + } + throw new \RuntimeException(sprintf($message, $path)); + } + // return early if there is no chance of matching anything in this file + if (!preg_match('{\b(?:class|interface'.$extraTypes.')\s}i', $contents)) { + return array(); + } + // strip heredocs/nowdocs + $contents = preg_replace('{<<<[ \t]*([\'"]?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)(?:\s*)\\2(?=\s+|[;,.)])}s', 'null', $contents); + // strip strings + $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '<?') { + $contents = preg_replace('{^.+?<\?}s', '<?', $contents, 1, $replacements); + if ($replacements === 0) { + return array(); + } + } + // strip non-php blocks in the file + $contents = preg_replace('{\?>(?:[^<]++|<(?!\?))*+<\?}s', '?><?', $contents); + // strip trailing non-php code if needed + $pos = strrpos($contents, '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '<?')) { + $contents = substr($contents, 0, $pos); + } + // strip comments if short open tags are in the file + if (preg_match('{(<\?)(?!(php|hh))}i', $contents)) { + $contents = preg_replace('{//.* | /\*(?:[^*]++|\*(?!/))*\*/}x', '', $contents); + } + preg_match_all('{ + (?: + \b(?<![\$:>])(?P<type>class|interface'.$extraTypes.') \s++ (?P<name>[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?<![\$:>])(?P<ns>namespace) (?P<nsname>\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] + ) + }ix', $contents, $matches); + $classes = array(); + $namespace = ''; + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace(array(' ', "\t", "\r", "\n"), '', $matches['nsname'][$i]) . '\\'; + } else { + $name = $matches['name'][$i]; + // skip anon classes extending/implementing + if ($name === 'extends' || $name === 'implements') { + continue; + } + if ($name[0] === ':') { + // This is an XHP class, https://github.com/facebook/xhp + $name = 'xhp'.substr(str_replace(array('-', ':'), array('_', '__'), $name), 1); + } elseif ($matches['type'][$i] === 'enum') { + // In Hack, something like: + // enum Foo: int { HERP = '123'; } + // The regex above captures the colon, which isn't part of + // the class name. + $name = rtrim($name, ':'); + } + $classes[] = ltrim($namespace . $name, '\\'); + } + } + return $classes; +} + +$options = getopt('i:f:', []); +$file = $options["f"]; +$input = $options["i"]; + +$classes = findClasses($file); +foreach($classes as $class) { + echo '$classmap[\''.$class.'\'] = \''.$input.'/'.$file.'\';'; + echo "\n"; +} diff --git a/guix/build-system/composer.scm b/guix/build-system/composer.scm new file mode 100644 index 0000000000..2bb807e614 --- /dev/null +++ b/guix/build-system/composer.scm @@ -0,0 +1,165 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2019 Julien Lepiller <julien@lepiller.eu> +;;; +;;; 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 composer) + #:use-module (guix store) + #:use-module (guix utils) + #:use-module (guix derivations) + #:use-module (guix search-paths) + #:use-module (guix build-system) + #:use-module (guix build-system gnu) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (gnu packages) + #:use-module (ice-9 match) + #:use-module (srfi srfi-1) + #:export (%composer-build-system-modules + lower + composer-build + composer-build-system)) + +;; Commentary: +;; +;; Standard build procedure for PHP packages using Composer. This is implemented +;; as an extension of `gnu-build-system'. +;; +;; Code: + +(define (default-php) + "Return the default PHP package." + + ;; Do not use `@' to avoid introducing circular dependencies. + (let ((module (resolve-interface '(gnu packages php)))) + (module-ref module 'php))) + +(define (default-findclass) + "Return the default findclass script." + (search-auxiliary-file "findclass.php")) + +(define (default-composer-classloader) + "Return the default composer-classloader package." + + ;; Do not use `@' to avoid introducing circular dependencies. + (let ((module (resolve-interface '(gnu packages php-xyz)))) + (module-ref module 'composer-classloader))) + +(define %composer-build-system-modules + ;; Build-side modules imported by default. + `((guix build composer-build-system) + (guix build union) + (json) + (json builder) + (json parser) + (json record) + ,@%gnu-build-system-modules)) + +(define* (lower name + #:key source inputs native-inputs outputs system target + (php (default-php)) + (composer-classloader (default-composer-classloader)) + (findclass (default-findclass)) + #:allow-other-keys + #:rest arguments) + "Return a bag for NAME." + (define private-keywords + '(#:target #:php #:composer-classloader #:findclass #:inputs #:native-inputs)) + + (and (not target) ;XXX: no cross-compilation + (bag + (name name) + (system system) + (host-inputs `(,@(if source + `(("source" ,source)) + '()) + ,@inputs + + ;; Keep the standard inputs of 'gnu-build-system'. + ,@(standard-packages))) + (build-inputs `(("php" ,php) + ("findclass.php" ,findclass) + ("composer-classloader" ,composer-classloader) + ,@native-inputs)) + (outputs outputs) + (build composer-build) + (arguments (strip-keyword-arguments private-keywords arguments))))) + +(define* (composer-build name inputs + #:key + guile source + (outputs '("out")) + (configure-flags ''()) + (search-paths '()) + (out-of-source? #t) + (composer-file "composer.json") + (tests? #t) + (test-target "test") + (test-flags ''()) + (install-target "install") + (validate-runpath? #t) + (patch-shebangs? #t) + (strip-binaries? #t) + (strip-flags #~'("--strip-debug")) + (strip-directories #~'("lib" "lib64" "libexec" + "bin" "sbin")) + (phases '(@ (guix build composer-build-system) + %standard-phases)) + (system (%current-system)) + (imported-modules %composer-build-system-modules) + (modules '((guix build composer-build-system) + (guix build utils)))) + "Build SOURCE using PHP, and with INPUTS. This assumes that SOURCE provides +a 'composer.json' file as its build system." + (define builder + (with-imported-modules imported-modules + #~(begin + (use-modules #$@(sexp->gexp modules)) + + #$(with-build-variables inputs outputs + #~(composer-build + #:source #$source + #:system #$system + #:outputs %outputs + #:inputs %build-inputs + #:search-paths '#$(map search-path-specification->sexp + search-paths) + #:phases #$phases + #:out-of-source? #$out-of-source? + #:composer-file #$composer-file + #:tests? #$tests? + #:test-target #$test-target + #:test-flags #$test-flags + #:install-target #$install-target + #:validate-runpath? #$validate-runpath? + #:patch-shebangs? #$patch-shebangs? + #:strip-binaries? #$strip-binaries? + #:strip-flags #$strip-flags + #:strip-directories #$strip-directories))))) + + (gexp->derivation name builder + #:system system + #:target #f + #:graft? #f + #:guile-for-build guile)) + +(define composer-build-system + (build-system + (name 'composer) + (description "The standard Composer build system") + (lower lower))) + +;;; composer.scm ends here diff --git a/guix/build/composer-build-system.scm b/guix/build/composer-build-system.scm new file mode 100644 index 0000000000..8896384e0a --- /dev/null +++ b/guix/build/composer-build-system.scm @@ -0,0 +1,301 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2019 Julien Lepiller <julien@lepiller.eu> +;;; Copyright © 2023 Nicolas Graves <ngraves@ngraves.fr> +;;; +;;; 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 composer-build-system) + #:use-module ((guix build gnu-build-system) #:prefix gnu:) + #:use-module (guix build utils) + #:use-module (ice-9 match) + #:use-module (json) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:export (%standard-phases + composer-build)) + +;; Commentary: +;; +;; Builder-side code of the standard composer build procedure. +;; +;; Code: + +(define (json->require dict) + (if dict + (let loop ((result '()) (require dict)) + (match require + (() result) + ((((? (cut string-contains <> "/") name) . _) + require ...) + (loop (cons name result) require)) + ((_ require ...) (loop result require)) + (_ result))) + '())) + +(define (if-specified-to-list fn) + (match-lambda + ((? unspecified?) '()) + (arg (fn arg)) + (_ '()))) + +(define-json-mapping <composer-autoload> make-composer-autoload + composer-autoload? + json->composer-autoload + (psr-4 composer-autoload-psr-4 "psr-4" + (match-lambda + ((? unspecified?) '()) + ((? (lambda (al) + (and (list? al) (pair? (car al)) (vector? (cdar al)))) al) + (append-map + (lambda (vect-el) + (list (cons (caar al) vect-el))) + (vector->list (cdar al)))) + ((? list? l) l) + (_ '()))) + (psr-0 composer-autoload-psr-0 "psr-0" (if-specified-to-list identity)) + (classmap composer-autoload-classmap "classmap" + (if-specified-to-list vector->list)) + (files composer-autoload-files "files" + (if-specified-to-list vector->list))) + +(define-json-mapping <composer-package> make-composer-package composer-package? + json->composer-package + (name composer-package-name) + (autoload composer-package-autoload "autoload" + (if-specified-to-list json->composer-autoload)) + (autoload-dev composer-package-autoload-dev "autoload-dev" + (if-specified-to-list json->composer-autoload)) + (require composer-package-require "require" json->require) + (dev-require composer-package-dev-require "require-dev" json->require) + (scripts composer-package-scripts "scripts" + (if-specified-to-list identity)) + (binaries composer-package-binaries "bin" + (if-specified-to-list vector->list))) + +(define* (read-package-data #:key (filename "composer.json")) + (call-with-input-file filename + (lambda (port) + (json->composer-package (json->scm port))))) + +(define* (create-test-autoload #:key composer-file inputs outputs tests? + #:allow-other-keys) + "Create the autoload.php file for tests. This is a standalone phase so that +the autoload.php file can be edited before the check phase." + (when tests? + (mkdir-p "vendor") + (create-autoload (string-append (getcwd) "/vendor") composer-file + inputs #:dev-dependencies? #t))) + +(define (find-bin script inputs) + (search-input-file inputs + (string-append + "bin/" + (string-drop script (string-length "vendor/bin/"))))) + +(define* (check #:key composer-file inputs + tests? test-target test-flags #:allow-other-keys) + "Test the given package. +Please note that none of the PHP packages at the time of the rewrite of the +build-system did use the test-script field. This means that the @code{match +test-script} part is not tested on a real example and relies on the original +implementation." + (if tests? + (let* ((package-data (read-package-data #:filename composer-file)) + (scripts (composer-package-scripts package-data)) + (test-script (assoc-ref scripts test-target))) + (match test-script + ((? string? bin) + (let ((command (find-bin bin inputs))) + (unless (zero? (apply system command test-flags)) + (throw 'failed-command command)))) + (('@ (? string? bins) ...) + (for-each + (lambda (c) + (let ((command (find-bin c inputs))) + (unless (zero? (apply system command test-flags)) + (throw 'failed-command command)))) + bins)) + (_ (if (file-exists? "phpunit.xml.dist") + (apply invoke + (with-exception-handler + (lambda (exn) + (if (search-error? exn) + (error "\ +Missing php-phpunit-phpunit native input.~%") + (raise exn))) + (lambda () + (search-input-file (or inputs '()) "bin/phpunit"))) + test-flags)) + (format #t "No test suite found.~%")))) + (format #t "Test suite not run.~%"))) + +(define* (create-autoload vendor composer-file inputs #:key dev-dependencies?) + "creates an autoload.php file that sets up the class locations for this package, +so it can be autoloaded by PHP when the package classes are required." + (with-output-to-file (string-append vendor "/autoload.php") + (lambda _ + (display (string-append + "<?php +// autoload.php @generated by Guix +$psr4map = $classmap = array(); +require_once '" vendor "/autoload_conf.php'; +require_once '" (assoc-ref inputs "composer-classloader") "/share/web/composer/ClassLoader.php'; +$loader = new \\Composer\\Autoload\\ClassLoader(); +foreach ($psr4map as $namespace => $paths) { + foreach ($paths as $path) { + $loader->addPsr4($namespace, $path); + } +} +$loader->addClassMap($classmap); +$loader->register(); +")))) + ;; Now, create autoload_conf.php that contains the actual data, as a set + ;; of arrays + (let* ((package-data (read-package-data #:filename composer-file)) + (autoload (composer-package-autoload package-data)) + (autoload-dev (composer-package-autoload-dev package-data)) + (dependencies (composer-package-require package-data)) + (dependencies-dev (composer-package-dev-require package-data))) + (with-output-to-file (string-append vendor "/autoload_conf.php") + (lambda _ + (format #t "<?php~%") + (format #t "// autoload_conf.php @generated by Guix~%") + (force-output) + (for-each + (match-lambda + ((key . value) + (let ((vals (if (list? value) + (reverse value) + (list value)))) + (apply + format + #t + (string-append + "$psr4map['~a'][] = [" + (string-join + (make-list (length vals) "'~a/../~a'") ",") + "];~%") + (cons* (string-join (string-split key #\\) "\\\\") + (append-map (lambda (v) (list vendor v)) vals))))) + (_ (format #t ""))) + (delete-duplicates + (append + (composer-autoload-psr-4 autoload) + (if (and dev-dependencies? (not (null? autoload-dev))) + (composer-autoload-psr-4 autoload-dev) + '())) + '())) + (for-each + (lambda (psr0) + (match psr0 + ((key . value) + (format #t "$psr4map['~a'][] = ['~a/../~a/~a'];~%" + (string-join (string-split key #\\) "\\\\") + vendor + value + (string-join (string-split key #\\) "/"))) + (_ (format #t "")))) + (append + (composer-autoload-psr-0 autoload) + (if (and dev-dependencies? (not (null? autoload-dev))) + (composer-autoload-psr-0 autoload-dev) + '()))) + (for-each + (lambda (classmap) + (for-each + (lambda (file) + (invoke "php" (assoc-ref inputs "findclass.php") + "-i" (string-append vendor "/..") "-f" file)) + (find-files classmap ".(php|hh|inc)$"))) + (append + (composer-autoload-classmap autoload) + (if (and dev-dependencies? (not (null? autoload-dev))) + (composer-autoload-classmap autoload-dev) + '()))) + (for-each + (lambda (file) + (format #t "require_once '~a/../~a';~%" vendor file)) + (append + (composer-autoload-files autoload) + (if (and dev-dependencies? (not (null? autoload-dev))) + (composer-autoload-files autoload-dev) + '()))) + (for-each + (lambda (dep) + (format + #t "require_once '~a';~%" + (search-input-file + inputs + (string-append "/share/web/" dep "/vendor/autoload_conf.php")))) + dependencies) + ;; Also add native-inputs that are not necessarily given in the + ;; composer.json. This allows to simply add a package in tests by + ;; adding it in native-inputs, without the need to patch composer.json. + (for-each + (match-lambda + ((name . loc) + (match (find-files loc "autoload_conf\\.php$") + (() #t) + (((? string? conf) . ()) + (format #t "require_once '~a';~%" conf)) + (_ #t))) + (_ #t)) + (or inputs '())))))) + +(define* (install #:key inputs outputs composer-file #:allow-other-keys) + "Install the given package." + (let* ((out (assoc-ref outputs "out")) + (package-data (read-package-data #:filename composer-file)) + (name (composer-package-name package-data)) + (php-dir (string-append out "/share/web/" name)) + (bin-dir (string-append php-dir "/vendor/bin")) + (bin (string-append out "/bin")) + (binaries (composer-package-binaries package-data))) + (mkdir-p php-dir) + (copy-recursively "." php-dir) + (mkdir-p (string-append php-dir "/vendor")) + (when binaries + (mkdir-p bin-dir) + (mkdir-p bin) + (for-each + (lambda (file) + (let ((installed-file (string-append bin-dir "/" (basename file))) + (bin-file (string-append bin "/" (basename file))) + (original-file (string-append php-dir "/" file))) + (symlink original-file installed-file) + (symlink original-file bin-file))) + binaries)) + (create-autoload (string-append php-dir "/vendor") + composer-file inputs))) + +(define %standard-phases + ;; Everything is as with the GNU Build System except for the `configure' + ;; , `build', `check' and `install' phases. + (modify-phases gnu:%standard-phases + (delete 'bootstrap) + (delete 'configure) + (delete 'build) + (delete 'check) + (replace 'install install) + (add-after 'install 'check check) + (add-after 'install 'create-test-autoload create-test-autoload))) + +(define* (composer-build #:key inputs (phases %standard-phases) + #:allow-other-keys #:rest args) + "Build the given package, applying all of PHASES in order." + (apply gnu:gnu-build #:inputs inputs #:phases phases args)) + +;;; composer-build-system.scm ends here |