deferred_components_validator.dart 8.94 KB
Newer Older
1 2 3 4 5 6 7
// 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';
8
import '../base/logger.dart';
9
import '../base/platform.dart';
10 11 12 13 14 15 16 17 18 19 20 21
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 {
22
  DeferredComponentsValidator(this.projectDir, this.logger, this.platform, {
23
    this.exitOnFail = true,
24
    String? title,
25
  }) : outputDir = projectDir
26 27 28 29 30 31 32 33 34
        .childDirectory('build')
        .childDirectory(kDeferredComponentsTempDirectory),
      inputs = <File>[],
      outputs = <File>[],
      title = title ?? 'Deferred components setup verification',
      generatedFiles = <String>[],
      modifiedFiles = <String>[],
      invalidFiles = <String, String>{},
      diffLines = <String>[];
35 36 37

  /// Logger to use for [displayResults] output.
  final Logger logger;
38

39 40
  final Platform platform;

41 42 43 44 45 46 47 48 49 50 51 52 53
  /// 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;

54 55 56
  /// The root directory of the flutter project.
  final Directory projectDir;

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
  /// 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.
77
  Map<String, dynamic>? loadingUnitComparisonResults;
78 79 80 81 82 83 84 85 86

  /// 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.
  ///
87
  /// Returns false if no problems or recommendations were detected.
88 89 90 91 92 93
  ///
  /// 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
94
    || (loadingUnitComparisonResults != null && !(loadingUnitComparisonResults!['match'] as bool));
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113

  /// 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) {
114 115 116
      logger.printStatus(_thickDivider);
      logger.printStatus(title, indent: (_thickDivider.length - title.length) ~/ 2, emphasis: true);
      logger.printStatus(_thickDivider);
117 118
      // Log any file reading/existence errors.
      if (invalidFiles.isNotEmpty) {
119
        logger.printStatus('Errors checking the following files:\n', emphasis: true);
120
        for (final String key in invalidFiles.keys) {
121
          logger.printStatus('  - $key: ${invalidFiles[key]}\n');
122 123 124 125
        }
      }
      // Log diff file contents, with color highlighting
      if (diffLines != null && diffLines.isNotEmpty) {
126 127
        logger.printStatus('Diff between `android` and expected files:', emphasis: true);
        logger.printStatus('');
128 129 130 131 132 133 134 135 136 137 138 139
        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;
          }
140
          logger.printStatus(line, color: color);
141
        }
142
        logger.printStatus('');
143 144 145
      }
      // Log any newly generated and modified files.
      if (generatedFiles.isNotEmpty) {
146
        logger.printStatus('Newly generated android files:', emphasis: true);
147
        for (final String filePath in generatedFiles) {
148 149
          final String shortenedPath = filePath.substring(projectDir.parent.path.length + 1);
          logger.printStatus('  - $shortenedPath', color: TerminalColor.grey);
150
        }
151
        logger.printStatus('');
152 153
      }
      if (modifiedFiles.isNotEmpty) {
154
        logger.printStatus('Modified android files:', emphasis: true);
155
        for (final String filePath in modifiedFiles) {
156 157
          final String shortenedPath = filePath.substring(projectDir.parent.path.length + 1);
          logger.printStatus('  - $shortenedPath', color: TerminalColor.grey);
158
        }
159
        logger.printStatus('');
160 161
      }
      if (generatedFiles.isNotEmpty || modifiedFiles.isNotEmpty) {
162
        logger.printStatus('''
163 164 165
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.''');
166
        if (diffLines != null && diffLines.isNotEmpty && !platform.isWindows) {
167
          logger.printStatus(r'''
168 169 170 171 172 173

The recommended changes can be quickly applied by running:

  $ patch -p0 < build/setup_deferred_components.diff
''');
        }
174
        logger.printStatus('$_thinDivider\n');
175 176 177
      }
      // Log loading unit golden changes, if any.
      if (loadingUnitComparisonResults != null) {
178
        if ((loadingUnitComparisonResults!['new'] as List<LoadingUnit>).isNotEmpty) {
179
          logger.printStatus('New loading units were found:', emphasis: true);
180
          for (final LoadingUnit unit in loadingUnitComparisonResults!['new'] as List<LoadingUnit>) {
181
            logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
182
          }
183
          logger.printStatus('');
184
        }
185
        if ((loadingUnitComparisonResults!['missing'] as Set<LoadingUnit>).isNotEmpty) {
186
          logger.printStatus('Previously existing loading units no longer exist:', emphasis: true);
187
          for (final LoadingUnit unit in loadingUnitComparisonResults!['missing'] as Set<LoadingUnit>) {
188
            logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
189
          }
190
          logger.printStatus('');
191
        }
192
        if (loadingUnitComparisonResults!['match'] as bool) {
193
          logger.printStatus('No change in generated loading units.\n');
194
        } else {
195
          logger.printStatus('''
196 197 198 199 200 201 202 203 204 205 206 207
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.
208 209
      logger.printStatus('''
Setup verification can be skipped by passing the `--no-validate-deferred-components`
210 211 212 213 214
flag, however, doing so may put your app at risk of not functioning even if the
build is successful.
$_thickDivider''');
      return;
    }
215
    logger.printStatus('$title passed.');
216 217 218 219 220 221 222 223
  }

  void attemptToolExit() {
    if (exitOnFail && changesNeeded) {
      throwToolExit('Setup for deferred components incomplete. See recommended actions.', exitCode: 1);
    }
  }
}