| // 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:path/path.dart' as p; |
| |
| import 'common/core.dart'; |
| import 'common/output_utils.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(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 _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 } |