forked from WordPress/gutenberg
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a package license checker (WordPress#8808)
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
Showing
4 changed files
with
241 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` ); | ||
} | ||
} ); |