Skip to content

Commit

Permalink
Add a package license checker (WordPress#8808)
Browse files Browse the repository at this point in the history
Ensure that all packages we distribute have a GPL2 compatible license, and any other packages we use during development have some form of OSS license.

Fixes WordPress#6508, WordPress#7822.
  • Loading branch information
pento authored Aug 13, 2018
1 parent dab3fc1 commit fa3e58a
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 1 deletion.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"build:packages": "cross-env EXCLUDE_PACKAGES=babel-plugin-import-jsx-pragma,jest-console,postcss-themes node ./bin/packages/build.js",
"build": "npm run build:packages && cross-env NODE_ENV=production webpack",
"check-engines": "check-node-version --package",
"check-licenses": "concurrently \"wp-scripts check-licenses --prod --gpl2\" \"wp-scripts check-licenses --dev\"",
"ci": "concurrently \"npm run lint\" \"npm run test-unit:coverage-ci\"",
"predev": "npm run check-engines",
"dev": "npm run build:packages && concurrently \"cross-env webpack --watch\" \"npm run dev:packages\"",
Expand All @@ -171,7 +172,7 @@
"lint-css": "stylelint '**/*.scss'",
"lint-css:fix": "stylelint '**/*.scss' --fix",
"package-plugin": "./bin/build-plugin-zip.sh",
"postinstall": "npm run build:packages",
"postinstall": "npm run check-licenses && npm run build:packages",
"pot-to-php": "./bin/pot-to-php.js",
"precommit": "lint-staged",
"publish:check": "npm run build:packages && lerna updated",
Expand Down
1 change: 1 addition & 0 deletions packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@wordpress/babel-preset-default": "file:../babel-preset-default",
"@wordpress/jest-preset-default": "file:../jest-preset-default",
"@wordpress/npm-package-json-lint-config": "file:../npm-package-json-lint-config",
"chalk": "^2.4.1",
"cross-spawn": "^5.1.0",
"jest": "^23.4.2",
"npm-package-json-lint": "^3.3.0",
Expand Down
237 changes: 237 additions & 0 deletions packages/scripts/scripts/check-licenses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/**
* External dependencies
*/
const spawn = require( 'cross-spawn' );
const { existsSync, readFileSync } = require( 'fs' );
const chalk = require( 'chalk' );

/**
* Internal dependencies
*/
const { hasCliArg } = require( '../utils' );

/*
* WARNING: Changes to this file may inadvertently cause us to distribute code that
* is not GPL2 compatible.
*
* When adding a new license (for example, when a new package has a variation of the
* various license strings), please ensure that changes to this file are explicitly
* reviewed and approved.
*/

const ERROR = chalk.reset.inverse.bold.red( ' ERROR ' );

const prod = hasCliArg( '--prod' ) || hasCliArg( '--production' );
const dev = hasCliArg( '--dev' ) || hasCliArg( '--development' );
const gpl2 = hasCliArg( '--gpl2' );

/*
* A list of license strings that we've found to be GPL2 compatible.
*
* Note the strings with "AND" in them at the bottom: these should only be added when
* all the licenses in that string are GPL2 compatible.
*/
const gpl2CompatibleLicenses = [
'Apache-2.0 WITH LLVM-exception',
'Artistic-2.0',
'BSD',
'BSD-2-Clause',
'BSD-3-Clause',
'BSD-like',
'CC-BY-3.0',
'CC-BY-4.0',
'CC0-1.0',
'GPL-2.0',
'GPL-2.0+',
'GPL-2.0-or-later',
'ISC',
'LGPL-2.1',
'MIT',
'MIT/X11',
'MIT (http://mootools.net/license.txt)',
'MPL-2.0',
'Public Domain',
'Unlicense',
'WTFPL',
'Zlib',
'(MIT AND BSD-3-Clause)',
'(MIT AND Zlib)',
];

/*
* A list of OSS license strings that aren't GPL2 compatible.
*
* We're cool with using packages that are licensed under any of these if we're not
* distributing them (for example, build tools), but we can't included them in a release.
*/
const otherOssLicenses = [
'Apache-2.0',
'Apache 2.0',
'Apache License, Version 2.0',
'Apache version 2.0',
];

const licenses = [
...gpl2CompatibleLicenses,
...( gpl2 ? [] : otherOssLicenses ),
];

/*
* Some packages don't included a license string in their package.json file, but they
* do have a license listed elsewhere. These files are checked for matching license strings.
*/
const licenseFiles = [
'LICENCE',
'license',
'LICENSE',
'LICENSE.md',
'LICENSE.txt',
'LICENSE-MIT',
'MIT-LICENSE.txt',
'Readme.md',
'README.md',
];

/*
* When searching through files for licensing information, these are the strings we look for,
* and their matching license.
*/
const licenseFileStrings = {
'Apache-2.0': [
'Licensed under the Apache License, Version 2.0',
],
BSD: [
'Redistributions in binary form must reproduce the above copyright notice,',
],
MIT: [
'Permission is hereby granted, free of charge,',
'## License\n\nMIT',
'## License\n\n MIT',
],
};

/**
* Check if a license string matches the given license.
*
* The license string can be a single license, or an SPDX-compatible "OR" license string.
* eg, "(MIT OR Zlib)".
*
* @param {string} allowedLicense The license that's allowed.
* @param {string} licenseType The license string to check.
*
* @return {boolean} true if the licenseType matches the allowedLicense, false if it doesn't.
*/
const checkLicense = ( allowedLicense, licenseType ) => {
if ( ! licenseType ) {
return false;
}

// Some licenses have unusual capitalisation in them.
const formattedAllowedLicense = allowedLicense.toLowerCase();
const formattedlicenseType = licenseType.toLowerCase();

if ( formattedAllowedLicense === formattedlicenseType ) {
return true;
}

// We can skip the parsing below if there isn't an 'OR' in the license.
if ( licenseType.indexOf( 'OR' ) < 0 ) {
return false;
}

/*
* In order to do a basic parse of SPDX-compatible "OR" license strings, we:
* - Remove surrounding brackets: "(mit or zlib)" -> "mit or zlib"
* - Split it into an array: "mit or zlib" -> [ "mit", "zlib" ]
* - Trim any remaining whitespace from each element
*/
const subLicenseTypes = formattedlicenseType
.replace( /^\(*/g, '' )
.replace( /\)*$/, '' )
.split( ' or ' )
.map( ( e ) => e.trim() );

// We can then check our array of licenses against the allowedLicense.
return undefined !== subLicenseTypes.find( ( subLicenseType ) => checkLicense( allowedLicense, subLicenseType ) );
};

// Use `npm ls` to grab a list of all the packages.
const child = spawn.sync( 'npm', [
'ls',
'--parseable',
...( prod ? [ '--prod' ] : [] ),
...( dev ? [ '--dev' ] : [] ),
] );

const modules = child.stdout.toString().split( '\n' );

modules.forEach( ( path ) => {
if ( ! path ) {
return;
}

const filename = path + '/package.json';
if ( ! existsSync( filename ) ) {
process.stdout.write( `Unable to locate package.json in ${ path }.` );
process.exit( 1 );
}

/*
* The package.json format can be kind of weird. We allow for the following formats:
* - { license: 'MIT' }
* - { license: { type: 'MIT' } }
* - { licenses: [ 'MIT', 'Zlib' ] }
* - { licenses: [ { type: 'MIT' }, { type: 'Zlib' } ] }
*/
const packageInfo = require( filename );
const license = packageInfo.license ||
(
packageInfo.licenses &&
packageInfo.licenses
.map( ( l ) => l.type || l )
.join( ' OR ' )
);
let licenseType = typeof license === 'object' ? license.type : license;

/*
* If we haven't been able to detect a license in the package.json file, try reading
* it from the files defined in licenseFiles, instead.
*/
if ( licenseType === undefined ) {
licenseType = licenseFiles.reduce( ( detectedType, licenseFile ) => {
if ( detectedType ) {
return detectedType;
}

const licensePath = path + '/' + licenseFile;

if ( existsSync( licensePath ) ) {
const licenseText = readFileSync( licensePath ).toString();

// Check if the file contains any of the strings in licenseFileStrings
return Object.keys( licenseFileStrings ).reduce( ( stringDetectedType, licenseStringType ) => {
const licenseFileString = licenseFileStrings[ licenseStringType ];

return licenseFileString.reduce( ( currentDetectedType, fileString ) => {
if ( licenseText.includes( fileString ) ) {
return licenseStringType;
}
return currentDetectedType;
}, stringDetectedType );
}, detectedType );
}
}, false );
}

if ( ! licenseType ) {
return false;
}

// Now that we finally have a license to check, see if any of the allowed licenses match.
const allowed = licenses.find( ( allowedLicense ) => checkLicense( allowedLicense, licenseType ) );

if ( ! allowed ) {
process.exitCode = 1;
process.stdout.write( `${ ERROR } Module ${ packageInfo.name } has an incompatible license '${ licenseType }'.\n` );
}
} );

0 comments on commit fa3e58a

Please sign in to comment.