| // Copyright 2014 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:async'; | |
| import 'dart:convert'; | |
| import 'dart:core' hide print; | |
| import 'dart:io' hide exit; | |
| import 'dart:typed_data'; | |
| import 'package:crypto/crypto.dart'; | |
| import 'package:meta/meta.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'allowlist.dart'; | |
| import 'run_command.dart'; | |
| import 'utils.dart'; | |
| const _kHeartbeatInterval = Duration(seconds: 30); | |
| /// The path to the `dart` executable; set at the top of `main` | |
| late final String dart; | |
| /// The path to the `pub` executable; set at the top of `main` | |
| late final String pub; | |
| /// When you call this, you can pass additional arguments to pass custom | |
| /// arguments to flutter analyze. For example, you might want to call this | |
| /// script with the parameter --dart-sdk to use custom dart sdk. | |
| /// | |
| /// For example: | |
| /// bin/cache/dart-sdk/bin/dart dev/bots/analyze.dart --dart-sdk=/tmp/dart-sdk | |
| Future<void> main(List<String> arguments) async { | |
| final String dartSdk = path.join( | |
| Directory.current.absolute.path, | |
| _getDartSdkFromArguments(arguments) ?? path.join(flutterRoot, 'bin', 'cache', 'dart-sdk'), | |
| ); | |
| dart = path.join(dartSdk, 'bin', Platform.isWindows ? 'dart.exe' : 'dart'); | |
| pub = path.join(dartSdk, 'bin', Platform.isWindows ? 'pub.bat' : 'pub'); | |
| printProgress('STARTING ANALYSIS'); | |
| await run(arguments); | |
| if (hasError) { | |
| reportErrorsAndExit('${bold}Analysis failed.$reset'); | |
| } | |
| reportSuccessAndExit('${bold}Analysis successful.$reset'); | |
| } | |
| /// Scans [arguments] for an argument of the form `--dart-sdk` or | |
| /// `--dart-sdk=...` and returns the configured SDK, if any. | |
| String? _getDartSdkFromArguments(List<String> arguments) { | |
| String? result; | |
| for (var i = 0; i < arguments.length; i += 1) { | |
| if (arguments[i] == '--dart-sdk') { | |
| if (result != null) { | |
| foundError(<String>['The --dart-sdk argument must not be used more than once.']); | |
| return null; | |
| } | |
| if (i + 1 < arguments.length) { | |
| result = arguments[i + 1]; | |
| } else { | |
| foundError(<String>['--dart-sdk must be followed by a path.']); | |
| return null; | |
| } | |
| } | |
| if (arguments[i].startsWith('--dart-sdk=')) { | |
| if (result != null) { | |
| foundError(<String>['The --dart-sdk argument must not be used more than once.']); | |
| return null; | |
| } | |
| result = arguments[i].substring('--dart-sdk='.length); | |
| } | |
| } | |
| return result; | |
| } | |
| void _printHelp(List<Validation> validations) { | |
| print('Usage: dart dev/bots/analyze.dart [arguments]'); | |
| print(''); | |
| print('Options:'); | |
| print(' -h, --help Show this help message.'); | |
| print(' --only=rule1,rule2,... Run only the specified validations.'); | |
| print(' --skip=rule1,rule2,... Skip the specified validations.'); | |
| print(''); | |
| print('Available rules:'); | |
| for (final validation in validations) { | |
| print(' ${validation.name.padRight(25)} ${validation.description}'); | |
| } | |
| } | |
| /// Represents a specific validation step in the analyze script. | |
| @visibleForTesting | |
| class Validation { | |
| /// Creates a new validation step. | |
| const Validation(this.name, this.description, this.callback); | |
| /// The short name of the rule used for filtering with --only or --skip. | |
| final String name; | |
| /// A human-readable description of the validation step. | |
| final String description; | |
| /// The function that performs the validation. | |
| final Future<void> Function() callback; | |
| } | |
| Future<void> run(List<String> arguments, {List<Validation>? validationsForTesting}) async { | |
| var assertsEnabled = false; | |
| assert(() { | |
| assertsEnabled = true; | |
| return true; | |
| }()); | |
| if (!assertsEnabled) { | |
| foundError(<String>['The analyze.dart script must be run with --enable-asserts.']); | |
| } | |
| CommandResult? dartAnalyzeResult; | |
| final passthroughArguments = <String>[]; | |
| final List<Validation> validations = | |
| validationsForTesting ?? | |
| _getValidations( | |
| passthroughArguments: passthroughArguments, | |
| getDartAnalyzeResult: () => dartAnalyzeResult, | |
| onDartAnalyzeResult: (CommandResult? result) => dartAnalyzeResult = result, | |
| ); | |
| final seenNames = <String>{}; | |
| for (final validation in validations) { | |
| if (!seenNames.add(validation.name)) { | |
| foundError(<String>['Duplicate validation name "${validation.name}" found in analyze.dart.']); | |
| } | |
| } | |
| var onlyRules = <String>[]; | |
| var skipRules = <String>[]; | |
| for (final arg in arguments) { | |
| if (arg == '-h' || arg == '--help') { | |
| _printHelp(validations); | |
| return; | |
| } | |
| if (arg.startsWith('--only=')) { | |
| if (onlyRules.isNotEmpty) { | |
| foundError(<String>['The --only argument must not be used more than once.']); | |
| return; | |
| } | |
| onlyRules = arg.substring('--only='.length).split(','); | |
| continue; | |
| } | |
| if (arg.startsWith('--skip=')) { | |
| if (skipRules.isNotEmpty) { | |
| foundError(<String>['The --skip argument must not be used more than once.']); | |
| return; | |
| } | |
| skipRules = arg.substring('--skip='.length).split(','); | |
| continue; | |
| } | |
| passthroughArguments.add(arg); | |
| } | |
| if (onlyRules.isNotEmpty && skipRules.isNotEmpty) { | |
| foundError(<String>['Cannot use both --only and --skip at the same time.']); | |
| return; | |
| } | |
| final Set<String> validNames = validations.map((Validation v) => v.name).toSet(); | |
| for (final rule in onlyRules) { | |
| if (!validNames.contains(rule)) { | |
| foundError(<String>['Unknown rule "$rule" passed to --only.']); | |
| return; | |
| } | |
| } | |
| for (final rule in skipRules) { | |
| if (!validNames.contains(rule)) { | |
| foundError(<String>['Unknown rule "$rule" passed to --skip.']); | |
| return; | |
| } | |
| } | |
| for (final validation in validations) { | |
| final bool shouldRun = onlyRules.isNotEmpty | |
| ? onlyRules.contains(validation.name) | |
| : !skipRules.contains(validation.name); | |
| if (shouldRun) { | |
| printProgress('${validation.description} [${validation.name}]'); | |
| await validation.callback(); | |
| } | |
| } | |
| } | |
| List<Validation> _getValidations({ | |
| required List<String> passthroughArguments, | |
| required CommandResult? Function() getDartAnalyzeResult, | |
| required void Function(CommandResult?) onDartAnalyzeResult, | |
| }) { | |
| return <Validation>[ | |
| Validation( | |
| 'release-branch', | |
| 'Release branch validation', | |
| () => verifyReleaseBranchState(flutterRoot), | |
| ), | |
| Validation( | |
| 'target-platform', | |
| 'TargetPlatform tool/framework consistency', | |
| () => verifyTargetPlatform(flutterRoot), | |
| ), | |
| Validation( | |
| 'tool-end-to-end', | |
| 'All tool test files end in _test.dart...', | |
| () => verifyToolTestsEndInTestDart(flutterRoot), | |
| ), | |
| Validation( | |
| 'no-checked-mode', | |
| 'Debug mode instead of checked mode...', | |
| () => verifyNoCheckedMode(flutterRoot), | |
| ), | |
| Validation('no-binaries', 'Unexpected binaries...', () => verifyNoBinaries(flutterRoot)), | |
| Validation( | |
| 'no-trailing-spaces', | |
| 'Trailing spaces...', | |
| () => verifyNoTrailingSpaces(flutterRoot), | |
| ), | |
| Validation( | |
| 'spaces-after-flow', | |
| 'Spaces after flow control statements...', | |
| () => verifySpacesAfterFlowControlStatements(flutterRoot), | |
| ), | |
| Validation('no-missing-license', 'Licenses...', () => verifyNoMissingLicense(flutterRoot)), | |
| Validation( | |
| 'no-bad-imports-flutter', | |
| 'Bad imports (framework)...', | |
| () => verifyNoBadImportsInFlutter(flutterRoot), | |
| ), | |
| Validation( | |
| 'internationalization', | |
| 'Internationalization...', | |
| () => verifyInternationalizations(flutterRoot, dart), | |
| ), | |
| Validation( | |
| 'stock-app-localizations', | |
| 'Localization files of stocks app...', | |
| () => verifyStockAppLocalizations(flutterRoot), | |
| ), | |
| Validation('lint-kotlin', 'Lint Kotlin files...', () => lintKotlinFiles(flutterRoot)), | |
| Validation( | |
| 'lint-kotlin-templates', | |
| 'Lint generated Kotlin files from templates...', | |
| () => lintKotlinTemplatedFiles(flutterRoot), | |
| ), | |
| Validation( | |
| 'update-packages', | |
| 'Package dependencies...', | |
| () => runCommand(flutter, <String>['update-packages'], workingDirectory: flutterRoot), | |
| ), | |
| Validation('package-allowlist', 'Package Allowlist...', () => _checkConsumerDependencies()), | |
| Validation('dart-analysis', 'Dart analysis...', () async { | |
| final CommandResult result = await _runFlutterAnalyze( | |
| flutterRoot, | |
| options: <String>['--flutter-repo', ...passthroughArguments], | |
| ); | |
| onDartAnalyzeResult(result); | |
| }), | |
| Validation( | |
| 'format', | |
| 'Check formatting of Dart files...', | |
| () => runCommand(dart, <String>[ | |
| '--enable-asserts', | |
| path.join(flutterRoot, 'dev', 'tools', 'bin', 'format.dart'), | |
| ], workingDirectory: flutterRoot), | |
| ), | |
| Validation('executable-allowlist', 'Executable allowlist...', () => _checkForNewExecutables()), | |
| Validation( | |
| 'dart-analysis-watch', | |
| 'Dart analysis (with --watch)...', | |
| () => _runFlutterAnalyze( | |
| flutterRoot, | |
| failureMessage: 'Dart analyzer failed when --watch was used.', | |
| options: <String>['--flutter-repo', '--watch', '--benchmark', ...passthroughArguments], | |
| ), | |
| ), | |
| Validation( | |
| 'snippets', | |
| 'Snippet code...', | |
| () => runCommand(dart, <String>[ | |
| '--enable-asserts', | |
| path.join(flutterRoot, 'dev', 'bots', 'analyze_snippet_code.dart'), | |
| '--verbose', | |
| ], workingDirectory: flutterRoot), | |
| ), | |
| Validation( | |
| 'code-samples', | |
| 'Code sample link validation...', | |
| () => runCommand(dart, <String>[ | |
| '--enable-asserts', | |
| path.join(flutterRoot, 'dev', 'bots', 'check_code_samples.dart'), | |
| ], workingDirectory: flutterRoot), | |
| ), | |
| Validation( | |
| 'mega-gallery', | |
| 'Dart analysis (mega gallery)...', | |
| () => _verifyMegaGallery(flutterRoot, dart, passthroughArguments), | |
| ), | |
| Validation( | |
| 'integration-templates', | |
| 'Up to date integration test template files...', | |
| () => verifyIntegrationTestTemplateFiles(flutterRoot), | |
| ), | |
| Validation( | |
| 'cross-imports', | |
| 'Cross-import test validation...', | |
| () => runCommand(dart, <String>[ | |
| '--enable-asserts', | |
| path.join(flutterRoot, 'dev', 'bots', 'check_tests_cross_imports.dart'), | |
| ], workingDirectory: flutterRoot), | |
| ), | |
| Validation( | |
| 'cross-imports-examples', | |
| 'Examples cross-import test validation...', | |
| () => runCommand(dart, <String>[ | |
| '--enable-asserts', | |
| path.join(flutterRoot, 'dev', 'bots', 'check_examples_cross_imports.dart'), | |
| ], workingDirectory: flutterRoot), | |
| ), | |
| ]; | |
| } | |
| // TESTS | |
| Future<void> verifyReleaseBranchState(String workringDirerctory) async { | |
| final ProcessResult result = await Process.run(dart, <String>[ | |
| 'bin/check_engine_version.dart', | |
| ], workingDirectory: path.join(workringDirerctory, 'dev', 'tools')); | |
| if (result.exitCode != 0) { | |
| foundError(<String>['${result.stderr}']); | |
| } | |
| } | |
| Future<void> verifyTargetPlatform(String workingDirectory) async { | |
| final framework = File('$workingDirectory/packages/flutter/lib/src/foundation/platform.dart'); | |
| final frameworkPlatforms = <String>{}; | |
| List<String> lines = framework.readAsLinesSync(); | |
| var index = 0; | |
| while (true) { | |
| if (index >= lines.length) { | |
| foundError(<String>['${framework.path}: Can no longer find TargetPlatform enum.']); | |
| return; | |
| } | |
| if (lines[index].startsWith('enum TargetPlatform {')) { | |
| index += 1; | |
| break; | |
| } | |
| index += 1; | |
| } | |
| while (true) { | |
| if (index >= lines.length) { | |
| foundError(<String>['${framework.path}: Could not find end of TargetPlatform enum.']); | |
| return; | |
| } | |
| String line = lines[index].trim(); | |
| final int comment = line.indexOf('//'); | |
| if (comment >= 0) { | |
| line = line.substring(0, comment); | |
| } | |
| if (line == '}') { | |
| break; | |
| } | |
| if (line.isNotEmpty) { | |
| if (line.endsWith(',')) { | |
| frameworkPlatforms.add(line.substring(0, line.length - 1)); | |
| } else { | |
| foundError(<String>[ | |
| '${framework.path}:$index: unparseable line when looking for TargetPlatform values', | |
| ]); | |
| } | |
| } | |
| index += 1; | |
| } | |
| final tool = File('$workingDirectory/packages/flutter_tools/lib/src/resident_runner.dart'); | |
| final toolPlatforms = <String>{}; | |
| lines = tool.readAsLinesSync(); | |
| index = 0; | |
| while (true) { | |
| if (index >= lines.length) { | |
| foundError(<String>['${tool.path}: Can no longer find nextPlatform logic.']); | |
| return; | |
| } | |
| if (lines[index].trim().startsWith('const platforms = <String>[')) { | |
| index += 1; | |
| break; | |
| } | |
| index += 1; | |
| } | |
| while (true) { | |
| if (index >= lines.length) { | |
| foundError(<String>['${tool.path}: Could not find end of nextPlatform logic.']); | |
| return; | |
| } | |
| final String line = lines[index].trim(); | |
| if (line.startsWith("'") && line.endsWith("',")) { | |
| toolPlatforms.add(line.substring(1, line.length - 2)); | |
| } else if (line == '];') { | |
| break; | |
| } else { | |
| foundError(<String>[ | |
| '${tool.path}:$index: unparseable line when looking for nextPlatform values', | |
| ]); | |
| } | |
| index += 1; | |
| } | |
| final Set<String> frameworkExtra = frameworkPlatforms.difference(toolPlatforms); | |
| if (frameworkExtra.isNotEmpty) { | |
| foundError(<String>[ | |
| 'TargetPlatform has some extra values not found in the tool: ${frameworkExtra.join(", ")}', | |
| ]); | |
| } | |
| final Set<String> toolExtra = toolPlatforms.difference(frameworkPlatforms); | |
| if (toolExtra.isNotEmpty) { | |
| foundError(<String>[ | |
| 'The nextPlatform logic in the tool has some extra values not found in TargetPlatform: ${toolExtra.join(", ")}', | |
| ]); | |
| } | |
| } | |
| /// Verify tool test files end in `_test.dart`. | |
| /// | |
| /// The test runner will only recognize files ending in `_test.dart` as tests to | |
| /// be run: https://github.com/dart-lang/test/tree/master/pkgs/test#running-tests | |
| Future<void> verifyToolTestsEndInTestDart(String workingDirectory) async { | |
| final String toolsTestPath = path.join(workingDirectory, 'packages', 'flutter_tools', 'test'); | |
| final violations = <String>[]; | |
| // detect files that contains calls to test(), testUsingContext(), and testWithoutContext() | |
| final callsTestFunctionPattern = RegExp( | |
| r'^ *(test\(.*\)|testUsingContext\(.*\)|testWithoutContext\(.*\))', | |
| ); | |
| await for (final File file in _allFiles(toolsTestPath, 'dart', minimumMatches: 300)) { | |
| final bool isValidTestFile = file.path.endsWith('_test.dart'); | |
| if (isValidTestFile) { | |
| continue; | |
| } | |
| final bool isTestData = file.path.contains(r'test_data'); | |
| if (isTestData) { | |
| continue; | |
| } | |
| final bool isInTestShard = file.path.contains(r'.shard/'); | |
| if (!isInTestShard) { | |
| continue; | |
| } | |
| final bool callsTestFunction = file.readAsStringSync().contains(callsTestFunctionPattern); | |
| if (!callsTestFunction) { | |
| continue; | |
| } | |
| violations.add(file.path); | |
| } | |
| if (violations.isNotEmpty) { | |
| foundError(<String>[ | |
| '${bold}Found flutter_tools tests that do not end in `_test.dart`; these will not be run by the test runner$reset', | |
| ...violations, | |
| ]); | |
| } | |
| } | |
| String _generateLicense(String prefix) { | |
| return '${prefix}Copyright 2014 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.'; | |
| } | |
| 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 ?? 1, | |
| _generateLicense('// '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'h', | |
| overrideMinimumMatches ?? 30, | |
| _generateLicense('// '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'm', | |
| overrideMinimumMatches ?? 30, | |
| _generateLicense('// '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'cc', | |
| overrideMinimumMatches ?? 10, | |
| _generateLicense('// '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'cpp', | |
| overrideMinimumMatches ?? 0, | |
| _generateLicense('// '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'swift', | |
| overrideMinimumMatches ?? 10, | |
| _generateLicense('// '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'gradle', | |
| overrideMinimumMatches ?? 80, | |
| _generateLicense('// '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'gn', | |
| overrideMinimumMatches ?? 0, | |
| _generateLicense('# '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'sh', | |
| overrideMinimumMatches ?? 1, | |
| _generateLicense('# '), | |
| header: r'#!/usr/bin/env bash\n', | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'bat', | |
| overrideMinimumMatches ?? 1, | |
| _generateLicense('REM '), | |
| header: r'@ECHO off\n', | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'ps1', | |
| overrideMinimumMatches ?? 1, | |
| _generateLicense('# '), | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'html', | |
| overrideMinimumMatches ?? 1, | |
| '<!-- ${_generateLicense('')} -->', | |
| trailingBlank: false, | |
| header: r'<!DOCTYPE HTML>\n', | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'xml', | |
| overrideMinimumMatches ?? 1, | |
| '<!-- ${_generateLicense('')} -->', | |
| header: r'(<\?xml version="1.0" encoding="utf-8"\?>\n)?', | |
| ); | |
| await _verifyNoMissingLicenseForExtension( | |
| workingDirectory, | |
| 'frag', | |
| overrideMinimumMatches ?? 1, | |
| _generateLicense('// '), | |
| header: r'#version 320 es(\n)+', | |
| ); | |
| } | |
| Future<void> _verifyNoMissingLicenseForExtension( | |
| String workingDirectory, | |
| String extension, | |
| int minimumMatches, | |
| String license, { | |
| bool trailingBlank = true, | |
| // The "header" is a regular expression matching the header that comes before | |
| // the license in some files. | |
| String header = '', | |
| }) async { | |
| assert(!license.endsWith('\n')); | |
| final String licensePattern = RegExp.escape('$license\n${trailingBlank ? '\n' : ''}'); | |
| final errors = <String>[]; | |
| await 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 (path.basename(file.path) == 'Package.swift') { | |
| continue; | |
| } | |
| if (!contents.startsWith(RegExp(header + licensePattern))) { | |
| errors.add(file.path); | |
| } | |
| } | |
| // Fail if any errors | |
| if (errors.isNotEmpty) { | |
| final fileDoes = errors.length == 1 ? 'file does' : '${errors.length} files do'; | |
| foundError(<String>[ | |
| '${bold}The following $fileDoes not have the right license header for $extension files:$reset', | |
| ...errors.map<String>((String error) => ' $error'), | |
| 'The expected license header is:', | |
| if (header.isNotEmpty) 'A header matching the regular expression "$header",', | |
| if (header.isNotEmpty) 'followed by the following license text:', | |
| license, | |
| if (trailingBlank) '...followed by a blank line.', | |
| ]); | |
| } | |
| } | |
| Future<void> verifyNoBadImportsInFlutter(String workingDirectory) async { | |
| final errors = <String>[]; | |
| final String libPath = path.join(workingDirectory, 'packages', 'flutter', 'lib'); | |
| final String srcPath = path.join(workingDirectory, 'packages', 'flutter', 'lib', 'src'); | |
| // Verify there's one libPath/*.dart for each srcPath/*/. | |
| final List<String> packages = | |
| Directory(libPath) | |
| .listSync() | |
| .where( | |
| (FileSystemEntity entity) => entity is File && path.extension(entity.path) == '.dart', | |
| ) | |
| .map<String>((FileSystemEntity entity) => path.basenameWithoutExtension(entity.path)) | |
| .toList() | |
| ..sort(); | |
| final List<String> directories = | |
| Directory(srcPath) | |
| .listSync() | |
| .whereType<Directory>() | |
| .map<String>((Directory entity) => path.basename(entity.path)) | |
| .toList() | |
| ..sort(); | |
| if (!_listEquals<String>(packages, directories)) { | |
| errors.add( | |
| <String>[ | |
| 'flutter/lib/*.dart does not match flutter/lib/src/*/:', | |
| 'These are the exported packages:', | |
| ...packages.map<String>((String path) => ' lib/$path.dart'), | |
| 'These are the directories:', | |
| ...directories.map<String>((String path) => ' lib/src/$path/'), | |
| ].join('\n'), | |
| ); | |
| } | |
| // Verify that the imports are well-ordered. | |
| final dependencyMap = <String, Set<String>>{}; | |
| for (final directory in directories) { | |
| dependencyMap[directory] = await _findFlutterDependencies(path.join(srcPath, directory)); | |
| } | |
| assert( | |
| dependencyMap['material']!.contains('widgets') && | |
| dependencyMap['widgets']!.contains('rendering') && | |
| dependencyMap['rendering']!.contains('painting'), | |
| ); // to make sure we're convinced _findFlutterDependencies is finding some | |
| for (final String package in dependencyMap.keys) { | |
| if (dependencyMap[package]!.contains(package)) { | |
| errors.add( | |
| 'One of the files in the $yellow$package$reset package imports that package recursively.', | |
| ); | |
| } | |
| } | |
| for (final String key in dependencyMap.keys) { | |
| for (final String dependency in dependencyMap[key]!) { | |
| if (dependencyMap[dependency] != null) { | |
| continue; | |
| } | |
| // Sanity check before performing _deepSearch, to ensure there's no rogue | |
| // dependencies. | |
| final String validFilenames = dependencyMap.keys | |
| .map((String name) => '$name.dart') | |
| .join(', '); | |
| errors.add( | |
| '$key imported package:flutter/$dependency.dart ' | |
| 'which is not one of the valid exports { $validFilenames }.\n' | |
| 'Consider changing $dependency.dart to one of them.', | |
| ); | |
| } | |
| } | |
| for (final String package in dependencyMap.keys) { | |
| final List<String>? loop = _deepSearch<String>(dependencyMap, package); | |
| if (loop != null) { | |
| errors.add('${yellow}Dependency loop:$reset ${loop.join(' depends on ')}'); | |
| } | |
| } | |
| // Fail if any errors | |
| if (errors.isNotEmpty) { | |
| foundError(<String>[ | |
| if (errors.length == 1) | |
| '${bold}An error was detected when looking at import dependencies within the Flutter package:$reset' | |
| else | |
| '${bold}Multiple errors were detected when looking at import dependencies within the Flutter package:$reset', | |
| ...errors, | |
| ]); | |
| } | |
| } | |
| Future<void> verifyInternationalizations(String workingDirectory, String dartExecutable) async { | |
| final EvalResult materialGenResult = await _evalCommand(dartExecutable, <String>[ | |
| path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'), | |
| '--material', | |
| '--remove-undefined', | |
| ], workingDirectory: workingDirectory); | |
| final EvalResult cupertinoGenResult = await _evalCommand(dartExecutable, <String>[ | |
| path.join('dev', 'tools', 'localization', 'bin', 'gen_localizations.dart'), | |
| '--cupertino', | |
| '--remove-undefined', | |
| ], workingDirectory: workingDirectory); | |
| final String materialLocalizationsFile = path.join( | |
| workingDirectory, | |
| 'packages', | |
| 'flutter_localizations', | |
| 'lib', | |
| 'src', | |
| 'l10n', | |
| 'generated_material_localizations.dart', | |
| ); | |
| final String cupertinoLocalizationsFile = path.join( | |
| workingDirectory, | |
| 'packages', | |
| 'flutter_localizations', | |
| 'lib', | |
| 'src', | |
| 'l10n', | |
| 'generated_cupertino_localizations.dart', | |
| ); | |
| final String expectedMaterialResult = await File(materialLocalizationsFile).readAsString(); | |
| final String expectedCupertinoResult = await File(cupertinoLocalizationsFile).readAsString(); | |
| if (materialGenResult.stdout.trim() != expectedMaterialResult.trim()) { | |
| foundError(<String>[ | |
| '<<<<<<< $materialLocalizationsFile', | |
| expectedMaterialResult.trim(), | |
| '=======', | |
| materialGenResult.stdout.trim(), | |
| '>>>>>>> gen_localizations', | |
| 'The contents of $materialLocalizationsFile are different from that produced by gen_localizations.', | |
| '', | |
| 'Did you forget to run gen_localizations.dart after updating a .arb file?', | |
| ]); | |
| } | |
| if (cupertinoGenResult.stdout.trim() != expectedCupertinoResult.trim()) { | |
| foundError(<String>[ | |
| '<<<<<<< $cupertinoLocalizationsFile', | |
| expectedCupertinoResult.trim(), | |
| '=======', | |
| cupertinoGenResult.stdout.trim(), | |
| '>>>>>>> gen_localizations', | |
| 'The contents of $cupertinoLocalizationsFile are different from that produced by gen_localizations.', | |
| '', | |
| 'Did you forget to run gen_localizations.dart after updating a .arb file?', | |
| ]); | |
| } | |
| } | |
| Future<void> verifyStockAppLocalizations(String workingDirectory) async { | |
| final appRoot = Directory( | |
| path.join(workingDirectory, 'dev', 'benchmarks', 'test_apps', 'stocks'), | |
| ); | |
| if (!appRoot.existsSync()) { | |
| foundError(<String>['Stocks app does not exist at expected location: ${appRoot.path}']); | |
| } | |
| // Regenerate the localizations. | |
| final String flutterExecutable = path.join( | |
| workingDirectory, | |
| 'bin', | |
| 'flutter${Platform.isWindows ? '.bat' : ''}', | |
| ); | |
| await _evalCommand(flutterExecutable, const <String>['gen-l10n'], workingDirectory: appRoot.path); | |
| final i10nDirectory = Directory(path.join(appRoot.path, 'lib', 'i18n')); | |
| if (!i10nDirectory.existsSync()) { | |
| foundError(<String>[ | |
| 'Localization files for stocks app not found at expected location: ${i10nDirectory.path}', | |
| ]); | |
| } | |
| // Check that regeneration did not dirty the tree. | |
| final EvalResult result = await _evalCommand('git', <String>[ | |
| 'diff', | |
| '--name-only', | |
| '--exit-code', | |
| i10nDirectory.path, | |
| ], workingDirectory: workingDirectory); | |
| if (result.exitCode == 1) { | |
| foundError(<String>[ | |
| 'The following localization files for the stocks app appear to be out of date:', | |
| ...(const LineSplitter().convert(result.stdout).map((String line) => ' * $line')), | |
| 'Run "flutter gen-l10n" in "${path.relative(appRoot.path, from: workingDirectory)}" to regenerate.', | |
| ]); | |
| } else if (result.exitCode != 0) { | |
| foundError(<String>[ | |
| 'Failed to run "git diff" on localization files of stocks app:', | |
| result.stderr, | |
| ]); | |
| } | |
| } | |
| /// Verifies that all instances of "checked mode" have been migrated to "debug mode". | |
| Future<void> verifyNoCheckedMode(String workingDirectory) async { | |
| final String flutterPackages = path.join(workingDirectory, 'packages'); | |
| final List<File> files = await _allFiles( | |
| flutterPackages, | |
| 'dart', | |
| minimumMatches: 400, | |
| ).where((File file) => path.extension(file.path) == '.dart').toList(); | |
| final problems = <String>[]; | |
| for (final file in files) { | |
| var lineCount = 0; | |
| for (final String line in file.readAsLinesSync()) { | |
| if (line.toLowerCase().contains('checked mode')) { | |
| problems.add( | |
| '${file.path}:$lineCount uses deprecated "checked mode" instead of "debug mode".', | |
| ); | |
| } | |
| lineCount += 1; | |
| } | |
| } | |
| if (problems.isNotEmpty) { | |
| foundError(problems); | |
| } | |
| } | |
| Future<void> verifyNoTrailingSpaces(String workingDirectory, {int minimumMatches = 4000}) async { | |
| final List<File> files = await _allFiles(workingDirectory, null, minimumMatches: minimumMatches) | |
| .where((File file) => path.basename(file.path) != 'serviceaccount.enc') | |
| .where((File file) => path.basename(file.path) != 'Ahem.ttf') | |
| .where((File file) => path.extension(file.path) != '.snapshot') | |
| .where((File file) => path.extension(file.path) != '.png') | |
| .where((File file) => path.extension(file.path) != '.jpg') | |
| .where((File file) => path.extension(file.path) != '.ico') | |
| .where((File file) => path.extension(file.path) != '.jar') | |
| .where((File file) => path.extension(file.path) != '.swp') | |
| .toList(); | |
| final problems = <String>[]; | |
| for (final file in files) { | |
| final List<String> lines = file.readAsLinesSync(); | |
| for (var index = 0; index < lines.length; index += 1) { | |
| if (lines[index].endsWith(' ')) { | |
| problems.add('${file.path}:${index + 1}: trailing U+0020 space character'); | |
| } else if (lines[index].endsWith('\t')) { | |
| problems.add('${file.path}:${index + 1}: trailing U+0009 tab character'); | |
| } | |
| } | |
| if (lines.isNotEmpty && lines.last == '') { | |
| problems.add('${file.path}:${lines.length}: trailing blank line'); | |
| } | |
| } | |
| if (problems.isNotEmpty) { | |
| foundError(problems); | |
| } | |
| } | |
| final RegExp _flowControlStatementWithoutSpace = RegExp( | |
| r'(^|[ \t])(if|switch|for|do|while|catch)\(', | |
| multiLine: true, | |
| ); | |
| Future<void> verifySpacesAfterFlowControlStatements( | |
| String workingDirectory, { | |
| int minimumMatches = 4000, | |
| }) async { | |
| const extensions = <String>{ | |
| // .dart omitted from this list because the Dart auto formatter ensures | |
| // spaces after flow control statements. | |
| '.java', | |
| '.js', | |
| '.kt', | |
| '.swift', | |
| '.c', | |
| '.cc', | |
| '.cpp', | |
| '.h', | |
| '.m', | |
| }; | |
| final List<File> files = await _allFiles( | |
| workingDirectory, | |
| null, | |
| minimumMatches: minimumMatches, | |
| ).where((File file) => extensions.contains(path.extension(file.path))).toList(); | |
| final problems = <String>[]; | |
| for (final file in files) { | |
| final List<String> lines = file.readAsLinesSync(); | |
| for (var index = 0; index < lines.length; index += 1) { | |
| if (lines[index].contains(_flowControlStatementWithoutSpace)) { | |
| problems.add('${file.path}:${index + 1}: no space after flow control statement'); | |
| } | |
| } | |
| } | |
| if (problems.isNotEmpty) { | |
| foundError(problems); | |
| } | |
| } | |
| @immutable | |
| class Hash256 { | |
| const Hash256(this.a, this.b, this.c, this.d); | |
| factory Hash256.fromDigest(Digest digest) { | |
| assert(digest.bytes.length == 32); | |
| return Hash256( | |
| digest.bytes[0] << 56 | | |
| digest.bytes[1] << 48 | | |
| digest.bytes[2] << 40 | | |
| digest.bytes[3] << 32 | | |
| digest.bytes[4] << 24 | | |
| digest.bytes[5] << 16 | | |
| digest.bytes[6] << 8 | | |
| digest.bytes[7] << 0, | |
| digest.bytes[8] << 56 | | |
| digest.bytes[9] << 48 | | |
| digest.bytes[10] << 40 | | |
| digest.bytes[11] << 32 | | |
| digest.bytes[12] << 24 | | |
| digest.bytes[13] << 16 | | |
| digest.bytes[14] << 8 | | |
| digest.bytes[15] << 0, | |
| digest.bytes[16] << 56 | | |
| digest.bytes[17] << 48 | | |
| digest.bytes[18] << 40 | | |
| digest.bytes[19] << 32 | | |
| digest.bytes[20] << 24 | | |
| digest.bytes[21] << 16 | | |
| digest.bytes[22] << 8 | | |
| digest.bytes[23] << 0, | |
| digest.bytes[24] << 56 | | |
| digest.bytes[25] << 48 | | |
| digest.bytes[26] << 40 | | |
| digest.bytes[27] << 32 | | |
| digest.bytes[28] << 24 | | |
| digest.bytes[29] << 16 | | |
| digest.bytes[30] << 8 | | |
| digest.bytes[31] << 0, | |
| ); | |
| } | |
| final int a; | |
| final int b; | |
| final int c; | |
| final int d; | |
| @override | |
| bool operator ==(Object other) { | |
| if (other.runtimeType != runtimeType) { | |
| return false; | |
| } | |
| return other is Hash256 && other.a == a && other.b == b && other.c == c && other.d == d; | |
| } | |
| @override | |
| int get hashCode => Object.hash(a, b, c, d); | |
| } | |
| // DO NOT ADD ANY ENTRIES TO THIS LIST. | |
| // We have a policy of not checking in binaries into this repository. | |
| // If you are adding/changing template images, use the flutter_template_images | |
| // package and a .img.tmpl placeholder instead. | |
| // If you have other binaries to add, please consult johnmccutchan for advice. | |
| final Set<Hash256> _legacyBinaries = <Hash256>{ | |
| // DEFAULT ICON IMAGES | |
| // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-hdpi/ic_launcher.png | |
| // packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/res/mipmap-hdpi/ic_launcher.png | |
| // (also used by many examples) | |
| const Hash256(0x6A7C8F0D703E3682, 0x108F9662F8133022, 0x36240D3F8F638BB3, 0x91E32BFB96055FEF), | |
| // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-mdpi/ic_launcher.png | |
| // (also used by many examples) | |
| const Hash256(0xC7C0C0189145E4E3, 0x2A401C61C9BDC615, 0x754B0264E7AFAE24, 0xE834BB81049EAF81), | |
| // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xhdpi/ic_launcher.png | |
| // (also used by many examples) | |
| const Hash256(0xE14AA40904929BF3, 0x13FDED22CF7E7FFC, 0xBF1D1AAC4263B5EF, 0x1BE8BFCE650397AA), | |
| // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xxhdpi/ic_launcher.png | |
| // (also used by many examples) | |
| const Hash256(0x4D470BF22D5C17D8, 0x4EDC5F82516D1BA8, 0xA1C09559CD761CEF, 0xB792F86D9F52B540), | |
| // packages/flutter_tools/templates/app/android.tmpl/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | |
| // (also used by many examples) | |
| const Hash256(0x3C34E1F298D0C9EA, 0x3455D46DB6B7759C, 0x8211A49E9EC6E44B, 0x635FC5C87DFB4180), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | |
| // (also used by a few examples) | |
| const Hash256(0x7770183009E91411, 0x2DE7D8EF1D235A6A, 0x30C5834424858E0D, 0x2F8253F6B8D31926), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png | |
| // (also used by many examples) | |
| const Hash256(0x5925DAB509451F9E, 0xCBB12CE8A625F9D4, 0xC104718EE20CAFF8, 0xB1B51032D1CD8946), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png | |
| // (also used by many examples) | |
| const Hash256(0xC4D9A284C12301D0, 0xF50E248EC53ED51A, 0x19A10147B774B233, 0x08399250B0D44C55), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png | |
| // (also used by many examples) | |
| const Hash256(0xBF97F9D3233F33E1, 0x389B09F7B8ADD537, 0x41300CB834D6C7A5, 0xCA32CBED363A4FB2), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png | |
| // (also used by many examples) | |
| const Hash256(0x285442F69A06B45D, 0x9D79DF80321815B5, 0x46473548A37B7881, 0x9B68959C7B8ED237), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png | |
| // (also used by many examples) | |
| const Hash256(0x2AB64AF8AC727EA9, 0x9C6AB9EAFF847F46, 0xFBF2A9A0A78A0ABC, 0xBF3180F3851645B4), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png | |
| // (also used by many examples) | |
| const Hash256(0x9DCA09F4E5ED5684, 0xD3C4DFF41F4E8B7C, 0xB864B438172D72BE, 0x069315FA362930F9), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png | |
| // (also used by many examples) | |
| const Hash256(0xD5AD04DE321EF37C, 0xACC5A7B960AFCCE7, 0x1BDCB96FA020C482, 0x49C1545DD1A0F497), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png | |
| // (also used by many examples) | |
| const Hash256(0x809ABFE75C440770, 0xC13C4E2E46D09603, 0xC22053E9D4E0E227, 0x5DCB9C1DCFBB2C75), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png | |
| // (also used by many examples) | |
| const Hash256(0x3DB08CB79E7B01B9, 0xE81F956E3A0AE101, 0x48D0FAFDE3EA7AA7, 0x0048DF905AA52CFD), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png | |
| // (also used by many examples) | |
| const Hash256(0x23C13D463F5DCA5C, 0x1F14A14934003601, 0xC29F1218FD461016, 0xD8A22CEF579A665F), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png | |
| // (also used by many examples) | |
| const Hash256(0x6DB7726530D71D3F, 0x52CB59793EB69131, 0x3BAA04796E129E1E, 0x043C0A58A1BFFD2F), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png | |
| // (also used by many examples) | |
| const Hash256(0xCEE565F5E6211656, 0x9B64980B209FD5CA, 0x4B3D3739011F5343, 0x250B33A1A2C6EB65), | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png | |
| // packages/flutter_tools/templates/app/ios.tmpl/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png | |
| // packages/flutter_tools/templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png | |
| // (also used by many examples) | |
| const Hash256(0x93AE7D494FAD0FB3, 0x0CBF3AE746A39C4B, 0xC7A0F8BBF87FBB58, 0x7A3F3C01F3C5CE20), | |
| // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png | |
| // (also used by a few examples) | |
| const Hash256(0xB18BEBAAD1AD6724, 0xE48BCDF699BA3927, 0xDF3F258FEBE646A3, 0xAB5C62767C6BAB40), | |
| // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png | |
| // (also used by a few examples) | |
| const Hash256(0xF90D839A289ECADB, 0xF2B0B3400DA43EB8, 0x08B84908335AE4A0, 0x07457C4D5A56A57C), | |
| // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png | |
| // (also used by a few examples) | |
| const Hash256(0x592C2ABF84ADB2D3, 0x91AED8B634D3233E, 0x2C65369F06018DCD, 0x8A4B27BA755EDCBE), | |
| // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png | |
| // (also used by a few examples) | |
| const Hash256(0x75D9A0C034113CA8, 0xA1EC11C24B81F208, 0x6630A5A5C65C7D26, 0xA5DC03A1C0A4478C), | |
| // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png | |
| // (also used by a few examples) | |
| const Hash256(0xA896E65745557732, 0xC72BD4EE3A10782F, 0xE2AA95590B5AF659, 0x869E5808DB9C01C1), | |
| // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png | |
| // (also used by a few examples) | |
| const Hash256(0x3A69A8A1AAC5D9A8, 0x374492AF4B6D07A4, 0xCE637659EB24A784, 0x9C4DFB261D75C6A3), | |
| // packages/flutter_tools/templates/app/macos.tmpl/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png | |
| // (also used by a few examples) | |
| const Hash256(0xD29D4E0AF9256DC9, 0x2D0A8F8810608A5E, 0x64A132AD8B397CA2, 0xC4DDC0B1C26A68C3), | |
| // packages/flutter_tools/templates/app/web/icons/Icon-192.png.copy.tmpl | |
| // dev/integration_tests/flutter_gallery/web/icons/Icon-192.png | |
| const Hash256(0x3DCE99077602F704, 0x21C1C6B2A240BC9B, 0x83D64D86681D45F2, 0x154143310C980BE3), | |
| // packages/flutter_tools/templates/app/web/icons/Icon-512.png.copy.tmpl | |
| // dev/integration_tests/flutter_gallery/web/icons/Icon-512.png | |
| const Hash256(0xBACCB205AE45f0B4, 0x21BE1657259B4943, 0xAC40C95094AB877F, 0x3BCBE12CD544DCBE), | |
| // packages/flutter_tools/templates/app/web/favicon.png.copy.tmpl | |
| // dev/integration_tests/flutter_gallery/web/favicon.png | |
| const Hash256(0x7AB2525F4B86B65D, 0x3E4C70358A17E5A1, 0xAAF6F437f99CBCC0, 0x46DAD73d59BB9015), | |
| // GALLERY ICONS | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_background.png | |
| const Hash256(0x03CFDE53C249475C, 0x277E8B8E90AC8A13, 0xE5FC13C358A94CCB, 0x67CA866C9862A0DD), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_foreground.png | |
| const Hash256(0x86A83E23A505EFCC, 0x39C358B699EDE12F, 0xC088EE516A1D0C73, 0xF3B5D74DDAD164B1), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-hdpi/ic_launcher.png | |
| const Hash256(0xD813B1A77320355E, 0xB68C485CD47D0F0F, 0x3C7E1910DCD46F08, 0x60A6401B8DC13647), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_background.png | |
| const Hash256(0x35AFA76BD5D6053F, 0xEE927436C78A8794, 0xA8BA5F5D9FC9653B, 0xE5B96567BB7215ED), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_foreground.png | |
| const Hash256(0x263CE9B4F1F69B43, 0xEBB08AE9FE8F80E7, 0x95647A59EF2C040B, 0xA8AEB246861A7DFF), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png | |
| const Hash256(0x5E1A93C3653BAAFF, 0x1AAC6BCEB8DCBC2F, 0x2AE7D68ECB07E507, 0xCB1FA8354B28313A), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_background.png | |
| const Hash256(0xA5C77499151DDEC6, 0xDB40D0AC7321FD74, 0x0646C0C0F786743F, 0x8F3C3C408CAC5E8C), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_foreground.png | |
| const Hash256(0x33DE450980A2A16B, 0x1982AC7CDC1E7B01, 0x919E07E0289C2139, 0x65F85BCED8895FEF), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png | |
| const Hash256(0xC3B8577F4A89BA03, 0x830944FB06C3566B, 0x4C99140A2CA52958, 0x089BFDC3079C59B7), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_background.png | |
| const Hash256(0xDEBC241D6F9C5767, 0x8980FDD46FA7ED0C, 0x5B8ACD26BCC5E1BC, 0x473C89B432D467AD), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_foreground.png | |
| const Hash256(0xBEFE5F7E82BF8B64, 0x148D869E3742004B, 0xF821A9F5A1BCDC00, 0x357D246DCC659DC2), | |
| // dev/integration_tests/flutter_gallery/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | |
| const Hash256(0xC385404341FF9EDD, 0x30FBE76F0EC99155, 0x8EA4F4AFE8CC0C60, 0x1CA3EDEF177E1DA8), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-1024.png | |
| const Hash256(0x6BE5751A29F57A80, 0x36A4B31CC542C749, 0x984E49B22BD65CAA, 0x75AE8B2440848719), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-120.png | |
| const Hash256(0x9972A2264BFA8F8D, 0x964AFE799EADC1FA, 0x2247FB31097F994A, 0x1495DC32DF071793), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-152.png | |
| const Hash256(0x4C7CC9B09BEEDA24, 0x45F57D6967753910, 0x57D68E1A6B883D2C, 0x8C52701A74F1400F), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-167.png | |
| const Hash256(0x66DACAC1CFE4D349, 0xDBE994CB9125FFD7, 0x2D795CFC9CF9F739, 0xEDBB06CE25082E9C), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-180.png | |
| const Hash256(0x5188621015EBC327, 0xC9EF63AD76E60ECE, 0xE82BDC3E4ABF09E2, 0xEE0139FA7C0A2BE5), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20.png | |
| const Hash256(0x27D2752D04EE9A6B, 0x78410E208F74A6CD, 0xC90D9E03B73B8C60, 0xD05F7D623E790487), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29.png | |
| const Hash256(0xBB20556B2826CF85, 0xD5BAC73AA69C2AC3, 0x8E71DAD64F15B855, 0xB30CB73E0AF89307), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-40.png | |
| const Hash256(0x623820FA45CDB0AC, 0x808403E34AD6A53E, 0xA3E9FDAE83EE0931, 0xB020A3A4EF2CDDE7), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-58.png | |
| const Hash256(0xC6D631D1E107215E, 0xD4A58FEC5F3AA4B5, 0x0AE9724E07114C0C, 0x453E5D87C2CAD3B3), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60.png | |
| const Hash256(0x4B6F58D1EB8723C6, 0xE717A0D09FEC8806, 0x90C6D1EF4F71836E, 0x618672827979B1A2), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png | |
| const Hash256(0x0A1744CC7634D508, 0xE85DD793331F0C8A, 0x0B7C6DDFE0975D8F, 0x29E91C905BBB1BED), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-80.png | |
| const Hash256(0x24032FBD1E6519D6, 0x0BA93C0D5C189554, 0xF50EAE23756518A2, 0x3FABACF4BD5DAF08), | |
| // dev/integration_tests/flutter_gallery/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-87.png | |
| const Hash256(0xC17BAE6DF6BB234A, 0xE0AF4BEB0B805F12, 0x14E74EB7AA9A30F1, 0x5763689165DA7DDF), | |
| // STOCKS ICONS | |
| // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-hdpi/ic_launcher.png | |
| const Hash256(0x74052AB5241D4418, 0x7085180608BC3114, 0xD12493C50CD8BBC7, 0x56DED186C37ACE84), | |
| // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-mdpi/ic_launcher.png | |
| const Hash256(0xE37947332E3491CB, 0x82920EE86A086FEA, 0xE1E0A70B3700A7DA, 0xDCAFBDD8F40E2E19), | |
| // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png | |
| const Hash256(0xE608CDFC0C8579FB, 0xE38873BAAF7BC944, 0x9C9D2EE3685A4FAE, 0x671EF0C8BC41D17C), | |
| // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png | |
| const Hash256(0xBD53D86977DF9C54, 0xF605743C5ABA114C, 0x9D51D1A8BB917E1A, 0x14CAA26C335CAEBD), | |
| // dev/benchmarks/test_apps/stocks/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png | |
| const Hash256(0x64E4D02262C4F3D0, 0xBB4FDC21CD0A816C, 0x4CD2A0194E00FB0F, 0x1C3AE4142FAC0D15), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png | |
| const Hash256(0x5BA3283A76918FC0, 0xEE127D0F22D7A0B6, 0xDF03DAED61669427, 0x93D89DDD87A08117), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png | |
| const Hash256(0xCD7F26ED31DEA42A, 0x535D155EC6261499, 0x34E6738255FDB2C4, 0xBD8D4BDDE9A99B05), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png | |
| const Hash256(0x3FA1225FC9A96A7E, 0xCD071BC42881AB0E, 0x7747EB72FFB72459, 0xA37971BBAD27EE24), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png | |
| const Hash256(0xCD867001ACD7BBDB, 0x25CDFD452AE89FA2, 0x8C2DC980CAF55F48, 0x0B16C246CFB389BC), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png | |
| const Hash256(0x848E9736E5C4915A, 0x7945BCF6B32FD56B, 0x1F1E7CDDD914352E, 0xC9681D38EF2A70DA), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification.png | |
| const Hash256(0x654BA7D6C4E05CA0, 0x7799878884EF8F11, 0xA383E1F24CEF5568, 0x3C47604A966983C8), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@2x.png | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png | |
| const Hash256(0x743056FE7D83FE42, 0xA2990825B6AD0415, 0x1AF73D0D43B227AA, 0x07EBEA9B767381D9), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Notification@3x.png | |
| const Hash256(0xA7E1570812D119CF, 0xEF4B602EF28DD0A4, 0x100D066E66F5B9B9, 0x881765DC9303343B), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png | |
| const Hash256(0xB4102839A1E41671, 0x62DACBDEFA471953, 0xB1EE89A0AB7594BE, 0x1D9AC1E67DC2B2CE), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png | |
| const Hash256(0x70AC6571B593A967, 0xF1CBAEC9BC02D02D, 0x93AD766D8290ADE6, 0x840139BF9F219019), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png | |
| const Hash256(0x5D87A78386DA2C43, 0xDDA8FEF2CA51438C, 0xE5A276FE28C6CF0A, 0xEBE89085B56665B6), | |
| // dev/benchmarks/test_apps/stocks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png | |
| const Hash256(0x4D9F5E81F668DA44, 0xB20A77F8BF7BA2E1, 0xF384533B5AD58F07, 0xB3A2F93F8635CD96), | |
| // LEGACY ICONS | |
| // dev/benchmarks/complex_layout/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png | |
| // dev/benchmarks/microbenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png | |
| // examples/flutter_view/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png | |
| // (not really sure where this came from, or why neither the template nor most examples use them) | |
| const Hash256(0x6E645DC9ED913AAD, 0xB50ED29EEB16830D, 0xB32CA12F39121DB9, 0xB7BC1449DDDBF8B8), | |
| // dev/benchmarks/macrobenchmarks/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | |
| // dev/integration_tests/codegen/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | |
| // dev/integration_tests/ios_add2app/ios_add2app/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | |
| // dev/integration_tests/release_smoke_test/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | |
| const Hash256(0xDEFAC77E08EC71EC, 0xA04CCA3C95D1FC33, 0xB9F26E1CB15CB051, 0x47DEFC79CDD7C158), | |
| // examples/flutter_view/ios/Runner/ic_add.png | |
| // examples/platform_view/ios/Runner/ic_add.png | |
| const Hash256(0x3CCE7450334675E2, 0xE3AABCA20B028993, 0x127BE82FE0EB3DFF, 0x8B027B3BAF052F2F), | |
| // examples/image_list/images/coast.jpg | |
| const Hash256(0xDA957FD30C51B8D2, 0x7D74C2C918692DC4, 0xD3C5C99BB00F0D6B, 0x5EBB30395A6EDE82), | |
| // examples/image_list/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png | |
| const Hash256(0xB5792CA06F48A431, 0xD4379ABA2160BD5D, 0xE92339FC64C6A0D3, 0x417AA359634CD905), | |
| // TEST ASSETS | |
| // dev/benchmarks/macrobenchmarks/assets/999x1000.png | |
| const Hash256(0x553E9C36DFF3E610, 0x6A608BDE822A0019, 0xDE4F1769B6FBDB97, 0xBC3C20E26B839F59), | |
| // dev/bots/test/analyze-test-input/root/packages/foo/serviceaccount.enc | |
| const Hash256(0xA8100AE6AA1940D0, 0xB663BB31CD466142, 0xEBBDBD5187131B92, 0xD93818987832EB89), | |
| // dev/automated_tests/icon/test.png | |
| const Hash256(0xE214B4A0FEEEC6FA, 0x8E7AA8CC9BFBEC40, 0xBCDAC2F2DEBC950F, 0x75AF8EBF02BCE459), | |
| // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/drawable-land-xxhdpi/flutter_splash_screen.png | |
| // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/mipmap-land-xxhdpi/flutter_splash_screen.png | |
| const Hash256(0x2D4F8D7A3DFEF9D3, 0xA0C66938E169AB58, 0x8C6BBBBD1973E34E, 0x03C428416D010182), | |
| // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/drawable-xxhdpi/flutter_splash_screen.png | |
| // dev/integration_tests/android_splash_screens/splash_screen_kitchen_sink/android/app/src/main/res/mipmap-xxhdpi/flutter_splash_screen.png | |
| const Hash256(0xCD46C01BAFA3B243, 0xA6AA1645EEDDE481, 0x143AC8ABAB1A0996, 0x22CAA9D41F74649A), | |
| // dev/integration_tests/flutter_driver_screenshot_test/assets/red_square.png | |
| const Hash256(0x40054377E1E084F4, 0x4F4410CE8F44C210, 0xABA945DFC55ED0EF, 0x23BDF9469E32F8D3), | |
| // dev/integration_tests/flutter_driver_screenshot_test/test_driver/goldens/red_square_image/iPhone7,2.png | |
| const Hash256(0x7F9D27C7BC418284, 0x01214E21CA886B2F, 0x40D9DA2B31AE7754, 0x71D68375F9C8A824), | |
| // examples/flutter_view/assets/flutter-mark-square-64.png | |
| // examples/platform_view/assets/flutter-mark-square-64.png | |
| const Hash256(0xF416B0D8AC552EC8, 0x819D1F492D1AB5E6, 0xD4F20CF45DB47C22, 0x7BB431FEFB5B67B2), | |
| // packages/flutter_tools/test/data/intellij/plugins/Dart/lib/Dart.jar | |
| const Hash256(0x576E489D788A13DB, 0xBF40E4A39A3DAB37, 0x15CCF0002032E79C, 0xD260C69B29E06646), | |
| // packages/flutter_tools/test/data/intellij/plugins/flutter-intellij.jar | |
| const Hash256(0x4C67221E25626CB2, 0x3F94E1F49D34E4CF, 0x3A9787A514924FC5, 0x9EF1E143E5BC5690), | |
| // MISCELLANEOUS | |
| // dev/bots/serviceaccount.enc | |
| const Hash256(0x1F19ADB4D80AFE8C, 0xE61899BA776B1A8D, 0xCA398C75F5F7050D, 0xFB0E72D7FBBBA69B), | |
| // dev/docs/favicon.ico | |
| const Hash256(0x67368CA1733E933A, 0xCA3BC56EF0695012, 0xE862C371AD4412F0, 0x3EC396039C609965), | |
| // dev/snippets/assets/code_sample.png | |
| const Hash256(0xAB2211A47BDA001D, 0x173A52FD9C75EBC7, 0xE158942FFA8243AD, 0x2A148871990D4297), | |
| // dev/snippets/assets/code_snippet.png | |
| const Hash256(0xDEC70574DA46DFBB, 0xFA657A771F3E1FBD, 0xB265CFC6B2AA5FE3, 0x93BA4F325D1520BA), | |
| // packages/flutter_tools/static/Ahem.ttf | |
| const Hash256(0x63D2ABD0041C3E3B, 0x4B52AD8D382353B5, 0x3C51C6785E76CE56, 0xED9DACAD2D2E31C4), | |
| }; | |
| Future<void> verifyNoBinaries(String workingDirectory, {Set<Hash256>? legacyBinaries}) async { | |
| // Please do not add anything to the _legacyBinaries set above. | |
| // We have a policy of not checking in binaries into this repository. | |
| // If you are adding/changing template images, use the flutter_template_images | |
| // package and a .img.tmpl placeholder instead. | |
| // If you have other binaries to add, please consult johnmccutchan for advice. | |
| assert( | |
| _legacyBinaries | |
| .expand<int>((Hash256 hash) => <int>[hash.a, hash.b, hash.c, hash.d]) | |
| .reduce((int value, int element) => value ^ element) == | |
| 0x606B51C908B40BFA, // Please do not modify this line. | |
| ); | |
| legacyBinaries ??= _legacyBinaries; | |
| if (!Platform.isWindows) { | |
| // TODO(ianh): Port this to Windows | |
| final List<File> files = await _gitFiles(workingDirectory); | |
| final problems = <String>[]; | |
| for (final file in files) { | |
| if (FileSystemEntity.isLinkSync(file.path)) { | |
| continue; // Skip symlinks. | |
| } | |
| final Uint8List bytes = file.readAsBytesSync(); | |
| try { | |
| utf8.decode(bytes); | |
| } on FormatException catch (error) { | |
| final Digest digest = sha256.convert(bytes); | |
| if (!legacyBinaries.contains(Hash256.fromDigest(digest))) { | |
| problems.add('${file.path}:${error.offset}: file is not valid UTF-8'); | |
| } | |
| } | |
| } | |
| if (problems.isNotEmpty) { | |
| foundError(<String>[ | |
| ...problems, | |
| 'All files in this repository must be UTF-8. In particular, images and other binaries', | |
| 'must not be checked into this repository. This is because we are very sensitive to the', | |
| 'size of the repository as it is distributed to all our developers. If you have a binary', | |
| 'to which you need access, you should consider how to fetch it from another repository;', | |
| 'for example, the "assets-for-api-docs" repository is used for images in API docs.', | |
| 'To add assets to flutter_tools templates, see the instructions in the wiki:', | |
| 'https://github.com/flutter/flutter/blob/main/docs/tool/Managing-template-image-assets.md', | |
| ]); | |
| } | |
| } | |
| } | |
| // UTILITY FUNCTIONS | |
| bool _listEquals<T>(List<T> a, List<T> b) { | |
| if (a.length != b.length) { | |
| return false; | |
| } | |
| for (var index = 0; index < a.length; index += 1) { | |
| if (a[index] != b[index]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| Future<List<File>> _gitFiles(String workingDirectory, {bool runSilently = true}) async { | |
| final EvalResult evalResult = await _evalCommand( | |
| 'git', | |
| <String>['ls-files', '-z'], | |
| workingDirectory: workingDirectory, | |
| runSilently: runSilently, | |
| ); | |
| if (evalResult.exitCode != 0) { | |
| foundError(<String>[ | |
| 'git ls-files failed with exit code ${evalResult.exitCode}', | |
| '${bold}stdout:$reset', | |
| evalResult.stdout, | |
| '${bold}stderr:$reset', | |
| evalResult.stderr, | |
| ]); | |
| } | |
| final List<String> filenames = evalResult.stdout.split('\x00'); | |
| assert(filenames.last.isEmpty); // git ls-files gives a trailing blank 0x00 | |
| filenames.removeLast(); | |
| return filenames | |
| .where((String filename) => !filename.startsWith('engine/')) | |
| .map<File>((String filename) => File(path.join(workingDirectory, filename))) | |
| .toList(); | |
| } | |
| Stream<File> _allFiles( | |
| String workingDirectory, | |
| String? extension, { | |
| required int minimumMatches, | |
| }) async* { | |
| assert( | |
| extension == null || !extension.startsWith('.'), | |
| 'Extension argument should not start with a period.', | |
| ); | |
| final List<File> gitFiles = [ | |
| for (final File f in await _gitFiles(workingDirectory)) File(path.normalize(f.path)), | |
| ]; | |
| final Set<String> dartIgnoreDirectories = gitFiles | |
| .where((File f) => path.basename(f.path) == '.dartignore') | |
| .map((File f) => path.dirname(f.path)) | |
| .toSet(); | |
| const skipDirectories = {'.git', '.idea', '.gradle', '.dart_tool', 'build'}; | |
| var matches = 0; | |
| for (final file in gitFiles) { | |
| if (extension != null && path.extension(file.path) != '.$extension') { | |
| continue; | |
| } | |
| if (dartIgnoreDirectories.any((String d) => path.isWithin(d, file.path))) { | |
| continue; | |
| } | |
| final String relativePath = path.relative(file.path, from: workingDirectory); | |
| final List<String> components = path.split(relativePath); | |
| if (components.any((String c) => skipDirectories.contains(c) || c.endsWith('.tmpl'))) { | |
| continue; | |
| } | |
| if (_isGeneratedPluginRegistrant(file)) { | |
| continue; | |
| } | |
| switch (path.basename(file.path)) { | |
| case 'flutter_export_environment.sh' || 'gradlew.bat' || '.DS_Store': | |
| continue; | |
| } | |
| final FileStat stat = file.statSync(); | |
| if (stat.type != FileSystemEntityType.file) { | |
| continue; | |
| } | |
| matches += 1; | |
| yield file; | |
| } | |
| assert( | |
| matches >= minimumMatches, | |
| 'Expected to find at least $minimumMatches files with extension ".$extension" in "$workingDirectory", but only found $matches.', | |
| ); | |
| } | |
| class EvalResult { | |
| EvalResult({required this.stdout, required this.stderr, this.exitCode = 0}); | |
| static const kNotFoundExitCode = 127; | |
| final String stdout; | |
| final String stderr; | |
| final int exitCode; | |
| } | |
| // TODO(ianh): Refactor this to reuse the code in run_command.dart | |
| Future<EvalResult> _evalCommand( | |
| String executable, | |
| List<String> arguments, { | |
| required String workingDirectory, | |
| Map<String, String>? environment, | |
| bool allowNonZeroExit = false, | |
| bool runSilently = false, | |
| }) async { | |
| final commandDescription = | |
| '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; | |
| final String relativeWorkingDir = path.relative(workingDirectory); | |
| if (!runSilently) { | |
| print('RUNNING: cd $cyan$relativeWorkingDir$reset; $green$commandDescription$reset'); | |
| } | |
| final time = Stopwatch()..start(); | |
| final Process process; | |
| try { | |
| process = await Process.start( | |
| executable, | |
| arguments, | |
| workingDirectory: workingDirectory, | |
| environment: environment, | |
| ); | |
| } on ProcessException catch (e) { | |
| return EvalResult(stdout: '', stderr: e.toString(), exitCode: EvalResult.kNotFoundExitCode); | |
| } | |
| final Future<List<List<int>>> savedStdout = process.stdout.toList(); | |
| final Future<List<List<int>>> savedStderr = process.stderr.toList(); | |
| final int exitCode = await process.exitCode; | |
| final result = EvalResult( | |
| stdout: utf8.decode((await savedStdout).expand<int>((List<int> ints) => ints).toList()), | |
| stderr: utf8.decode((await savedStderr).expand<int>((List<int> ints) => ints).toList()), | |
| exitCode: exitCode, | |
| ); | |
| if (!runSilently) { | |
| print( | |
| 'ELAPSED TIME: $bold${prettyPrintDuration(time.elapsed)}$reset for $commandDescription in $relativeWorkingDir', | |
| ); | |
| } | |
| if (exitCode != 0 && !allowNonZeroExit) { | |
| foundError(<String>[ | |
| result.stderr, | |
| '${bold}ERROR:$red Last command exited with $exitCode.$reset', | |
| '${bold}Command:$red $commandDescription$reset', | |
| '${bold}Relative working directory:$red $relativeWorkingDir$reset', | |
| ]); | |
| } | |
| return result; | |
| } | |
| Future<void> _checkConsumerDependencies() async { | |
| const kCorePackages = <String>[ | |
| 'flutter', | |
| 'flutter_test', | |
| 'flutter_driver', | |
| 'flutter_localizations', | |
| 'integration_test', | |
| 'fuchsia_remote_debug_protocol', | |
| ]; | |
| final dependencies = <String>{}; | |
| // Parse the output of pub deps --json to determine all of the | |
| // current packages used by the core set of flutter packages. | |
| for (final package in kCorePackages) { | |
| final ProcessResult result = await Process.run(flutter, <String>[ | |
| 'pub', | |
| 'deps', | |
| '--json', | |
| '--directory=${path.join(flutterRoot, 'packages', package)}', | |
| ]); | |
| if (result.exitCode != 0) { | |
| foundError(<String>[result.stdout.toString(), result.stderr.toString()]); | |
| return; | |
| } | |
| final rawJson = json.decode(result.stdout as String) as Map<String, Object?>; | |
| final dependencyTree = <String, Map<String, Object?>>{ | |
| for (final Map<String, Object?> package | |
| in (rawJson['packages']! as List<Object?>).cast<Map<String, Object?>>()) | |
| package['name']! as String: package, | |
| }; | |
| final workset = <Map<String, Object?>>[]; | |
| workset.add(dependencyTree[package]!); | |
| while (workset.isNotEmpty) { | |
| final Map<String, Object?> currentPackage = workset.removeLast(); | |
| if (currentPackage['kind'] == 'dev') { | |
| continue; | |
| } | |
| dependencies.add(currentPackage['name']! as String); | |
| final List<String> currentDependencies = | |
| (currentPackage['directDependencies']! as List<Object?>).cast<String>(); | |
| for (final dependency in currentDependencies) { | |
| // Don't add dependencies we've already seen or we will get stuck | |
| // forever if there are any circular references. | |
| // TODO(dantup): Consider failing gracefully with the names of the | |
| // packages once the cycle between test_api and matcher is resolved. | |
| // https://github.com/dart-lang/test/issues/1979 | |
| if (!dependencies.contains(dependency)) { | |
| workset.add(dependencyTree[dependency]!); | |
| } | |
| } | |
| } | |
| } | |
| final Set<String> removed = kCorePackageAllowList.difference(dependencies); | |
| final Set<String> added = dependencies.difference(kCorePackageAllowList); | |
| String plural(int n, String s, String p) => n == 1 ? s : p; | |
| if (added.isNotEmpty) { | |
| foundError(<String>[ | |
| 'The transitive closure of package dependencies contains ${plural(added.length, "a non-allowlisted package", "non-allowlisted packages")}:', | |
| ' ${added.join(', ')}', | |
| 'We strongly desire to keep the number of dependencies to a minimum and', | |
| 'therefore would much prefer not to add new dependencies.', | |
| 'See dev/bots/allowlist.dart for instructions on how to update the package', | |
| 'allowlist if you nonetheless believe this is a necessary addition.', | |
| ]); | |
| } | |
| if (removed.isNotEmpty) { | |
| foundError(<String>[ | |
| 'Excellent news! ${plural(removed.length, "A package dependency has been removed!", "Multiple package dependencies have been removed!")}', | |
| ' ${removed.join(', ')}', | |
| 'To make sure we do not accidentally add ${plural(removed.length, "this dependency", "these dependencies")} back in the future,', | |
| 'please remove ${plural(removed.length, "this", "these")} packages from the allow-list in dev/bots/allowlist.dart.', | |
| 'Thanks!', | |
| ]); | |
| } | |
| } | |
| final Map<String, String> _kKotlinTemplateKeys = <String, String>{ | |
| 'androidIdentifier': 'dummyPackage', | |
| 'pluginClass': 'PluginClass', | |
| 'projectName': 'dummy', | |
| 'agpVersion': '0.0.0.1', | |
| 'kotlinVersion': '0.0.0.1', | |
| }; | |
| final String _kKotlinTemplateRelativePath = path.join('packages', 'flutter_tools', 'templates'); | |
| const List<String> _kKotlinExtList = <String>['.kt.tmpl', '.kts.tmpl']; | |
| const String _kKotlinTmplExt = '.tmpl'; | |
| final RegExp _kKotlinTemplatePattern = RegExp(r'{{(.*?)}}'); | |
| /// Copy kotlin template files from [_kKotlinTemplateRelativePath] into a system tmp folder | |
| /// then replace template values with values from [_kKotlinTemplateKeys] or "'dummy'" if an | |
| /// unknown key is found. Then run ktlint on the tmp folder to check for lint errors in the | |
| /// generated Kotlin files. | |
| Future<void> lintKotlinTemplatedFiles(String workingDirectory) async { | |
| final String templatePath = path.join(workingDirectory, _kKotlinTemplateRelativePath); | |
| final Iterable<File> files = Directory(templatePath) | |
| .listSync(recursive: true) | |
| .toList() | |
| .whereType<File>() | |
| .where((File file) => _kKotlinExtList.contains(path.extension(file.path, 2))) | |
| // Skip Pigeon-generated files, as they are intentionally not autoformatted | |
| // to minimize diffs when developers re-run Pigeon generation. | |
| .where((File file) => !path.basename(file.path).contains('.g.kt')); | |
| if (files.isEmpty) { | |
| foundError(<String>['No Kotlin template files found']); | |
| return; | |
| } | |
| final Directory tempDir = Directory.systemTemp.createTempSync('template_output'); | |
| for (final templateFile in files) { | |
| final String inputContent = await templateFile.readAsString(); | |
| final String modifiedContent = inputContent.replaceAllMapped( | |
| _kKotlinTemplatePattern, | |
| (Match match) => _kKotlinTemplateKeys[match[1]] ?? 'dummy', | |
| ); | |
| String outputFilename = path.basename(templateFile.path); | |
| outputFilename = outputFilename.substring( | |
| 0, | |
| outputFilename.length - _kKotlinTmplExt.length, | |
| ); // Remove '.tmpl' from file path | |
| // Ensure the first letter of the generated class is uppercase (instead of pluginClass) | |
| outputFilename = outputFilename.substring(0, 1).toUpperCase() + outputFilename.substring(1); | |
| final String relativePath = path.dirname(path.relative(templateFile.path, from: templatePath)); | |
| final String outputDir = path.join(tempDir.path, relativePath); | |
| await Directory(outputDir).create(recursive: true); | |
| final String outputFile = path.join(outputDir, outputFilename); | |
| final output = File(outputFile); | |
| await output.writeAsString(modifiedContent); | |
| } | |
| return lintKotlinFiles(tempDir.path).whenComplete(() { | |
| tempDir.deleteSync(recursive: true); | |
| }); | |
| } | |
| Future<void> lintKotlinFiles(String workingDirectory) async { | |
| const baselineRelativePath = 'dev/bots/test/analyze-test-input/ktlint-baseline.xml'; | |
| const editorConfigRelativePath = 'dev/bots/test/analyze-test-input/.editorconfig'; | |
| final EvalResult lintResult = await _evalCommand('ktlint', <String>[ | |
| '--baseline=$flutterRoot/$baselineRelativePath', | |
| '--editorconfig=$flutterRoot/$editorConfigRelativePath', | |
| ], workingDirectory: workingDirectory); | |
| if (lintResult.exitCode == EvalResult.kNotFoundExitCode) { | |
| foundError(<String>['Failed to find ktlint on PATH. Kotlin code analysis failed.']); | |
| } else if (lintResult.exitCode != 0) { | |
| final errorMessage = | |
| 'Found lint violations in Kotlin files:\n ${lintResult.stdout}\n\n' | |
| 'To reproduce this lint locally:\n' | |
| '1. Identify the CIPD version tag used to resolve this particular version of ktlint (check the dependencies section of this shard in the ci.yaml). \n' | |
| '2. Download that version from https://chrome-infra-packages.appspot.com/p/flutter/ktlint/linux-amd64/+/<version_tag>\n' | |
| '3. From the repository root, run `<path_to_ktlint>/ktlint --editorconfig=$editorConfigRelativePath --baseline=$baselineRelativePath`\n' | |
| 'Alternatively, if you use Android Studio, follow the docs at docs/platforms/android/Kotlin-android-studio-formatting.md to enable auto formatting.'; | |
| foundError(<String>[errorMessage]); | |
| } | |
| } | |
| const List<String> _kIgnoreList = <String>['Runner.rc.tmpl', 'flutter_window.cpp']; | |
| final String _kIntegrationTestsRelativePath = path.join('dev', 'integration_tests'); | |
| final String _kTemplateRelativePath = path.join( | |
| 'packages', | |
| 'flutter_tools', | |
| 'templates', | |
| 'app', | |
| 'windows.tmpl', | |
| 'runner', | |
| ); | |
| final String _kWindowsRunnerSubPath = path.join('windows', 'runner'); | |
| const String _kProjectNameKey = '{{projectName}}'; | |
| const String _kTmplExt = '.tmpl'; | |
| const String _kFlutterLicense = | |
| '// Copyright 2014 The Flutter Authors. All rights reserved.\n' | |
| '// Use of this source code is governed by a BSD-style license that can be\n' | |
| '// found in the LICENSE file.\n' | |
| '\n'; | |
| const String _kFlutterLicenseHtml = ''' | |
| <!-- Copyright 2014 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. --> | |
| '''; | |
| String _removeLicenseIfPresent(String fileContents) { | |
| if (fileContents.startsWith(_kFlutterLicense)) { | |
| return fileContents.substring(_kFlutterLicense.length); | |
| } | |
| if (fileContents.contains(_kFlutterLicenseHtml)) { | |
| return fileContents.replaceFirst(_kFlutterLicenseHtml, ''); | |
| } | |
| return fileContents; | |
| } | |
| Future<void> verifyIntegrationTestTemplateFiles(String flutterRoot) async { | |
| final errors = <String>[]; | |
| final String integrationTestsPath = path.join(flutterRoot, _kIntegrationTestsRelativePath); | |
| final String templatePath = path.join(flutterRoot, _kTemplateRelativePath); | |
| final Iterable<Directory> subDirs = Directory(integrationTestsPath) | |
| .listSync() | |
| .toList() | |
| .whereType<Directory>(); | |
| for (final testPath in subDirs) { | |
| final String projectName = path.basename(testPath.path); | |
| final String runnerPath = path.join(testPath.path, _kWindowsRunnerSubPath); | |
| final runner = Directory(runnerPath); | |
| if (!runner.existsSync()) { | |
| continue; | |
| } | |
| final Iterable<File> files = Directory(templatePath).listSync().toList().whereType<File>(); | |
| for (final templateFile in files) { | |
| final String fileName = path.basename(templateFile.path); | |
| if (_kIgnoreList.contains(fileName)) { | |
| continue; | |
| } | |
| String templateFileContents = templateFile.readAsLinesSync().join('\n'); | |
| String appFilePath = path.join(runnerPath, fileName); | |
| if (fileName.endsWith(_kTmplExt)) { | |
| appFilePath = appFilePath.substring( | |
| 0, | |
| appFilePath.length - _kTmplExt.length, | |
| ); // Remove '.tmpl' from app file path | |
| templateFileContents = templateFileContents.replaceAll( | |
| _kProjectNameKey, | |
| projectName, | |
| ); // Substitute template project name | |
| } | |
| String appFileContents = File(appFilePath).readAsLinesSync().join('\n'); | |
| appFileContents = _removeLicenseIfPresent(appFileContents); | |
| if (appFileContents != templateFileContents) { | |
| int indexOfDifference; | |
| for ( | |
| indexOfDifference = 0; | |
| indexOfDifference < appFileContents.length; | |
| indexOfDifference++ | |
| ) { | |
| if (indexOfDifference >= templateFileContents.length || | |
| templateFileContents.codeUnitAt(indexOfDifference) != | |
| appFileContents.codeUnitAt(indexOfDifference)) { | |
| break; | |
| } | |
| } | |
| final error = | |
| ''' | |
| Error: file $fileName mismatched for integration test $testPath | |
| Verify the integration test has been migrated to the latest app template. | |
| =====$appFilePath====== | |
| $appFileContents | |
| =====${templateFile.path}====== | |
| $templateFileContents | |
| ========== | |
| Diff at character #$indexOfDifference | |
| '''; | |
| errors.add(error); | |
| } | |
| } | |
| } | |
| if (errors.isNotEmpty) { | |
| foundError(errors); | |
| } | |
| } | |
| Future<CommandResult> _runFlutterAnalyze( | |
| String workingDirectory, { | |
| List<String> options = const <String>[], | |
| String? failureMessage, | |
| }) async { | |
| final stopwatch = Stopwatch()..start(); | |
| final heartbeatTimer = Timer.periodic(_kHeartbeatInterval, (Timer _) { | |
| print('Analysis in progress (${stopwatch.elapsed.inSeconds}s)...'); | |
| }); | |
| try { | |
| return await runCommand( | |
| flutter, | |
| <String>['analyze', ...options], | |
| workingDirectory: workingDirectory, | |
| failureMessage: failureMessage, | |
| ); | |
| } finally { | |
| heartbeatTimer.cancel(); | |
| } | |
| } | |
| // These files legitimately require executable permissions | |
| const Set<String> kExecutableAllowlist = <String>{ | |
| '.autoroller-preupload.sh', | |
| '.claude/skills', | |
| '.github/scripts/did_engine_change.sh', | |
| '.github/scripts/git_files_changed.sh', | |
| 'bin/dart', | |
| 'bin/flutter', | |
| 'bin/flutter-dev', | |
| 'bin/internal/last_engine_commit.sh', | |
| 'bin/internal/update_dart_sdk.sh', | |
| 'bin/internal/update_engine_version.sh', | |
| 'bin/internal/content_aware_hash.sh', | |
| 'dev/bots/codelabs_build_test.sh', | |
| 'dev/bots/docs.sh', | |
| 'dev/checks', | |
| 'dev/customer_testing/ci.sh', | |
| 'dev/integration_tests/flutter_gallery/tool/run_instrumentation_test.sh', | |
| 'dev/integration_tests/ios_add2app_life_cycle/build_and_test.sh', | |
| 'dev/integration_tests/deferred_components_test/download_assets.sh', | |
| 'dev/integration_tests/deferred_components_test/run_release_test.sh', | |
| 'dev/packages_autoroller/run', | |
| 'dev/tools/gen_keycodes/bin/gen_keycodes', | |
| 'dev/tools/repackage_gradle_wrapper.sh', | |
| 'dev/tools/bin/engine_hash.sh', | |
| 'dev/tools/format.sh', | |
| 'dev/tools/test/mock_git.sh', | |
| 'packages/flutter_tools/bin/macos_assemble.sh', | |
| 'packages/flutter_tools/bin/tool_backend.sh', | |
| 'packages/flutter_tools/bin/xcode_backend.sh', | |
| }; | |
| Future<void> _checkForNewExecutables() async { | |
| // 0b001001001 | |
| const executableBitMask = 0x49; | |
| final List<File> files = await _gitFiles(flutterRoot); | |
| final errors = <String>[]; | |
| for (final file in files) { | |
| final String relativePath = path.relative(file.path, from: flutterRoot); | |
| final FileStat stat = file.statSync(); | |
| final isExecutable = stat.mode & executableBitMask != 0x0; | |
| if (isExecutable && !kExecutableAllowlist.contains(relativePath)) { | |
| errors.add('$relativePath is executable: ${(stat.mode & 0x1FF).toRadixString(2)}'); | |
| } | |
| } | |
| if (errors.isNotEmpty) { | |
| throw Exception( | |
| '${errors.join('\n')}\n' | |
| 'found ${errors.length} unexpected executable file' | |
| '${errors.length == 1 ? '' : 's'}! If this was intended, you ' | |
| 'must add this file to kExecutableAllowlist in dev/bots/analyze.dart', | |
| ); | |
| } | |
| } | |
| final RegExp _importPattern = RegExp(r'''^\s*import (['"])package:flutter/([^.]+)\.dart\1'''); | |
| Future<Set<String>> _findFlutterDependencies(String srcPath) async { | |
| return _allFiles(srcPath, 'dart', minimumMatches: 1).expand<String>((File file) { | |
| final result = <String>{}; | |
| for (final String line in file.readAsLinesSync()) { | |
| final Match? match = _importPattern.firstMatch(line); | |
| if (match != null) { | |
| result.add(match.group(2)!); | |
| } | |
| } | |
| return result; | |
| }).toSet(); | |
| } | |
| List<T>? _deepSearch<T>(Map<T, Set<T>> map, T start, [Set<T>? seen]) { | |
| if (map[start] == null) { | |
| return null; // We catch these separately. | |
| } | |
| for (final T key in map[start]!) { | |
| if (key == start) { | |
| continue; // we catch these separately | |
| } | |
| if (seen != null && seen.contains(key)) { | |
| return <T>[start, key]; | |
| } | |
| final List<T>? result = _deepSearch<T>(map, key, <T>{ | |
| if (seen == null) start else ...seen, | |
| key, | |
| }); | |
| if (result != null) { | |
| if (result.first == result.last) { | |
| return result; | |
| } | |
| result.insert(0, start); | |
| return result; | |
| } | |
| } | |
| return null; | |
| } | |
| bool _isGeneratedPluginRegistrant(File file) { | |
| final String filename = path.basename(file.path); | |
| return !file.path.contains('.pub-cache') && | |
| (filename == 'GeneratedPluginRegistrant.java' || | |
| filename == 'GeneratedPluginRegistrant.swift' || | |
| filename == 'GeneratedPluginRegistrant.h' || | |
| filename == 'GeneratedPluginRegistrant.m' || | |
| filename == 'generated_plugin_registrant.dart' || | |
| filename == 'generated_plugin_registrant.h' || | |
| filename == 'generated_plugin_registrant.cc'); | |
| } | |
| Future<void> _verifyMegaGallery( | |
| String flutterRoot, | |
| String dart, | |
| List<String> passthroughArguments, | |
| ) async { | |
| final Directory outDir = Directory.systemTemp.createTempSync('flutter_mega_gallery.'); | |
| try { | |
| await runCommand(dart, <String>[ | |
| path.join(flutterRoot, 'dev', 'tools', 'mega_gallery.dart'), | |
| '--out', | |
| outDir.path, | |
| ], workingDirectory: flutterRoot); | |
| await _runFlutterAnalyze( | |
| outDir.path, | |
| failureMessage: 'Dart analyzer failed on mega_gallery benchmark.', | |
| options: <String>['--watch', '--benchmark', ...passthroughArguments], | |
| ); | |
| } finally { | |
| outDir.deleteSync(recursive: true); | |
| } | |
| } |