;;; 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 import composer) #:use-module (ice-9 match) #:use-module (json) #:use-module (gcrypt hash) #:use-module (guix base32) #:use-module (guix build git) #:use-module (guix build utils) #:use-module (guix build-system) #:use-module (guix import json) #:use-module (guix import utils) #:use-module ((guix licenses) #:prefix license:) #:use-module (guix memoization) #:use-module (guix packages) #:use-module (guix serialization) #:use-module (guix upstream) #:use-module (guix utils) #:use-module (srfi srfi-1) #:use-module (srfi srfi-11) #:use-module (srfi srfi-26) #:export (composer->guix-package %composer-updater composer-recursive-import %composer-base-url)) (define %composer-base-url (make-parameter "https://repo.packagist.org")) ;; XXX adapted from (guix scripts hash) (define (file-hash file select? recursive?) ;; Compute the hash of FILE. (if recursive? (let-values (((port get-hash) (open-sha256-port))) (write-file file port #:select? select?) (force-output port) (get-hash)) (call-with-input-file file port-sha256))) ;; XXX taken from (guix scripts hash) (define (vcs-file? file stat) (case (stat:type stat) ((directory) (member (basename file) '(".bzr" ".git" ".hg" ".svn" "CVS"))) ((regular) ;; Git sub-modules have a '.git' file that is a regular text file. (string=? (basename file) ".git")) (else #f))) (define (fix-version version) "Return a fixed version from a version string. For instance, v10.1 -> 10.1" (cond ((string-prefix? "version" version) (if (char-set-contains? char-set:digit (string-ref version 7)) (substring version 7) (substring version 8))) ((string-prefix? "v" version) (substring version 1)) (else version))) (define (latest-version versions) (fold (lambda (a b) (if (version>? (fix-version a) (fix-version b)) a b)) (car versions) versions)) (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-json-mapping make-composer-source composer-source? json->composer-source (type composer-source-type) (url composer-source-url) (reference composer-source-reference)) (define-json-mapping make-composer-package composer-package? json->composer-package (description composer-package-description) (homepage composer-package-homepage) (source composer-package-source "source" json->composer-source) (name composer-package-name "name" php-package-name) (version composer-package-version "version" fix-version) (require composer-package-require "require" json->require) (dev-require composer-package-dev-require "require-dev" json->require) (license composer-package-license "license" (lambda (vector) (map string->license (vector->list vector))))) (define* (composer-fetch name #:optional version) "Return an alist representation of the Composer metadata for the package NAME, or #f on failure." (let ((package (json-fetch (string-append (%composer-base-url) "/p/" name ".json")))) (if package (let* ((packages (assoc-ref package "packages")) (package (or (assoc-ref packages name) package)) (versions (filter (lambda (version) (and (not (string-contains version "dev")) (not (string-contains version "beta")))) (map car package))) (version (or (if (null? version) #f version) (latest-version versions)))) (assoc-ref package version)) #f))) (define (php-package-name name) "Given the NAME of a package on Packagist, return a Guix-compliant name for the package." (let ((name (string-join (string-split name #\/) "-"))) (if (string-prefix? "php-" name) (snake-case name) (string-append "php-" (snake-case name))))) (define (make-php-sexp composer-package) "Return the `package' s-expression for a PHP package for the given COMPOSER-PACKAGE." (let* ((source (composer-package-source composer-package)) (dependencies (map php-package-name (composer-package-require composer-package))) (dev-dependencies (map php-package-name (composer-package-dev-require composer-package))) (git? (equal? (composer-source-type source) "git"))) ((if git? call-with-temporary-directory call-with-temporary-output-file) (lambda* (temp #:optional port) (and (if git? (begin (mkdir-p temp) (git-fetch (composer-source-url source) (composer-source-reference source) temp)) (url-fetch (composer-source-url source) temp)) `(package (name ,(composer-package-name composer-package)) (version ,(composer-package-version composer-package)) (source (origin ,@(if git? `((method git-fetch) (uri (git-reference (url ,(composer-source-url source)) (commit ,(composer-source-reference source)))) (file-name (git-file-name name version)) (sha256 (base32 ,(bytevector->nix-base32-string (file-hash temp (negate vcs-file?) #t))))) `((method url-fetch) (uri ,(composer-source-url source)) (sha256 (base32 ,(guix-hash-url temp))))))) (build-system composer-build-system) ,@(if (null? dependencies) '() `((inputs (list ,(map string->symbol dependencies))))) ,@(if (null? dev-dependencies) '() `((native-inputs (list ,(map string->symbol dev-dependencies))))) (synopsis "") (description ,(composer-package-description composer-package)) (home-page ,(composer-package-homepage composer-package)) (license ,(or (composer-package-license composer-package) 'unknown-license!)))))))) (define composer->guix-package (memoize (lambda* (package-name #:key version #:allow-other-keys) "Fetch the metadata for PACKAGE-NAME from packagist.org, and return the `package' s-expression corresponding to that package, or #f on failure." (let ((package (composer-fetch package-name version))) (and package (let* ((package (json->composer-package package)) (dependencies-names (composer-package-require package)) (dev-dependencies-names (composer-package-dev-require package))) (values (make-php-sexp package) (append dependencies-names dev-dependencies-names)))))))) (define (guix-name->composer-name name) "Given a guix package name, return the name of the package in Packagist." (if (string-prefix? "php-" name) (let ((components (string-split (substring name 4) #\-))) (match components ((namespace name ...) (string-append namespace "/" (string-join name "-"))))) name)) (define (guix-package->composer-name package) "Given a Composer PACKAGE built from Packagist, return the name of the package in Packagist." (let ((upstream-name (assoc-ref (package-properties package) 'upstream-name)) (name (package-name package))) (if upstream-name upstream-name (guix-name->composer-name name)))) (define (string->license str) "Convert the string STR into a license object." (or (spdx-string->license str) (match str ("GNU LGPL" 'license:lgpl2.0) ("GPL" 'license:gpl3) ((or "BSD" "BSD License") 'license:bsd-3) ((or "MIT" "MIT license" "Expat license") 'license:expat) ("Public domain" 'license:public-domain) ((or "Apache License, Version 2.0" "Apache 2.0") 'license:asl2.0) (_ 'unknown-license!)))) (define (php-package? package) "Return true if PACKAGE is a PHP package from Packagist." (and (eq? (build-system-name (package-build-system package)) 'composer) (string-prefix? "php-" (package-name package)))) (define (latest-release package) "Return an for the latest release of PACKAGE." (let* ((php-name (guix-package->composer-name package)) (metadata (composer-fetch php-name)) (package (json->composer-package metadata)) (version (composer-package-version package)) (url (composer-source-url (composer-package-source package)))) (upstream-source (package (package-name package)) (version version) (urls (list url))))) (define %composer-updater (upstream-updater (name 'composer) (description "Updater for Composer packages") (pred php-package?) (import latest-release))) (define* (composer-recursive-import package-name #:optional version) (recursive-import package-name #:version version #:repo->guix-package composer->guix-package #:guix-name php-package-name))