| // Copyright 2020 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:core'; |
| import 'dart:io'; |
| import 'package:path/path.dart' as path; |
| import 'dart:io' as io_internals show exit; |
| |
| final bool hasColor = stdout.supportsAnsiEscapes; |
| final String bold = hasColor ? '\x1B[1m' : ''; // used for shard titles |
| final String red = hasColor ? '\x1B[31m' : ''; // used for errors |
| final String reset = hasColor ? '\x1B[0m' : ''; |
| final String reverse = hasColor ? '\x1B[7m' : ''; // used for clocks |
| |
| Future<void> main() async { |
| print('$clock STARTING ANALYSIS'); |
| try { |
| await run(); |
| } on ExitException catch (error) { |
| error.apply(); |
| } |
| print('$clock ${bold}Analysis successful.$reset'); |
| } |
| |
| Future<void> run() async { |
| final String cocoonPath = path.join(path.dirname(Platform.script.path), '..'); |
| print('$clock Root path: $cocoonPath'); |
| print('$clock Licenses...'); |
| await verifyConsistentLicenses(cocoonPath); |
| await verifyNoMissingLicense(cocoonPath); |
| } |
| |
| // TESTS |
| String _generateLicense(String prefix) { |
| return '${prefix}Copyright (2014|2015|2016|2017|2018|2019|2020|2021|2022|2023|2024) The Flutter Authors. All rights reserved.\n' |
| '${prefix}Use of this source code is governed by a BSD-style license that can be\n' |
| '${prefix}found in the LICENSE file.'; |
| } |
| |
| /// Ensure that LICENSES in Cocoon and its packages are consistent with each other. |
| /// |
| /// Verifies that every LICENSE file in Cocoon matches cocoon/LICENSE. |
| Future<void> verifyConsistentLicenses(String workingDirectory) async { |
| final String goldenLicensePath = '$workingDirectory/LICENSE'; |
| final String goldenLicense = File(goldenLicensePath).readAsStringSync(); |
| if (goldenLicense.isEmpty) { |
| throw Exception('No LICENSE was found at the root of Cocoon'); |
| } |
| |
| final List<String> badLicenses = <String>[]; |
| for (final FileSystemEntity entity in Directory(workingDirectory).listSync(recursive: true)) { |
| final String cocoonPath = entity.path.split('/../').last; |
| if (cocoonPath.contains(RegExp('(\.git)|(\.dart_tool)|(\.plugin_symlinks)'))) { |
| continue; |
| } |
| |
| if (path.basename(entity.path) == 'LICENSE') { |
| final String license = File(entity.path).readAsStringSync(); |
| if (license != goldenLicense) { |
| badLicenses.add(cocoonPath); |
| } |
| } |
| } |
| |
| if (badLicenses.isNotEmpty) { |
| exitWithError( |
| <String>['The following LICENSE files do not match the golden LICENSE at root:']..insertAll(1, badLicenses), |
| ); |
| } |
| } |
| |
| Future<void> verifyNoMissingLicense(String workingDirectory, {bool checkMinimums = true}) async { |
| final int? overrideMinimumMatches = checkMinimums ? null : 0; |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'dart', |
| overrideMinimumMatches ?? 2000, |
| _generateLicense('// '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'java', |
| overrideMinimumMatches ?? 39, |
| _generateLicense('// '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'h', |
| overrideMinimumMatches ?? 30, |
| _generateLicense('// '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'm', |
| overrideMinimumMatches ?? 30, |
| _generateLicense('// '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'swift', |
| overrideMinimumMatches ?? 10, |
| _generateLicense('// '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'gradle', |
| overrideMinimumMatches ?? 100, |
| _generateLicense('// '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'gn', |
| overrideMinimumMatches ?? 0, |
| _generateLicense('# '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'Dockerfile', |
| overrideMinimumMatches ?? 1, |
| _generateLicense('# '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'sh', |
| overrideMinimumMatches ?? 1, |
| '#![^\n]+sh\n' + _generateLicense('# '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'bat', |
| overrideMinimumMatches ?? 1, |
| _generateLicense(':: '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'ps1', |
| overrideMinimumMatches ?? 1, |
| _generateLicense('# '), |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'html', |
| overrideMinimumMatches ?? 1, |
| '<!-- ${_generateLicense('')} -->', |
| trailingBlank: false, |
| ); |
| await _verifyNoMissingLicenseForExtension( |
| workingDirectory, |
| 'xml', |
| overrideMinimumMatches ?? 1, |
| '<!-- ${_generateLicense('')} -->', |
| ); |
| } |
| |
| Future<void> _verifyNoMissingLicenseForExtension( |
| String workingDirectory, |
| String extension, |
| int minimumMatches, |
| String license, { |
| bool trailingBlank = true, |
| }) async { |
| assert(!license.endsWith('\n')); |
| final String licensePattern = license + '\n' + (trailingBlank ? '\n' : ''); |
| final List<String> errors = <String>[]; |
| for (final File file in _allFiles(workingDirectory, extension, minimumMatches: minimumMatches)) { |
| final String contents = file.readAsStringSync().replaceAll('\r\n', '\n'); |
| if (contents.isEmpty) continue; // let's not go down the /bin/true rabbit hole |
| if (!contents.startsWith(RegExp(licensePattern))) errors.add(file.path); |
| } |
| // Fail if any errors |
| if (errors.isNotEmpty) { |
| final String s = errors.length == 1 ? ' does' : 's do'; |
| exitWithError(<String>[ |
| '${bold}The following ${errors.length} file$s not have the right license header:$reset', |
| ...errors, |
| 'The expected license header is:', |
| license, |
| if (trailingBlank) '...followed by a blank line.', |
| ]); |
| } |
| } |
| |
| Iterable<File> _allFiles(String workingDirectory, String extension, {required int minimumMatches}) sync* { |
| assert(!extension.startsWith('.'), 'Extension argument should not start with a period.'); |
| final Set<FileSystemEntity> pending = <FileSystemEntity>{Directory(workingDirectory)}; |
| int matches = 0; |
| while (pending.isNotEmpty) { |
| final FileSystemEntity entity = pending.first; |
| pending.remove(entity); |
| if (path.extension(entity.path) == '.tmpl') continue; |
| if (entity is File) { |
| if (_isGeneratedPluginRegistrant(entity)) continue; |
| if (path.basename(entity.path) == 'AppDelegate.h') continue; |
| if (path.basename(entity.path) == 'flutter_export_environment.sh') continue; |
| if (path.basename(entity.path) == 'gradlew.bat') continue; |
| if (path.basename(entity.path) == 'Runner-Bridging-Header.h') continue; |
| if (path.basename(entity.path).endsWith('g.dart')) continue; |
| if (path.basename(entity.path).endsWith('mocks.mocks.dart')) continue; |
| if (path.basename(entity.path).endsWith('pb.dart')) continue; |
| if (path.basename(entity.path).endsWith('pbenum.dart')) continue; |
| if (path.basename(entity.path).endsWith('pbjson.dart')) continue; |
| if (path.basename(entity.path).endsWith('pbgrpc.dart')) continue; |
| if (path.basename(entity.path).endsWith('pbserver.dart')) continue; |
| if (path.extension(entity.path) == '.$extension') { |
| matches += 1; |
| yield entity; |
| } |
| if (path.basename(entity.path) == 'Dockerfile' && extension == 'Dockerfile') { |
| matches += 1; |
| yield entity; |
| } |
| } else if (entity is Directory) { |
| if (File(path.join(entity.path, '.dartignore')).existsSync()) continue; |
| if (path.basename(entity.path) == '.git') continue; |
| if (path.basename(entity.path) == '.gradle') continue; |
| if (path.basename(entity.path) == '.dart_tool') continue; |
| if (path.basename(entity.path) == 'third_party') continue; |
| if (_isPartOfAppTemplate(entity)) continue; |
| pending.addAll(entity.listSync()); |
| } |
| } |
| assert( |
| matches >= minimumMatches, |
| 'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.', |
| ); |
| } |
| |
| bool _isPartOfAppTemplate(Directory directory) { |
| const Set<String> templateDirs = <String>{ |
| 'android', |
| 'build', |
| 'ios', |
| 'linux', |
| 'macos', |
| 'web', |
| 'windows', |
| }; |
| // Project directories will have a metadata file in them. |
| if (File(path.join(directory.parent.path, '.metadata')).existsSync()) { |
| return templateDirs.contains(path.basename(directory.path)); |
| } |
| return false; |
| } |
| |
| bool _isGeneratedPluginRegistrant(File file) { |
| final String filename = path.basenameWithoutExtension(file.path); |
| return !path.split(file.path).contains('.pub-cache') && |
| (filename == 'generated_plugin_registrant' || filename == 'GeneratedPluginRegistrant'); |
| } |
| |
| void exitWithError(List<String> messages) { |
| final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset'; |
| print(redLine); |
| messages.forEach(print); |
| print(redLine); |
| exit(1); |
| } |
| |
| class ExitException implements Exception { |
| ExitException(this.exitCode); |
| |
| final int exitCode; |
| |
| void apply() { |
| io_internals.exit(exitCode); |
| } |
| } |
| |
| String get clock { |
| final DateTime now = DateTime.now(); |
| return '$reverse▌' |
| '${now.hour.toString().padLeft(2, "0")}:' |
| '${now.minute.toString().padLeft(2, "0")}:' |
| '${now.second.toString().padLeft(2, "0")}' |
| '▐$reset'; |
| } |