1
0
Fork 0

Use of SPDX license identifiers.

pull/661/head
Tom Klingenberg 2012-05-07 22:34:25 +03:00
parent e7dc42a8a2
commit 74ca58bcb6
7 changed files with 528 additions and 27 deletions

85
bin/fetch-spdx-identifier Executable file
View File

@ -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 File

@ -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
res/spdx-identifier.json Normal file
View File

@ -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 File

@ -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;
}
}

View File

@ -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()

View File

@ -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 File

@ -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);
}
}