| // Copyright 2013 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:io'; | |
| import 'package:file/file.dart'; | |
| import 'package:git/git.dart'; | |
| import 'package:path/path.dart' as p; | |
| import 'common/core.dart'; | |
| import 'common/file_utils.dart'; | |
| import 'common/output_utils.dart'; | |
| import 'common/package_command.dart'; | |
| const int _exitListFilesFailed = 3; | |
| 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', | |
| 'web_plugin_registrant', | |
| }; | |
| // File suffixes that otherwise match _codeFileExtensions to ignore. | |
| const Set<String> _ignoreSuffixList = <String>{ | |
| '.pb.dart', // Generated by protoc. | |
| '.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. | |
| }; | |
| // Third-party packages where the code doesn't have file-level annotation, just | |
| // the package-level LICENSE file. Each entry must be a directory relative to | |
| // third_party/packages, as that is the only directory where this is allowed. | |
| const Set<String> _unannotatedFileThirdPartyDirectories = <String>{ | |
| 'mustache_template', | |
| 'path_parsing', | |
| 'flutter_svg', | |
| 'flutter_svg_test', | |
| }; | |
| // 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 google_maps_flutter_web. | |
| RegExp(r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev', multiLine: true), | |
| // packages/third_party/mustache_template. | |
| RegExp( | |
| r'Copyright \(c\) 2013, Greg Lowe\n' | |
| r'All rights reserved.\n\n' | |
| r'Redistribution and use in source and binary forms, with or without ' | |
| r'modification, are permitted provided that the following conditions are met:\n', | |
| ), | |
| // packages/third_party/path_parsing. | |
| RegExp( | |
| r'Copyright \(c\) 2018 Dan Field\n\n' | |
| r'Permission is hereby granted, free of charge, to any person obtaining a copy\n' | |
| r'of this software and associated documentation files \(the "Software"\), to deal\n' | |
| r'in the Software without restriction, including without limitation the rights\n' | |
| r'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n' | |
| r'copies of the Software, and to permit persons to whom the Software is\n' | |
| r'furnished to do so, subject to the following conditions:', | |
| ), | |
| ]; | |
| // 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 | |
| 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(super.packagesDir, {super.platform, super.gitDir}); | |
| @override | |
| final String name = 'license-check'; | |
| @override | |
| List<String> get aliases => <String>['check-license']; | |
| @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 _getAllCheckedInFiles()).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, | |
| ); | |
| var passed = true; | |
| print('\n=======================================\n'); | |
| if (licenseFileFailures.isNotEmpty) { | |
| passed = false; | |
| printError('The following LICENSE files do not follow the expected format:'); | |
| for (final file in licenseFileFailures) { | |
| printError(' ${_repoRelativePath(file)}'); | |
| } | |
| 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(' ${_repoRelativePath(file)}'); | |
| } | |
| 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(' ${_repoRelativePath(file)}'); | |
| } | |
| 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\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 incorrectFirstPartyFiles = <File>[]; | |
| final unrecognizedThirdPartyFiles = <File>[]; | |
| // Most code file types in the repository use '//' comments. | |
| final String defaultFirstPartyLicenseBlock = _generateLicenseBlock('// '); | |
| // A few file types have a different comment structure. | |
| final firstPartyLicenseBlockByExtension = <String, String>{ | |
| '.sh': _generateLicenseBlock('# '), | |
| '.html': _generateLicenseBlock('', prefix: '<!-- ', suffix: ' -->'), | |
| }; | |
| for (final file in codeFiles) { | |
| print('Checking ${_repoRelativePath(file)}'); | |
| // Some third-party directories have code that doesn't annotate each file, | |
| // so for those check the LICENSE file instead. This is done even though | |
| // it's redundant to re-check it for each file because it ensures that we | |
| // are still validating every file individually, rather than having a | |
| // codepath where whole directories of files are ignored, which would have | |
| // a much worse failure mode. | |
| String content; | |
| if (_unannotatedFileThirdPartyDirectories.any( | |
| (String dir) => file.path.contains('/third_party/packages/$dir/'), | |
| )) { | |
| Directory packageDir = file.parent; | |
| while (packageDir.parent.basename != 'packages') { | |
| packageDir = packageDir.parent; | |
| } | |
| content = await packageDir.childFile('LICENSE').readAsString(); | |
| } else { | |
| content = await file.readAsString(); | |
| } | |
| // On Windows, git may auto-convert line endings on checkout; this should | |
| // still pass since they will be converted back on commit. | |
| content = content.replaceAll('\r\n', '\n'); | |
| final String firstPartyLicense = | |
| firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? | |
| defaultFirstPartyLicenseBlock; | |
| 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(firstPartyLicense)) { | |
| unrecognizedThirdPartyFiles.add(file); | |
| } | |
| } else { | |
| if (!content.contains(firstPartyLicense)) { | |
| 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 incorrectLicenseFiles = <File>[]; | |
| for (final file in files) { | |
| print('Checking ${_repoRelativePath(file)}'); | |
| // 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) { | |
| if (_ignoreBasenameList.contains(p.basenameWithoutExtension(file.path))) { | |
| return true; | |
| } | |
| if (_ignoreSuffixList.any(file.path.endsWith)) { | |
| return true; | |
| } | |
| if (_ignoredFullBasenameList.contains(p.basename(file.path))) { | |
| return true; | |
| } | |
| if (p.split(file.path).any((String part) => part.endsWith('_objc_gen'))) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool _isThirdParty(File file) { | |
| return path.split(file.path).contains('third_party'); | |
| } | |
| Future<Iterable<File>> _getAllCheckedInFiles() async { | |
| final GitDir git = await gitDir; | |
| final ProcessResult result = await git.runCommand(<String>['ls-files'], throwOnError: false); | |
| if (result.exitCode != 0) { | |
| printError('Unable to get list of files under source control'); | |
| throw ToolExit(_exitListFilesFailed); | |
| } | |
| return (result.stdout as String) | |
| .trim() | |
| .split('\n') | |
| .where((String path) => path.isNotEmpty) | |
| .map((String path) => rootDir.childFile(path)) | |
| // Filter out symbolic links to avoid checking files multiple times. | |
| .where((File f) => !rootDir.fileSystem.isLinkSync(f.path)); | |
| } | |
| // Returns the directories containing mapped submodules, if any. | |
| Future<Iterable<Directory>> _getSubmoduleDirectories() async { | |
| final submodulePaths = <Directory>[]; | |
| final Directory repoRoot = packagesDir.fileSystem.directory((await gitDir).path); | |
| final File submoduleSpec = repoRoot.childFile('.gitmodules'); | |
| if (submoduleSpec.existsSync()) { | |
| final 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; | |
| } | |
| String _repoRelativePath(File file) { | |
| return relativePosixPath(file, from: rootDir, platformContext: path); | |
| } | |
| } | |
| enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } |