Package: guix-patches;
Reported by: Julien Lepiller <julien <at> lepiller.eu>
Date: Sun, 12 Jul 2020 22:22:02 UTC
Severity: normal
Tags: patch
Done: Steve George <steve <at> futurile.net>
Bug is archived. No further changes may be made.
View this message in rfc822 format
From: Julien Lepiller <julien <at> lepiller.eu> To: 42338 <at> debbugs.gnu.org Subject: [bug#42338] [PATCH 03/34] guix: Add composer-build-system. Date: Mon, 13 Jul 2020 00:25:07 +0200
* guix/build-system/composer.scm: New file. * guix/build/composer-build-system.scm: New file. * guix/build-system/findclass.php: New file. * Makefile.am: Add them. * doc/guix.texi (Build Systems): Document it. --- Makefile.am | 2 + doc/guix.texi | 14 ++ guix/build-system/composer.scm | 169 ++++++++++++++++++++ guix/build-system/findclass.php | 102 ++++++++++++ guix/build/composer-build-system.scm | 224 +++++++++++++++++++++++++++ 5 files changed, 511 insertions(+) create mode 100644 guix/build-system/composer.scm create mode 100644 guix/build-system/findclass.php create mode 100644 guix/build/composer-build-system.scm diff --git a/Makefile.am b/Makefile.am index 623ddf32b2..363af872fa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -117,6 +117,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 \ @@ -164,6 +165,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 \ diff --git a/doc/guix.texi b/doc/guix.texi index 17338ed764..0613f669a0 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -6687,6 +6687,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/guix/build-system/composer.scm b/guix/build-system/composer.scm new file mode 100644 index 0000000000..19455b1920 --- /dev/null +++ b/guix/build-system/composer.scm @@ -0,0 +1,169 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2019 Julien Lepiller <julien <at> 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 (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." + (local-file (string-append (current-source-directory) "/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-system/findclass.php b/guix/build-system/findclass.php new file mode 100644 index 0000000000..a9704f809c --- /dev/null +++ b/guix/build-system/findclass.php @@ -0,0 +1,102 @@ +<?php +/** + * 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/composer-build-system.scm b/guix/build/composer-build-system.scm new file mode 100644 index 0000000000..3578b91954 --- /dev/null +++ b/guix/build/composer-build-system.scm @@ -0,0 +1,224 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2019 Julien Lepiller <julien <at> 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 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 _ + (format #t "<?php~%") + (format #t "// autoload.php @generated by Guix~%") + (format #t "$map = $psr4map = $classmap = array();~%") + (format #t "require_once '~a/autoload_conf.php';~%" vendor) + (format #t "require_once '~a/share/web/composer/ClassLoader.php';~%" + (assoc-ref inputs "composer-classloader")) + (format #t "$loader = new \\Composer\\Autoload\\ClassLoader();~%") + (format #t "foreach ($map as $namespace => $path) {~%") + (format #t " $loader->set($namespace, $path);~%") + (format #t "}~%") + (format #t "foreach ($psr4map as $namespace => $path) {~%") + (format #t " $loader->setPsr4($namespace, $path);~%") + (format #t "}~%") + (format #t "$loader->addClassMap($classmap);~%") + (format #t "$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 "<?php~%") + (format #t "// autoload_conf.php @generated by Guix~%") + (force-output) + (for-each + (lambda (psr4) + (match psr4 + ((key . value) + (format #t "$psr4map['~a'] = '~a/../~a';~%" + (string-join (string-split key #\\) "\\\\") + vendor value)))) + (append + (match (assoc-ref autoload "psr-4") + (('@ psr4 ...) psr4) + (#f '())) + (if dev-dependencies? + (match (assoc-ref autoload-dev "psr-4") + (('@ psr4 ...) psr4) + (#f '())) + '()))) + (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 + (or (assoc-ref autoload "classmap") '()) + (if dev-dependencies? + (or (assoc-ref autoload-dev "classmap") '()) + '()))) + (for-each + (lambda (dep) + (format #t "require_once '~a';~%" (find-php-dep inputs dep))) + (append + dependencies + (if dev-dependencies? + dependencies-dev + '()))))))) + +(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 (assoc-ref package-data "name")) + (php-dir (string-append out "/share/web/" name)) + (bin-dir (string-append php-dir "/vendor/bin")) + (bin (string-append out "/bin")) + (binaries (assoc-ref package-data "bin"))) + (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)) + #t) + +(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))) + +(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 -- 2.27.0
GNU bug tracking system
Copyright (C) 1999 Darren O. Benham,
1997,2003 nCipher Corporation Ltd,
1994-97 Ian Jackson.