Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Use of SPDX license identifiers.

  • Loading branch information...
commit 74ca58bcb692b10ec85b27965a7c33c8c563ae7b 1 parent e7dc42a
@ktomk ktomk authored
View
85 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;
+ }
+}
View
58 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
View
34 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"
+]
View
56 src/Composer/Command/ValidateCommand.php
@@ -18,67 +18,97 @@
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;
}
}
View
15 src/Composer/Compiler.php
@@ -63,9 +63,18 @@ public function compile($pharFile = 'composer.phar')
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()
View
228 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);
+ }
+}
View
83 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);
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.