From bb5d102b6ea5e6b5c06bbf90a58927c6180e23bc Mon Sep 17 00:00:00 2001 From: Julien Lepiller Date: Tue, 29 Oct 2019 20:58:51 +0100 Subject: [PATCH 03/34] guix: 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. --- Makefile.am | 5 +- doc/guix.texi | 14 ++ gnu/packages/aux-files/findclass.php | 125 +++++++++++++++ guix/build-system/composer.scm | 170 ++++++++++++++++++++ guix/build/composer-build-system.scm | 226 +++++++++++++++++++++++++++ 5 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 gnu/packages/aux-files/findclass.php create mode 100644 guix/build-system/composer.scm create mode 100644 guix/build/composer-build-system.scm diff --git a/Makefile.am b/Makefile.am index 6ce1430ea6..5af964b0e9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -115,6 +115,7 @@ MODULES = \ guix/build-system/cargo.scm \ guix/build-system/clojure.scm \ guix/build-system/cmake.scm \ + guix/build-system/composer.scm \ guix/build-system/dub.scm \ guix/build-system/dune.scm \ guix/build-system/emacs.scm \ @@ -163,6 +164,7 @@ MODULES = \ guix/build/cargo-build-system.scm \ guix/build/cargo-utils.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/emacs-build-system.scm \ @@ -354,7 +356,8 @@ AUX_FILES = \ gnu/packages/aux-files/linux-libre/4.4-i686.conf \ gnu/packages/aux-files/linux-libre/4.4-x86_64.conf \ gnu/packages/aux-files/pack-audit.c \ - gnu/packages/aux-files/run-in-namespace.c + gnu/packages/aux-files/run-in-namespace.c \ + gnu/packages/aux-files/findclass.php # Templates, examples. EXAMPLES = \ diff --git a/doc/guix.texi b/doc/guix.texi index ca4eb347c7..6ee4d7e5f0 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -6941,6 +6941,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 defvr +@defvr {Scheme Variable} 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 defvr + @defvr {Scheme Variable} 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 @@ + + * Jordi Boggiano + * + * 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 + * + * To the extent to wich it makes sense, as the author of the extract: + * Copyright © 2020 Julien Lepiller + */ + +/** + * 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) !== '(?:[^<]++|<(?!\?))*+<\?}s', '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface'.$extraTypes.') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?])(?Pnamespace) (?P\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..ebc472c717 --- /dev/null +++ b/guix/build-system/composer.scm @@ -0,0 +1,170 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2019 Julien Lepiller +;;; +;;; 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 . +(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 json) + (guix build union) + ,@%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 + '(#:source #: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 store name inputs + #:key (guile #f) + (outputs '("out")) (configure-flags ''()) + (search-paths '()) + (out-of-source? #t) + (composer-file "composer.json") + (tests? #t) + (test-target "test") + (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 json) + (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 + `(begin + (use-modules ,@modules) + (composer-build #:source ,(match (assoc-ref inputs "source") + (((? derivation? source)) + (derivation->output-path source)) + ((source) + source) + (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 + #:install-target ,install-target + #:validate-runpath? ,validate-runpath? + #:patch-shebangs? ,patch-shebangs? + #:strip-binaries? ,strip-binaries? + #:strip-flags ,strip-flags + #:strip-directories ,strip-directories))) + + (define guile-for-build + (match guile + ((? package?) + (package-derivation store guile system #:graft? #f)) + (#f ; the default + (let* ((distro (resolve-interface '(gnu packages commencement))) + (guile (module-ref distro 'guile-final))) + (package-derivation store guile system #:graft? #f))))) + + (build-expression->derivation store name builder + #:system system + #:inputs inputs + #:modules imported-modules + #:outputs outputs + #:guile-for-build guile-for-build)) + +(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..f73684f8d5 --- /dev/null +++ b/guix/build/composer-build-system.scm @@ -0,0 +1,226 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2019 Julien Lepiller +;;; +;;; 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 . + +(define-module (guix build composer-build-system) + #:use-module ((guix build gnu-build-system) #:prefix gnu:) + #:use-module (guix build json) + #:use-module (guix build utils) + #:use-module (ice-9 match) + #:export (%standard-phases + composer-build)) + +;; Commentary: +;; +;; Builder-side code of the standard composer build procedure. +;; +;; Code: + +(define* (read-package-data #:key (filename "composer.json")) + (call-with-input-file filename + (lambda (port) + (read-json port)))) + +(define* (check #:key composer-file inputs outputs tests? test-target #:allow-other-keys) + "Install the given package." + (when tests? + (mkdir-p "vendor") + (create-autoload (string-append (getcwd) "/vendor") composer-file + (append inputs outputs) #:dev-dependencies? #t) + (let* ((package-data (read-package-data #:filename composer-file)) + (scripts (match (assoc-ref package-data "scripts") + (('@ script ...) script) + (#f '()))) + (test-script + (assoc-ref scripts test-target)) + (dependencies (filter (lambda (dep) (string-contains dep "/")) + (map car + (match (assoc-ref package-data "require") + (('@ dependency ...) dependency) + (#f '()))))) + (dependencies-dev + (filter (lambda (dep) (string-contains dep "/")) + (map car + (match (assoc-ref package-data "require-dev") + (('@ dependency ...) dependency) + (#f '()))))) + (name (assoc-ref package-data "name"))) + (for-each + (lambda (input) + (let ((bin (find-php-bin (cdr input)))) + (when bin + (copy-recursively bin "vendor/bin")))) + inputs) + (match test-script + ((? string? command) + (unless (equal? (system command) 0) + (throw 'failed-command command))) + (('@ (? string? command) ...) + (for-each + (lambda (c) + (unless (equal? (system c) 0) + (throw 'failed-command c))) + command)) + (#f (invoke "vendor/bin/phpunit"))))) + #t) + +(define (find-php-bin input) + (let* ((web-dir (string-append input "/share/web")) + (vendors (if (file-exists? web-dir) + (find-files web-dir "^vendor$" #:directories? #t) + #f))) + (match vendors + ((vendor) + (let ((bin (string-append vendor "/bin"))) + (and (file-exists? bin) bin))) + (_ #f)))) + +(define (find-php-dep inputs dependency) + (let loop ((inputs (map cdr inputs))) + (if (null? inputs) + (throw 'unsatisfied-dependency "Unsatisfied dependency: required " dependency) + (let ((autoload (string-append (car inputs) "/share/web/" dependency "/vendor/autoload_conf.php"))) + (if (file-exists? autoload) + autoload + (loop (cdr inputs))))))) + +(define* (create-autoload vendor composer-file inputs #:key dev-dependencies?) + (with-output-to-file (string-append vendor "/autoload.php") + (lambda _ + (display " $path) { + $loader->set($namespace, $path); +} +foreach ($psr4map as $namespace => $path) { + $loader->setPsr4($namespace, $path); +} +$loader->addClassMap($classmap); +$loader->register(); +"))) + (let* ((package-data (read-package-data #:filename composer-file)) + (autoload + (match (assoc-ref package-data "autoload") + (('@ autoload ...) autoload) + (#f '()))) + (autoload-dev + (match (assoc-ref package-data "autoload-dev") + (('@ autoload-dev ...) autoload-dev) + (#f '()))) + (dependencies (filter (lambda (dep) (string-contains dep "/")) + (map car + (match (assoc-ref package-data "require") + (('@ dependency ...) dependency) + (#f '()))))) + (dependencies-dev + (filter (lambda (dep) (string-contains dep "/")) + (map car + (match (assoc-ref package-data "require-dev") + (('@ dependency ...) dependency) + (#f '())))))) + (with-output-to-file (string-append vendor "/autoload_conf.php") + (lambda _ + (format #t "