| // 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 '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/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>{ |
| '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 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', |
| ), |
| // 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. 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 _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); |
| |
| 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(' ${_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. 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 defaultFirstPartyLicenseBlock = _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 ${_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 List<File> incorrectLicenseFiles = <File>[]; |
| |
| for (final File 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; |
| } |
| |
| 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); |
| } |
| final Directory repoRoot = packagesDir.parent; |
| return (result.stdout as String) |
| .trim() |
| .split('\n') |
| .where((String path) => path.isNotEmpty) |
| .map((String path) => repoRoot.childFile(path)) |
| // Filter out symbolic links to avoid checking files multiple times. |
| .where((File f) => !repoRoot.fileSystem.isLinkSync(f.path)); |
| } |
| |
| // 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; |
| } |
| |
| String _repoRelativePath(File file) { |
| return p.posix.joinAll(path.split( |
| path.relative(file.absolute.path, from: packagesDir.parent.path))); |
| } |
| } |
| |
| enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } |