blob: f22a40240a1be6623cd5124949e168ed041f8216 [file] [log] [blame]
// 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
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() {
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(title, indent: (_thickDivider.length - title.length) ~/ 2, emphasis: true);
// 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.isNotEmpty) {
logger.printStatus('Diff between `android` and expected files:', emphasis: true);
for (final String line in diffLines) {
// We only care about diffs in files that have
// counterparts.
if (line.startsWith('Only in android')) {
TerminalColor color = TerminalColor.grey;
if (line.startsWith('+')) {
color =;
} else if (line.startsWith('-')) {
color =;
logger.printStatus(line, color: color);
// 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);
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);
if (generatedFiles.isNotEmpty || modifiedFiles.isNotEmpty) {
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.isNotEmpty && !platform.isWindows) {
The recommended changes can be quickly applied by running:
$ patch -p0 < build/setup_deferred_components.diff
// 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);
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);
if (loadingUnitComparisonResults!['match'] as bool) {
logger.printStatus('No change in generated loading units.\n');
} else {
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
This loading unit check will not fail again on the next build attempt
if no additional changes to the loading units are detected.
// TODO(garyq): Add link to web tutorial/guide once it is written.
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.
logger.printStatus('$title passed.');
void attemptToolExit() {
if (exitOnFail && changesNeeded) {
throwToolExit('Setup for deferred components incomplete. See recommended actions.', exitCode: 1);