diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 4e0e5931d3..e70e60b154 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -66,9 +66,40 @@ final List _firstPartyAuthors = [ 'The Chromium Authors', 'the Chromium project authors', 'The Flutter Authors', - 'the Flutter project authors', ]; +// The exact format of the BSD license that our license files should contain. +// Slight variants are not accepted because they may prevent consolidation in +// tools that assemble all licenses used in distributed applications. +// +// TODO(stuartmorgan): Add the copyright string here once that's completely +// standardized. +final String _fullBsdLicenseText = ''' +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + /// Validates that code files have copyright and license blocks. class LicenseCheckCommand extends PluginCommand { /// Creates a new license check command for [packagesDir]. @@ -90,13 +121,19 @@ class LicenseCheckCommand extends PluginCommand { @override Future run() async { - Iterable codeFiles = (await _getAllFiles()).where((File file) => + final Iterable codeFiles = (await _getAllFiles()).where((File file) => _codeFileExtensions.contains(p.extension(file.path)) && !_shouldIgnoreFile(file)); + final Iterable firstPartyLicenseFiles = (await _getAllFiles()).where( + (File file) => + p.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); - bool succeeded = await _checkLicenses(codeFiles); + final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); + print('\n=======================================\n'); + final bool licenseCheckSucceeded = + await _checkLicenseFiles(firstPartyLicenseFiles); - if (!succeeded) { + if (!copyrightCheckSucceeded || !licenseCheckSucceeded) { throw ToolExit(1); } } @@ -110,7 +147,7 @@ class LicenseCheckCommand extends PluginCommand { // Checks all license blocks for [codeFiles], returning false if any of them // fail validation. - Future _checkLicenses(Iterable codeFiles) async { + Future _checkCodeLicenses(Iterable codeFiles) async { final List filesWithoutDetectedCopyright = []; final List filesWithoutDetectedLicense = []; final List misplacedThirdPartyFiles = []; @@ -133,8 +170,7 @@ class LicenseCheckCommand extends PluginCommand { continue; } final String author = copyright.group(1); - if (!_firstPartyAuthors.contains(author) && - !p.split(file.path).contains('third_party')) { + if (!_firstPartyAuthors.contains(author) && !_isThirdParty(file)) { misplacedThirdPartyFiles.add(file); } @@ -146,7 +182,7 @@ class LicenseCheckCommand extends PluginCommand { filesWithoutDetectedLicense.add(file); } } - _print('\n\n'); + _print('\n'); // Sort by path for more usable output. final pathCompare = (File a, File b) => a.path.compareTo(b.path); @@ -188,7 +224,35 @@ class LicenseCheckCommand extends PluginCommand { filesWithoutDetectedLicense.isEmpty && misplacedThirdPartyFiles.isEmpty; if (succeeded) { - _print('All files passed validation!'); + _print('All source files passed validation!'); + } + return succeeded; + } + + // Checks all provide LICENSE files, returning false if any of them + // fail validation. + Future _checkLicenseFiles(Iterable files) async { + final List incorrectLicenseFiles = []; + + for (final File file in files) { + _print('Checking ${file.path}'); + if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { + incorrectLicenseFiles.add(file); + } + } + _print('\n'); + + if (incorrectLicenseFiles.isNotEmpty) { + _print('The following LICENSE files do not follow the expected format:'); + for (final File file in incorrectLicenseFiles) { + _print(' ${file.path}'); + } + _print('Please ensure that they use the exact format used in this repository".\n'); + } + + bool succeeded = incorrectLicenseFiles.isEmpty; + if (succeeded) { + _print('All LICENSE files passed validation!'); } return succeeded; } @@ -201,6 +265,10 @@ class LicenseCheckCommand extends PluginCommand { _ignoredFullBasenameList.contains(p.basename(path))); } + bool _isThirdParty(File file) { + return p.split(file.path).contains('third_party'); + } + Future> _getAllFiles() => packagesDir.parent .list(recursive: true, followLinks: false) .where((FileSystemEntity entity) => entity is File) diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index 524e727123..8ae956740d 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -134,7 +134,7 @@ void main() { // Sanity check that the test did actually check a file. expect(printedMessages, contains('Checking checked.cc')); - expect(printedMessages, contains('All files passed validation!')); + expect(printedMessages, contains('All source files passed validation!')); }); test('handles the comment styles for all supported languages', () async { @@ -154,7 +154,7 @@ void main() { expect(printedMessages, contains('Checking file_a.cc')); expect(printedMessages, contains('Checking file_b.sh')); expect(printedMessages, contains('Checking file_c.html')); - expect(printedMessages, contains('All files passed validation!')); + expect(printedMessages, contains('All source files passed validation!')); }); test('fails if any checked files are missing license blocks', () async { @@ -176,7 +176,8 @@ void main() { expect(printedMessages, contains(' bad.cc')); expect(printedMessages, contains(' bad.h')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('fails if any checked files are missing just the copyright', () async { @@ -195,7 +196,8 @@ void main() { contains('No copyright line was found for the following files:')); expect(printedMessages, contains(' bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('fails if any checked files are missing just the license', () async { @@ -214,7 +216,8 @@ void main() { contains('No recognized license was found for the following files:')); expect(printedMessages, contains(' bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('fails if any third-party code is not in a third_party directory', @@ -234,7 +237,8 @@ void main() { 'but are not in a "third_party/" directory:')); expect(printedMessages, contains(' third_party.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('succeeds for third-party code in a third_party directory', () async { @@ -252,7 +256,7 @@ void main() { // Sanity check that the test did actually check the file. expect(printedMessages, contains('Checking a_plugin/lib/src/third_party/file.cc')); - expect(printedMessages, contains('All files passed validation!')); + expect(printedMessages, contains('All source files passed validation!')); }); test('fails for licenses that the tool does not expect', () async { @@ -274,7 +278,8 @@ void main() { contains('No recognized license was found for the following files:')); expect(printedMessages, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); }); test('Apache is not recognized for new authors without validation changes', @@ -300,7 +305,107 @@ void main() { contains('No recognized license was found for the following files:')); expect(printedMessages, contains(' third_party/bad.cc')); // Failure shouldn't print the success message. - expect(printedMessages, isNot(contains('All files passed validation!'))); + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('passes if all first-party LICENSE files are correctly formatted', + () async { + File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_correctLicenseFileText); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the file. + expect(printedMessages, contains('Checking LICENSE')); + expect(printedMessages, contains('All LICENSE files passed validation!')); + }); + + test('fails if any first-party LICENSE files are incorrectly formatted', + () async { + File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_incorrectLicenseFileText); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + expect(printedMessages, + isNot(contains('All LICENSE files passed validation!'))); + }); + + test('ignores third-party LICENSE format', + () async { + File license = root.childDirectory('third_party').childFile('LICENSE'); + license.createSync(recursive: true); + license.writeAsStringSync(_incorrectLicenseFileText); + + await runner.run(['license-check']); + + // The file shouldn't be checked. + expect(printedMessages, isNot(contains('Checking third_party/LICENSE'))); + expect(printedMessages, contains('All LICENSE files passed validation!')); }); }); } + +const String _correctLicenseFileText = + '''Copyright 2017 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +// A common incorrect version created by copying text intended for a code file, +// with comment markers. +const String _incorrectLicenseFileText = + '''// Copyright 2017 The Flutter Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +''';