mirror of
https://github.com/flutter/packages.git
synced 2025-08-16 13:01:29 +08:00
Add missing licenses, and add a check (#3720)
Adds a new CI check that all code files have a copyright+license block (and that it's one we are expecting to see). Fixes the ~350 files (!) that did not have them. This includes all of the files in the .../example/ directories, following the example of flutter/flutter. (This does mean some manual intervention will be needed when generating new example directories in the future, but it's one-time per example.) Also standardized some variants that used different line breaks than most of the rest of the repo (likely added since I standardized them all a while ago, but didn't add a check for at the time to enforce going forward), to simplify the checks. Fixes flutter/flutter#77114
This commit is contained in:
209
script/tool/lib/src/license_check_command.dart
Normal file
209
script/tool/lib/src/license_check_command.dart
Normal file
@ -0,0 +1,209 @@
|
||||
// Copyright 2017 The Chromium 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 'dart:async';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'common.dart';
|
||||
|
||||
const Set<String> _codeFileExtensions = <String>{
|
||||
'.c',
|
||||
'.cc',
|
||||
'.cpp',
|
||||
'.dart',
|
||||
'.h',
|
||||
'.html',
|
||||
'.java',
|
||||
'.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.
|
||||
//
|
||||
// These are intentionally very simple, since almost all source in this
|
||||
// repository should be using the same license text, comment style, etc., so
|
||||
// they shouldn't need to be very flexible. Complexity can be added as-needed
|
||||
// on a case-by-case basis.
|
||||
final RegExp _copyrightRegex =
|
||||
RegExp(r'^(?://|#|<!--) Copyright \d+,? ([^.]+)', multiLine: true);
|
||||
// Non-Flutter code. When adding license regexes here, include the copyright
|
||||
// info to ensure that any new additions are flagged for added scrutiny in
|
||||
// review.
|
||||
// -----
|
||||
// Third-party code used in url_launcher_web.
|
||||
final RegExp _workivaLicenseRegex = RegExp(
|
||||
r'^// Copyright 2017 Workiva Inc..*'
|
||||
'^// Licensed under the Apache License, Version 2.0',
|
||||
multiLine: true,
|
||||
dotAll: true);
|
||||
|
||||
// TODO(stuartmorgan): Replace this with a single string once all the copyrights
|
||||
// are standardized.
|
||||
final List<String> _firstPartyAuthors = <String>[
|
||||
'The Chromium Authors',
|
||||
'the Chromium project authors',
|
||||
'The Flutter Authors',
|
||||
'the Flutter project authors',
|
||||
];
|
||||
|
||||
/// Validates that code files have copyright and license blocks.
|
||||
class LicenseCheckCommand extends PluginCommand {
|
||||
/// Creates a new license check command for [packagesDir].
|
||||
LicenseCheckCommand(
|
||||
Directory packagesDir,
|
||||
FileSystem fileSystem, {
|
||||
Print print = print,
|
||||
}) : _print = print,
|
||||
super(packagesDir, fileSystem);
|
||||
|
||||
final Print _print;
|
||||
|
||||
@override
|
||||
final String name = 'license-check';
|
||||
|
||||
@override
|
||||
final String description =
|
||||
'Ensures that all code files have copyright/license blocks.';
|
||||
|
||||
@override
|
||||
Future<Null> run() async {
|
||||
Iterable<File> codeFiles = (await _getAllFiles()).where((File file) =>
|
||||
_codeFileExtensions.contains(p.extension(file.path)) &&
|
||||
!_shouldIgnoreFile(file));
|
||||
|
||||
bool succeeded = await _checkLicenses(codeFiles);
|
||||
|
||||
if (!succeeded) {
|
||||
throw ToolExit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates the expected license block (without copyright) for first-party
|
||||
// code.
|
||||
String _generateLicense(String comment, {String suffix = ''}) {
|
||||
return '${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 false if any of them
|
||||
// fail validation.
|
||||
Future<bool> _checkLicenses(Iterable<File> codeFiles) async {
|
||||
final List<File> filesWithoutDetectedCopyright = <File>[];
|
||||
final List<File> filesWithoutDetectedLicense = <File>[];
|
||||
final List<File> misplacedThirdPartyFiles = <File>[];
|
||||
|
||||
// Most code file types in the repository use '//' comments.
|
||||
final String defaultBsdLicenseBlock = _generateLicense('// ');
|
||||
// A few file types have a different comment structure.
|
||||
final Map<String, String> bsdLicenseBlockByExtension = <String, String>{
|
||||
'.sh': _generateLicense('# '),
|
||||
'.html': _generateLicense('', suffix: ' -->'),
|
||||
};
|
||||
|
||||
for (final File file in codeFiles) {
|
||||
_print('Checking ${file.path}');
|
||||
final String content = await file.readAsString();
|
||||
|
||||
final RegExpMatch copyright = _copyrightRegex.firstMatch(content);
|
||||
if (copyright == null) {
|
||||
filesWithoutDetectedCopyright.add(file);
|
||||
continue;
|
||||
}
|
||||
final String author = copyright.group(1);
|
||||
if (!_firstPartyAuthors.contains(author) &&
|
||||
!p.split(file.path).contains('third_party')) {
|
||||
misplacedThirdPartyFiles.add(file);
|
||||
}
|
||||
|
||||
final String bsdLicense =
|
||||
bsdLicenseBlockByExtension[p.extension(file.path)] ??
|
||||
defaultBsdLicenseBlock;
|
||||
if (!content.contains(bsdLicense) &&
|
||||
!_workivaLicenseRegex.hasMatch(content)) {
|
||||
filesWithoutDetectedLicense.add(file);
|
||||
}
|
||||
}
|
||||
_print('\n\n');
|
||||
|
||||
// Sort by path for more usable output.
|
||||
final pathCompare = (File a, File b) => a.path.compareTo(b.path);
|
||||
filesWithoutDetectedCopyright.sort(pathCompare);
|
||||
filesWithoutDetectedLicense.sort(pathCompare);
|
||||
misplacedThirdPartyFiles.sort(pathCompare);
|
||||
|
||||
if (filesWithoutDetectedCopyright.isNotEmpty) {
|
||||
_print('No copyright line was found for the following files:');
|
||||
for (final File file in filesWithoutDetectedCopyright) {
|
||||
_print(' ${file.path}');
|
||||
}
|
||||
_print('Please check that they have a copyright and license block. '
|
||||
'If they do, the license check may need to be updated to recognize its '
|
||||
'format.\n');
|
||||
}
|
||||
|
||||
if (filesWithoutDetectedLicense.isNotEmpty) {
|
||||
_print('No recognized license was found for the following files:');
|
||||
for (final File file in filesWithoutDetectedLicense) {
|
||||
_print(' ${file.path}');
|
||||
}
|
||||
_print('Please check that they have a license at the top of the file. '
|
||||
'If they do, the license check may need to be updated to recognize '
|
||||
'either the license or the specific format of the license '
|
||||
'block.\n');
|
||||
}
|
||||
|
||||
if (misplacedThirdPartyFiles.isNotEmpty) {
|
||||
_print('The following files do not have a recognized first-party author '
|
||||
'but are not in a "third_party/" directory:');
|
||||
for (final File file in misplacedThirdPartyFiles) {
|
||||
_print(' ${file.path}');
|
||||
}
|
||||
_print('Please move these files to "third_party/".\n');
|
||||
}
|
||||
|
||||
bool succeeded = filesWithoutDetectedCopyright.isEmpty &&
|
||||
filesWithoutDetectedLicense.isEmpty &&
|
||||
misplacedThirdPartyFiles.isEmpty;
|
||||
if (succeeded) {
|
||||
_print('All files passed validation!');
|
||||
}
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
Future<List<File>> _getAllFiles() => packagesDir.parent
|
||||
.list(recursive: true, followLinks: false)
|
||||
.where((FileSystemEntity entity) => entity is File)
|
||||
.map((FileSystemEntity file) => file as File)
|
||||
.toList();
|
||||
}
|
Reference in New Issue
Block a user