Browse Source

Use of SPDX license identifiers.

Tom Klingenberg 13 years ago
parent
commit
74ca58bcb6

+ 85 - 0
bin/fetch-spdx-identifier

@@ -0,0 +1,85 @@
+#!/usr/bin/env php
+<?php
+
+$identifiers = new SPDXLicenseIdentifiersOnline;
+$printer = new JsonPrinter;
+$printer->printStringArray($identifiers->getStrings());
+
+/**
+ * SPDX Identifier List from the registry.
+ */
+class SPDXLicenseIdentifiersOnline
+{
+    const REGISTRY = 'http://www.spdx.org/licenses/';
+    const EXPRESSION = '//*[@typeof="spdx:License"]/code[@property="spdx:licenseId"]/text()';
+
+    private $identifiers;
+
+    /**
+     * @return string[]
+     */
+    public function getStrings()
+    {
+        if ($this->identifiers) {
+            return $this->identifiers;
+        }
+        $this->identifiers = $this->importNodesFromURL(
+            self::REGISTRY,
+            self::EXPRESSION
+        );
+
+        return $this->identifiers;
+    }
+
+    private function importNodesFromURL($url, $expressionTextNodes)
+    {
+        $doc = new DOMDocument();
+        $doc->loadHTMLFile($url);
+        $xp = new DOMXPath($doc);
+        $codes = $xp->query($expressionTextNodes);
+        if (!$codes) {
+            throw new \Exception(sprintf('XPath query failed: %s', $expressionTextNodes));
+        }
+        if ($codes->length < 20) {
+            throw new \Exception('Obtaining the license table failed, there can not be less than 20 identifiers.');
+        }
+        $identifiers = array();
+        foreach ($codes as $code) {
+            $identifiers[] = $code->nodeValue;
+        }
+
+        return $identifiers;
+    }
+}
+
+/**
+ * Print an array the way this script needs it.
+ */
+class JsonPrinter
+{
+    /**
+     *
+     * @param string[] $array
+     */
+    public function printStringArray(array $array)
+    {
+        $lines = array('');
+        $line = &$lines[0];
+        $last = count($array) - 1;
+        foreach ($array as $item => $code) {
+            $code = sprintf('"%s"%s', $code, $item === $last ? '' : ', ');
+            $length = strlen($line) + strlen($code) - 1;
+            if ($length > 76) {
+                $line = rtrim($line);
+                unset($line);
+                $lines[] = $code;
+                $line = &$lines[count($lines) - 1];
+            } else {
+                $line .= $code;
+            }
+        }
+        $json = sprintf("[%s]", implode("\n    ", $lines));
+        $json = str_replace(array("[\"", "\"]"), array("[\n    \"", "\"\n]"), $json);
+        echo $json;
+    }
+}

+ 45 - 13
doc/04-schema.md

@@ -121,20 +121,52 @@ Optional.
 
 The license of the package. This can be either a string or an array of strings.
 
-The recommended notation for the most common licenses is:
-
+The recommended notation for the most common licenses is (alphabetical):
+
+    Apache-2.0
+    BSD-2-Clause
+    BSD-3-Clause
+    BSD-4-Clause
+    GPL-2.0
+    GPL-2.0+
+    GPL-3.0
+    GPL-3.0+
+    LGPL-2.0
+    LGPL-2.0+
+    LGPL-3.0
+    LGPL-3.0+
     MIT
-    BSD-2
-    BSD-3
-    BSD-4
-    GPLv2
-    GPLv3
-    LGPLv2
-    LGPLv3
-    Apache2
-    WTFPL
-
-Optional, but it is highly recommended to supply this.
+
+Optional, but it is highly recommended to supply this. More identifiers are
+listed at the [SPDX Open Source License Registry](http://www.spdx.org/licenses/).
+
+An Example:
+
+    {
+        "license": "MIT"
+    }
+
+
+For a package, when there is a choice between licenses (“disjunctive license”),
+multiple can be specified as array.
+
+An Example for disjunctive licenses:
+
+    {
+        "license": [
+           "LGPL-2.0",
+           "GPL-3.0+"
+        ]
+    }
+
+Alternatively they can be separated with “or” and enclosed in brackets;
+
+    {
+        "license": "(LGPL-2.0 or GPL-3.0+)"
+    }
+
+Similarly when multiple licenses need to be applied (“conjunctive license”),
+they should be separated with “and” and enclosed in brackets.
 
 ### authors
 

+ 34 - 0
res/spdx-identifier.json

@@ -0,0 +1,34 @@
+[
+    "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "APL-1.0",
+    "ANTLR-PD", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APSL-1.0",
+    "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0", "Artistic-2.0", "AAL",
+    "BSL-1.0", "BSD-2-Clause", "BSD-2-Clause-NetBSD", "BSD-2-Clause-FreeBSD",
+    "BSD-3-Clause", "BSD-4-Clause", "BSD-4-Clause-UC", "CECILL-1.0",
+    "CECILL-1.1", "CECILL-2.0", "CECILL-B", "CECILL-C", "ClArtistic",
+    "CNRI-Python-GPL-Compatible", "CNRI-Python", "CDDL-1.0", "CDDL-1.1",
+    "CPAL-1.0", "CPL-1.0", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5",
+    "CC-BY-3.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", "CC-BY-ND-3.0",
+    "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", "CC-BY-NC-3.0",
+    "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0",
+    "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5", "CC-BY-NC-SA-3.0",
+    "CC-BY-SA-1.0", "CC-BY-SA-2.0", "CC-BY-SA-2.5", "CC-BY-SA-3.0", "CC0-1.0",
+    "CUA-OPL-1.0", "EPL-1.0", "eCos-2.0", "ECL-1.0", "ECL-2.0", "EFL-1.0",
+    "EFL-2.0", "Entessa", "ErlPL-1.1", "EUDatagrid", "EUPL-1.0", "EUPL-1.1",
+    "Fair", "Frameworx-1.0", "AGPL-3.0", "GFDL-1.1", "GFDL-1.2", "GFDL-1.3",
+    "GPL-1.0", "GPL-1.0+", "GPL-2.0", "GPL-2.0+",
+    "GPL-2.0-with-autoconf-exception", "GPL-2.0-with-bison-exception",
+    "GPL-2.0-with-classpath-exception", "GPL-2.0-with-font-exception",
+    "GPL-2.0-with-GCC-exception", "GPL-3.0", "GPL-3.0+",
+    "GPL-3.0-with-autoconf-exception", "GPL-3.0-with-GCC-exception", "LGPL-2.1",
+    "LGPL-2.1+", "LGPL-3.0", "LGPL-3.0+", "LGPL-2.0", "LGPL-2.0+", "gSOAP-1.3b",
+    "HPND", "IPL-1.0", "IPA", "ISC", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2",
+    "LPPL-1.3c", "Libpng", "LPL-1.0", "LPL-1.02", "MS-PL", "MS-RL", "MirOS",
+    "MIT", "Motosoto", "MPL-1.0", "MPL-1.1", "MPL-2.0", "Multics", "NASA-1.3",
+    "Naumen", "NGPL", "Nokia", "NPOSL-3.0", "NTP", "OCLC-2.0", "ODbL-1.0",
+    "PDDL-1.0", "OGTSL", "OSL-1.0", "OSL-2.0", "OSL-2.1", "OSL-3.0",
+    "OLDAP-2.8", "OpenSSL", "PHP-3.0", "PHP-3.01", "PostgreSQL", "Python-2.0",
+    "QPL-1.0", "RPSL-1.0", "RPL-1.5", "RHeCos-1.1", "RSCPL", "Ruby", "SAX-PD",
+    "OFL-1.0", "OFL-1.1", "SimPL-2.0", "Sleepycat", "SugarCRM-1.1.3", "SPL-1.0",
+    "Watcom-1.0", "NCSA", "VSL-1.0", "W3C", "WXwindows", "Xnet", "XFree86-1.1",
+    "YPL-1.0", "YPL-1.1", "Zimbra-1.3", "Zlib", "ZPL-1.1", "ZPL-2.0", "ZPL-2.1"
+]

+ 43 - 13
src/Composer/Command/ValidateCommand.php

@@ -18,67 +18,97 @@ use Symfony\Component\Console\Output\OutputInterface;
 use Composer\Json\JsonFile;
 use Composer\Json\JsonValidationException;
 use Composer\Util\RemoteFilesystem;
+use Composer\Util\SPDXLicenseIdentifier;
 
 /**
+ * ValidateCommand
+ *
  * @author Robert Schönthal <seroscho@googlemail.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
  */
 class ValidateCommand extends Command
 {
+    /**
+     * configure
+     */
     protected function configure()
     {
         $this
             ->setName('validate')
             ->setDescription('Validates a composer.json')
             ->setDefinition(array(
-                new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json')
-            ))
+            new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json')
+        ))
             ->setHelp(<<<EOT
 The validate command validates a given composer.json
 
 EOT
-            )
-        ;
+            );
     }
 
+    /**
+     * @param \Symfony\Component\Console\Input\InputInterface $input
+     * @param \Symfony\Component\Console\Output\OutputInterface $output
+     *
+     * @return int
+     */
     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $file = $input->getArgument('file');
 
         if (!file_exists($file)) {
-            $output->writeln('<error>'.$file.' not found.</error>');
+            $output->writeln('<error>' . $file . ' not found.</error>');
+
             return 1;
         }
         if (!is_readable($file)) {
-            $output->writeln('<error>'.$file.' is not readable.</error>');
+            $output->writeln('<error>' . $file . ' is not readable.</error>');
+
             return 1;
         }
 
         $laxValid = false;
         try {
             $json = new JsonFile($file, new RemoteFilesystem($this->getIO()));
-            $json->read();
+            $manifest = $json->read();
 
             $json->validateSchema(JsonFile::LAX_SCHEMA);
             $laxValid = true;
             $json->validateSchema();
         } catch (JsonValidationException $e) {
             if ($laxValid) {
-                $output->writeln('<info>'.$file.' is valid for simple usage with composer but has</info>');
+                $output->writeln('<info>' . $file . ' is valid for simple usage with composer but has</info>');
                 $output->writeln('<info>strict errors that make it unable to be published as a package:</info>');
             } else {
-                $output->writeln('<error>'.$file.' is invalid, the following errors were found:</error>');
+                $output->writeln('<error>' . $file . ' is invalid, the following errors were found:</error>');
             }
             foreach ($e->getErrors() as $message) {
-                $output->writeln('<error>'.$message.'</error>');
+                $output->writeln('<error>' . $message . '</error>');
             }
+
             return 1;
         } catch (\Exception $e) {
-            $output->writeln('<error>'.$file.' contains a JSON Syntax Error:</error>');
-            $output->writeln('<error>'.$e->getMessage().'</error>');
+            $output->writeln('<error>' . $file . ' contains a JSON Syntax Error:</error>');
+            $output->writeln('<error>' . $e->getMessage() . '</error>');
+
             return 1;
         }
 
-        $output->writeln('<info>'.$file.' is valid</info>');
+        if (isset($manifest['license'])) {
+            try {
+                $identifier = new SPDXLicenseIdentifier($manifest['license']);
+            } catch (\InvalidArgumentException $e) {
+                $output->writeln(sprintf(
+                    '<warning>License "%s" is not a SPDX license identifier.</warning>',
+                    print_r($manifest['license'], true)
+                ));
+            }
+        } else {
+            $output->writeln('<warning>No license specified.</warning>');
+        }
+
+        $output->writeln('<info>' . $file . ' is valid</info>');
+
+        return 0;
     }
 }

+ 12 - 3
src/Composer/Compiler.php

@@ -63,9 +63,18 @@ class Compiler
         foreach ($finder as $file) {
             $this->addFile($phar, $file);
         }
-        $this->addFile($phar, new \SplFileInfo(__DIR__.'/Autoload/ClassLoader.php'), false);
-        $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../res/composer-schema.json'), false);
-        $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../src/Composer/IO/hiddeninput.exe'), false);
+        $this->addFile($phar, new \SplFileInfo(__DIR__ . '/Autoload/ClassLoader.php'), false);
+
+        $finder = new Finder();
+        $finder->files()
+            ->name('*.json')
+            ->in(__DIR__ . '/../../res')
+        ;
+
+        foreach ($finder as $file) {
+            $this->addFile($phar, $file, false);
+        }
+        $this->addFile($phar, new \SplFileInfo(__DIR__ . '/../../src/Composer/IO/hiddeninput.exe'), false);
 
         $finder = new Finder();
         $finder->files()

+ 228 - 0
src/Composer/Util/SPDXLicenseIdentifier.php

@@ -0,0 +1,228 @@
+<?php
+
+/*
+ * 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.
+ */
+
+namespace Composer\Util;
+
+/**
+ * SPDX License Identifier
+ *
+ * Supports composer array and SPDX tag notation for disjunctive/conjunctive
+ * licenses.
+ *
+ * @author Tom Klingenberg <tklingenberg@lastflood.net>
+ */
+class SPDXLicenseIdentifier
+{
+    /**
+     * @var array
+     */
+    private $identifiers;
+    /**
+     * @var array|string
+     */
+    private $license;
+
+    /**
+     * @param string|string[] $license
+     */
+    public function __construct($license)
+    {
+        $this->initIdentifiers();
+        $this->setLicense($license);
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->getLicense();
+    }
+
+    /**
+     * @return string
+     */
+    public function getLicense()
+    {
+        return $this->license;
+    }
+
+    /**
+     * @param array|string $license
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setLicense($license)
+    {
+        if (is_array($license)) {
+            $license = $this->getLicenseFromArray($license);
+        }
+        if (!is_string($license)) {
+            throw new \InvalidArgumentException(sprintf(
+                'Array or String expected, %s given.', gettype($license)
+            ));
+        }
+        if (!$this->isValidLicenseString($license)) {
+            throw new \InvalidArgumentException(sprintf(
+                'Invalid license: "%s"', $license
+            ));
+        }
+        $this->license = $license;
+    }
+
+    /**
+     * @param array $licenses
+     *
+     * @return string
+     */
+    private function getLicenseFromArray(array $licenses)
+    {
+        $buffer = '';
+        foreach ($licenses as $license) {
+            $buffer .= ($buffer ? ' or ' : '(') . (string)$license;
+        }
+        $buffer .= $buffer ? ')' : '';
+
+        return $buffer;
+    }
+
+    /**
+     * init SPDX identifiers
+     */
+    private function initIdentifiers()
+    {
+        $jsonFile = __DIR__ . '/../../../res/spdx-identifier.json';
+        $this->identifiers = $this->arrayFromJSONFile($jsonFile);
+    }
+
+    /**
+     * @param string $file
+     *
+     * @return array
+     * @throws \RuntimeException
+     */
+    private function arrayFromJSONFile($file)
+    {
+        $data = json_decode(file_get_contents($file));
+        if (!$data || !is_array($data)) {
+            throw new \RuntimeException(sprintf('Not a json array in file "%s"', $file));
+        }
+
+        return $data;
+    }
+
+    /**
+     * @param string $identifier
+     *
+     * @return bool
+     */
+    private function isValidLicenseIdentifier($identifier)
+    {
+        return in_array($identifier, $this->identifiers);
+    }
+
+    /**
+     * @param string $license
+     *
+     * @return bool
+     * @throws \RuntimeException
+     */
+    private function isValidLicenseString($license)
+    {
+        $tokens = array(
+            'po' => '\(',
+            'pc' => '\)',
+            'op' => '(?:or|and)',
+            'lix' => '(?:NONE|NOASSERTION)',
+            'lir' => 'LicenseRef-\d+',
+            'lic' => '[-+_.a-zA-Z0-9]{3,}',
+            'ws' => '\s+',
+            '_' => '.',
+        );
+        $next = function () use ($license, $tokens)
+        {
+            static $offset = 0;
+            if ($offset >= strlen($license)) {
+                return null;
+            }
+            foreach ($tokens as $name => $token) {
+                if (false === $r = preg_match("~$token~", $license, $matches, PREG_OFFSET_CAPTURE, $offset)) {
+                    throw new \RuntimeException('Pattern for token %s failed (regex error).', $name);
+                }
+                if ($r === 0) {
+                    continue;
+                }
+                if ($matches[0][1] !== $offset) {
+                    continue;
+                }
+                $offset += strlen($matches[0][0]);
+
+                return array($name, $matches[0][0]);
+            }
+            throw new \RuntimeException('At least the last pattern needs to match, but it did not (dot-match-all is missing?).');
+        };
+        $open = 0;
+        $require = 1;
+        $lastop = null;
+        while (list ($token, $string) = $next()) {
+            switch ($token) {
+                case 'po':
+                    if ($open || !$require) {
+                        return false;
+                    }
+                    $open = 1;
+                    break;
+                case 'pc':
+                    if ($open !== 1 || $require || !$lastop) {
+                        return false;
+                    }
+                    $open = 2;
+                    break;
+                case 'op':
+                    if ($require || !$open) {
+                        return false;
+                    }
+                    $lastop || $lastop = $string;
+                    if ($lastop !== $string) {
+                        return false;
+                    }
+                    $require = 1;
+                    break;
+                case 'lix':
+                    if ($open) {
+                        return false;
+                    }
+                    goto lir;
+                case 'lic':
+                    if (!$this->isValidLicenseIdentifier($string)) {
+                        return false;
+                    }
+                    // Fall-through intended
+                case 'lir':
+                    lir:
+                    if (!$require) {
+                        return false;
+                    }
+                    $require = 0;
+                    break;
+                case 'ws':
+                    break;
+                case '_':
+                    return false;
+                default:
+                    throw new \RuntimeException(sprintf('Unparsed token: %s.', print_r($token, true)));
+            }
+        }
+
+        return !($open % 2 || $require);
+    }
+}

+ 83 - 0
tests/Composer/Test/Util/SPDXLicenseIdentifierTest.php

@@ -0,0 +1,83 @@
+<?php
+namespace Composer\Test\Util;
+
+use Composer\Test\TestCase;
+use Composer\Util\SPDXLicenseIdentifier;
+
+class SPDXLicenseIdentifierTest extends TestCase
+{
+
+    public static function provideValidLicenses()
+    {
+        $valid = array_merge(
+            array(
+                "MIT",
+                "NONE",
+                "NOASSERTION",
+                "LicenseRef-3",
+                array("LGPL-2.0", "GPL-3.0+"),
+                "(LGPL-2.0 or GPL-3.0+)",
+                "(EUDatagrid and GPL-3.0+)",
+            ),
+            json_decode(file_get_contents(__DIR__ . '/../../../../res/spdx-identifier.json'))
+        );
+
+        foreach ($valid as &$r) {
+            $r = array($r);
+        }
+
+        return $valid;
+    }
+
+    public static function provideInvalidLicenses()
+    {
+        return array(
+            array(NULL),
+            array(""),
+            array("The system pwns you"),
+            array("()"),
+            array("(MIT)"),
+            array("MIT NONE"),
+            array("MIT (MIT and MIT)"),
+            array("(MIT and MIT) MIT"),
+            array(array("LGPL-2.0", "The system pwns you")),
+            array("and GPL-3.0+"),
+            array("EUDatagrid and GPL-3.0+"),
+            array("(GPL-3.0 and GPL-2.0 or GPL-3.0+)"),
+            array("(EUDatagrid and GPL-3.0+ and  )"),
+            array("(EUDatagrid xor GPL-3.0+)"),
+            array("(MIT Or MIT)"),
+            array("(NONE or MIT)"),
+            array("(NOASSERTION or MIT)"),
+        );
+    }
+
+    /**
+     * @dataProvider provideValidLicenses
+     * @param $license
+     */
+    public function testConstructor($license)
+    {
+        $identifier = new SPDXLicenseIdentifier($license);
+        $this->assertInstanceOf('Composer\Util\SPDXLicenseIdentifier', $identifier);
+    }
+
+    /**
+     * @dataProvider provideInvalidLicenses
+     * @expectedException InvalidArgumentException
+     * @param string|array $invalidLicense
+     */
+    public function testInvalidLicenses($invalidLicense)
+    {
+        $identifier = new SPDXLicenseIdentifier($invalidLicense);
+    }
+
+    public function testGetLicense()
+    {
+        $license = new SPDXLicenseIdentifier('NONE');
+        $string = $license->getLicense();
+        $this->assertInternalType('string', $string);
+        $string = (string)$license;
+        $this->assertInternalType('string', $string);
+    }
+}