mirror of
https://github.com/flutter/packages.git
synced 2025-08-06 17:28:42 +08:00
309 lines
12 KiB
Dart
309 lines
12 KiB
Dart
// Copyright 2013 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:git/git.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:platform/platform.dart';
|
|
|
|
import 'common/core.dart';
|
|
import 'common/package_command.dart';
|
|
|
|
const Set<String> _codeFileExtensions = <String>{
|
|
'.c',
|
|
'.cc',
|
|
'.cpp',
|
|
'.dart',
|
|
'.h',
|
|
'.html',
|
|
'.java',
|
|
'.kt',
|
|
'.m',
|
|
'.mm',
|
|
'.swift',
|
|
'.sh',
|
|
};
|
|
|
|
// Basenames without extensions of files to ignore.
|
|
const Set<String> _ignoreBasenameList = <String>{
|
|
'flutter_export_environment',
|
|
'GeneratedPluginRegistrant',
|
|
'generated_plugin_registrant',
|
|
};
|
|
|
|
// File suffixes that otherwise match _codeFileExtensions to ignore.
|
|
const Set<String> _ignoreSuffixList = <String>{
|
|
'.g.dart', // Generated API code.
|
|
'.mocks.dart', // Generated by Mockito.
|
|
};
|
|
|
|
// Full basenames of files to ignore.
|
|
const Set<String> _ignoredFullBasenameList = <String>{
|
|
'resource.h', // Generated by VS.
|
|
};
|
|
|
|
// Copyright and license regexes for third-party code.
|
|
//
|
|
// These are intentionally very simple, since there is very little third-party
|
|
// code in this repository. Complexity can be added as-needed on a case-by-case
|
|
// basis.
|
|
//
|
|
// When adding license regexes here, include the copyright info to ensure that
|
|
// any new additions are flagged for added scrutiny in review.
|
|
final List<RegExp> _thirdPartyLicenseBlockRegexes = <RegExp>[
|
|
// Third-party code used in url_launcher_web.
|
|
RegExp(
|
|
r'^// Copyright 2017 Workiva Inc\..*'
|
|
r'^// Licensed under the Apache License, Version 2\.0',
|
|
multiLine: true,
|
|
dotAll: true,
|
|
),
|
|
// Third-party code used in google_maps_flutter_web.
|
|
RegExp(
|
|
r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev',
|
|
multiLine: true,
|
|
),
|
|
// bsdiff in flutter/packages.
|
|
RegExp(
|
|
r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n'
|
|
r'// Use of this source code is governed by a BSD-style license that can be\n'
|
|
r'// found in the LICENSE file\.\n',
|
|
),
|
|
];
|
|
|
|
// 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.
|
|
// standardized.
|
|
const String _fullBsdLicenseText = '''
|
|
Copyright 2013 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.
|
|
''';
|
|
|
|
/// Validates that code files have copyright and license blocks.
|
|
class LicenseCheckCommand extends PackageCommand {
|
|
/// Creates a new license check command for [packagesDir].
|
|
LicenseCheckCommand(Directory packagesDir,
|
|
{Platform platform = const LocalPlatform(), GitDir? gitDir})
|
|
: super(packagesDir, platform: platform, gitDir: gitDir);
|
|
|
|
@override
|
|
final String name = 'license-check';
|
|
|
|
@override
|
|
final String description =
|
|
'Ensures that all code files have copyright/license blocks.';
|
|
|
|
@override
|
|
Future<void> run() async {
|
|
// Create a set of absolute paths to submodule directories, with trailing
|
|
// separator, to do prefix matching with to test directory inclusion.
|
|
final Iterable<String> submodulePaths = (await _getSubmoduleDirectories())
|
|
.map(
|
|
(Directory dir) => '${dir.absolute.path}${platform.pathSeparator}');
|
|
|
|
final Iterable<File> allFiles = (await _getAllFiles()).where(
|
|
(File file) => !submodulePaths.any(file.absolute.path.startsWith));
|
|
|
|
final Iterable<File> codeFiles = allFiles.where((File file) =>
|
|
_codeFileExtensions.contains(p.extension(file.path)) &&
|
|
!_shouldIgnoreFile(file));
|
|
final Iterable<File> firstPartyLicenseFiles = allFiles.where((File file) =>
|
|
path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file));
|
|
|
|
final List<File> licenseFileFailures =
|
|
await _checkLicenseFiles(firstPartyLicenseFiles);
|
|
final Map<_LicenseFailureType, List<File>> codeFileFailures =
|
|
await _checkCodeLicenses(codeFiles);
|
|
|
|
bool passed = true;
|
|
|
|
print('\n=======================================\n');
|
|
|
|
if (licenseFileFailures.isNotEmpty) {
|
|
passed = false;
|
|
printError(
|
|
'The following LICENSE files do not follow the expected format:');
|
|
for (final File file in licenseFileFailures) {
|
|
printError(' ${file.path}');
|
|
}
|
|
printError('Please ensure that they use the exact format used in this '
|
|
'repository".\n');
|
|
}
|
|
|
|
if (codeFileFailures[_LicenseFailureType.incorrectFirstParty]!.isNotEmpty) {
|
|
passed = false;
|
|
printError('The license block for these files is missing or incorrect:');
|
|
for (final File file
|
|
in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) {
|
|
printError(' ${file.path}');
|
|
}
|
|
printError(
|
|
'If this third-party code, move it to a "third_party/" directory, '
|
|
'otherwise ensure that you are using the exact copyright and license '
|
|
'text used by all first-party files in this repository.\n');
|
|
}
|
|
|
|
if (codeFileFailures[_LicenseFailureType.unknownThirdParty]!.isNotEmpty) {
|
|
passed = false;
|
|
printError(
|
|
'No recognized license was found for the following third-party files:');
|
|
for (final File file
|
|
in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) {
|
|
printError(' ${file.path}');
|
|
}
|
|
print('Please check that they have a license at the top of the file. '
|
|
'If they do, the license check needs to be updated to recognize '
|
|
'the new third-party license block.\n');
|
|
}
|
|
|
|
if (!passed) {
|
|
throw ToolExit(1);
|
|
}
|
|
|
|
printSuccess('All files passed validation!');
|
|
}
|
|
|
|
// Creates the expected copyright+license block for first-party code.
|
|
String _generateLicenseBlock(
|
|
String comment, {
|
|
String prefix = '',
|
|
String suffix = '',
|
|
}) {
|
|
return '$prefix${comment}Copyright 2013 The Flutter Authors. All rights reserved.\n'
|
|
'${comment}Use of this source code is governed by a BSD-style license that can be\n'
|
|
'${comment}found in the LICENSE file.$suffix\n';
|
|
}
|
|
|
|
/// Checks all license blocks for [codeFiles], returning any that fail
|
|
/// validation.
|
|
Future<Map<_LicenseFailureType, List<File>>> _checkCodeLicenses(
|
|
Iterable<File> codeFiles) async {
|
|
final List<File> incorrectFirstPartyFiles = <File>[];
|
|
final List<File> unrecognizedThirdPartyFiles = <File>[];
|
|
|
|
// Most code file types in the repository use '//' comments.
|
|
final String defaultFirstParyLicenseBlock = _generateLicenseBlock('// ');
|
|
// A few file types have a different comment structure.
|
|
final Map<String, String> firstPartyLicenseBlockByExtension =
|
|
<String, String>{
|
|
'.sh': _generateLicenseBlock('# '),
|
|
'.html': _generateLicenseBlock('', prefix: '<!-- ', suffix: ' -->'),
|
|
};
|
|
|
|
for (final File file in codeFiles) {
|
|
print('Checking ${file.path}');
|
|
// On Windows, git may auto-convert line endings on checkout; this should
|
|
// still pass since they will be converted back on commit.
|
|
final String content =
|
|
(await file.readAsString()).replaceAll('\r\n', '\n');
|
|
|
|
final String firstParyLicense =
|
|
firstPartyLicenseBlockByExtension[p.extension(file.path)] ??
|
|
defaultFirstParyLicenseBlock;
|
|
if (_isThirdParty(file)) {
|
|
// Third-party directories allow either known third-party licenses, our
|
|
// the first-party license, as there may be local additions.
|
|
if (!_thirdPartyLicenseBlockRegexes
|
|
.any((RegExp regex) => regex.hasMatch(content)) &&
|
|
!content.contains(firstParyLicense)) {
|
|
unrecognizedThirdPartyFiles.add(file);
|
|
}
|
|
} else {
|
|
if (!content.contains(firstParyLicense)) {
|
|
incorrectFirstPartyFiles.add(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by path for more usable output.
|
|
int pathCompare(File a, File b) => a.path.compareTo(b.path);
|
|
incorrectFirstPartyFiles.sort(pathCompare);
|
|
unrecognizedThirdPartyFiles.sort(pathCompare);
|
|
|
|
return <_LicenseFailureType, List<File>>{
|
|
_LicenseFailureType.incorrectFirstParty: incorrectFirstPartyFiles,
|
|
_LicenseFailureType.unknownThirdParty: unrecognizedThirdPartyFiles,
|
|
};
|
|
}
|
|
|
|
/// Checks all provided LICENSE [files], returning any that fail validation.
|
|
Future<List<File>> _checkLicenseFiles(Iterable<File> files) async {
|
|
final List<File> incorrectLicenseFiles = <File>[];
|
|
|
|
for (final File file in files) {
|
|
print('Checking ${file.path}');
|
|
// On Windows, git may auto-convert line endings on checkout; this should
|
|
// still pass since they will be converted back on commit.
|
|
final String contents = file.readAsStringSync().replaceAll('\r\n', '\n');
|
|
if (!contents.contains(_fullBsdLicenseText)) {
|
|
incorrectLicenseFiles.add(file);
|
|
}
|
|
}
|
|
|
|
return incorrectLicenseFiles;
|
|
}
|
|
|
|
bool _shouldIgnoreFile(File file) {
|
|
final String path = file.path;
|
|
return _ignoreBasenameList.contains(p.basenameWithoutExtension(path)) ||
|
|
_ignoreSuffixList.any((String suffix) =>
|
|
path.endsWith(suffix) ||
|
|
_ignoredFullBasenameList.contains(p.basename(path)));
|
|
}
|
|
|
|
bool _isThirdParty(File file) {
|
|
return path.split(file.path).contains('third_party');
|
|
}
|
|
|
|
Future<List<File>> _getAllFiles() => packagesDir.parent
|
|
.list(recursive: true, followLinks: false)
|
|
.where((FileSystemEntity entity) => entity is File)
|
|
.map((FileSystemEntity file) => file as File)
|
|
.toList();
|
|
|
|
// Returns the directories containing mapped submodules, if any.
|
|
Future<Iterable<Directory>> _getSubmoduleDirectories() async {
|
|
final List<Directory> submodulePaths = <Directory>[];
|
|
final Directory repoRoot =
|
|
packagesDir.fileSystem.directory((await gitDir).path);
|
|
final File submoduleSpec = repoRoot.childFile('.gitmodules');
|
|
if (submoduleSpec.existsSync()) {
|
|
final RegExp pathLine = RegExp(r'path\s*=\s*(.*)');
|
|
for (final String line in submoduleSpec.readAsLinesSync()) {
|
|
final RegExpMatch? match = pathLine.firstMatch(line);
|
|
if (match != null) {
|
|
submodulePaths.add(repoRoot.childDirectory(match.group(1)!.trim()));
|
|
}
|
|
}
|
|
}
|
|
return submodulePaths;
|
|
}
|
|
}
|
|
|
|
enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty }
|