// 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 '../base/common.dart'; import '../base/deferred_component.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/terminal.dart'; /// A class to configure and run deferred component setup verification checks /// and tasks. /// /// Once constructed, checks and tasks can be executed by calling the respective /// methods. The results of the checks are stored internally and can be /// displayed to the user by calling [displayResults]. /// /// The results of each check are handled internally as they are not meant to /// be run isolated. abstract class DeferredComponentsValidator { DeferredComponentsValidator(this.projectDir, this.logger, this.platform, { this.exitOnFail = true, String? title, }) : outputDir = projectDir .childDirectory('build') .childDirectory(kDeferredComponentsTempDirectory), inputs = <File>[], outputs = <File>[], title = title ?? 'Deferred components setup verification', generatedFiles = <String>[], modifiedFiles = <String>[], invalidFiles = <String, String>{}, diffLines = <String>[]; /// Logger to use for [displayResults] output. final Logger logger; final Platform platform; /// When true, failed checks and tasks will result in [attemptToolExit] /// triggering [throwToolExit]. final bool exitOnFail; /// The name of the golden file that tracks the latest loading units /// generated. static const String kLoadingUnitsCacheFileName = 'deferred_components_loading_units.yaml'; /// The directory in the build folder to generate missing/modified files into. static const String kDeferredComponentsTempDirectory = 'android_deferred_components_setup_files'; /// The title printed at the top of the results of [displayResults] final String title; /// The root directory of the flutter project. final Directory projectDir; /// The temporary directory that the validator writes recommended files into. final Directory outputDir; /// Files that were newly generated by this validator. final List<String> generatedFiles; /// Existing files that were modified by this validator. final List<String> modifiedFiles; /// Files that were invalid and unable to be checked. These files are input /// files that the validator tries to read rather than output files the /// validator generates. The key is the file name and the value is the message /// or reason it was invalid. final Map<String, String> invalidFiles; // TODO(garyq): implement the diff task. /// Output of the diff task. final List<String> diffLines; /// Tracks the new and missing loading units. Map<String, dynamic>? loadingUnitComparisonResults; /// All files read by the validator. final List<File> inputs; /// All files output by the validator. final List<File> outputs; /// Returns true if there were any recommended changes that should /// be applied. /// /// Returns false if no problems or recommendations were detected. /// /// If no checks are run, then this will default to false and will remain so /// until a failing check finishes running. bool get changesNeeded => generatedFiles.isNotEmpty || modifiedFiles.isNotEmpty || invalidFiles.isNotEmpty || (loadingUnitComparisonResults != null && !(loadingUnitComparisonResults!['match'] as bool)); /// Handles the results of all executed checks by calling [displayResults] and /// [attemptToolExit]. /// /// This should be called after all desired checks and tasks are executed. void handleResults() { displayResults(); attemptToolExit(); } static const String _thickDivider = '================================================================================='; static const String _thinDivider = '---------------------------------------------------------------------------------'; /// Displays the results of this validator's executed checks and tasks in a /// human readable format. /// /// All checks that are desired should be run before calling this method. void displayResults() { if (changesNeeded) { logger.printStatus(_thickDivider); logger.printStatus(title, indent: (_thickDivider.length - title.length) ~/ 2, emphasis: true); logger.printStatus(_thickDivider); // Log any file reading/existence errors. if (invalidFiles.isNotEmpty) { logger.printStatus('Errors checking the following files:\n', emphasis: true); for (final String key in invalidFiles.keys) { logger.printStatus(' - $key: ${invalidFiles[key]}\n'); } } // Log diff file contents, with color highlighting if (diffLines != null && diffLines.isNotEmpty) { logger.printStatus('Diff between `android` and expected files:', emphasis: true); logger.printStatus(''); for (final String line in diffLines) { // We only care about diffs in files that have // counterparts. if (line.startsWith('Only in android')) { continue; } TerminalColor color = TerminalColor.grey; if (line.startsWith('+')) { color = TerminalColor.green; } else if (line.startsWith('-')) { color = TerminalColor.red; } logger.printStatus(line, color: color); } logger.printStatus(''); } // Log any newly generated and modified files. if (generatedFiles.isNotEmpty) { logger.printStatus('Newly generated android files:', emphasis: true); for (final String filePath in generatedFiles) { final String shortenedPath = filePath.substring(projectDir.parent.path.length + 1); logger.printStatus(' - $shortenedPath', color: TerminalColor.grey); } logger.printStatus(''); } if (modifiedFiles.isNotEmpty) { logger.printStatus('Modified android files:', emphasis: true); for (final String filePath in modifiedFiles) { final String shortenedPath = filePath.substring(projectDir.parent.path.length + 1); logger.printStatus(' - $shortenedPath', color: TerminalColor.grey); } logger.printStatus(''); } if (generatedFiles.isNotEmpty || modifiedFiles.isNotEmpty) { logger.printStatus(''' The above files have been placed into `build/$kDeferredComponentsTempDirectory`, a temporary directory. The files should be reviewed and moved into the project's `android` directory.'''); if (diffLines != null && diffLines.isNotEmpty && !platform.isWindows) { logger.printStatus(r''' The recommended changes can be quickly applied by running: $ patch -p0 < build/setup_deferred_components.diff '''); } logger.printStatus('$_thinDivider\n'); } // Log loading unit golden changes, if any. if (loadingUnitComparisonResults != null) { if ((loadingUnitComparisonResults!['new'] as List<LoadingUnit>).isNotEmpty) { logger.printStatus('New loading units were found:', emphasis: true); for (final LoadingUnit unit in loadingUnitComparisonResults!['new'] as List<LoadingUnit>) { logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2); } logger.printStatus(''); } if ((loadingUnitComparisonResults!['missing'] as Set<LoadingUnit>).isNotEmpty) { logger.printStatus('Previously existing loading units no longer exist:', emphasis: true); for (final LoadingUnit unit in loadingUnitComparisonResults!['missing'] as Set<LoadingUnit>) { logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2); } logger.printStatus(''); } if (loadingUnitComparisonResults!['match'] as bool) { logger.printStatus('No change in generated loading units.\n'); } else { logger.printStatus(''' It is recommended to verify that the changed loading units are expected and to update the `deferred-components` section in `pubspec.yaml` to incorporate any changes. The full list of generated loading units can be referenced in the $kLoadingUnitsCacheFileName file located alongside pubspec.yaml. This loading unit check will not fail again on the next build attempt if no additional changes to the loading units are detected. $_thinDivider\n'''); } } // TODO(garyq): Add link to web tutorial/guide once it is written. logger.printStatus(''' Setup verification can be skipped by passing the `--no-validate-deferred-components` flag, however, doing so may put your app at risk of not functioning even if the build is successful. $_thickDivider'''); return; } logger.printStatus('$title passed.'); } void attemptToolExit() { if (exitOnFail && changesNeeded) { throwToolExit('Setup for deferred components incomplete. See recommended actions.', exitCode: 1); } } }