Unverified Commit 9df362a7 authored by Gary Qian's avatar Gary Qian Committed by GitHub

[flutter_tools] Refactor DeferredComponents validator and add build target (#76400)

parent d60e9de7
// 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.
// @dart = 2.8
import '../base/common.dart';
import '../base/deferred_component.dart';
import '../base/file_system.dart';
import '../base/terminal.dart';
import '../build_system/build_system.dart';
import '../globals.dart' as globals;
/// 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.env, {
this.exitOnFail = true,
String title,
}) : outputDir = env.projectDir
.childDirectory('build')
.childDirectory(kDeferredComponentsTempDirectory),
inputs = <File>[],
outputs = <File>[],
title = title ?? 'Deferred components setup verification',
generatedFiles = <String>[],
modifiedFiles = <String>[],
invalidFiles = <String, String>{},
diffLines = <String>[];
/// The build environment that should be used to find the input files to run
/// checks against.
///
/// The checks in this class are meant to be used as part of a build process,
/// so an environment should be available.
final Environment env;
/// 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 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.
///
/// Retuns 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) {
env.logger.printStatus(_thickDivider);
env.logger.printStatus(title, indent: (_thickDivider.length - title.length) ~/ 2, emphasis: true);
env.logger.printStatus(_thickDivider);
// Log any file reading/existence errors.
if (invalidFiles.isNotEmpty) {
env.logger.printStatus('Errors checking the following files:\n', emphasis: true);
for (final String key in invalidFiles.keys) {
env.logger.printStatus(' - $key: ${invalidFiles[key]}\n');
}
}
// Log diff file contents, with color highlighting
if (diffLines != null && diffLines.isNotEmpty) {
env.logger.printStatus('Diff between `android` and expected files:', emphasis: true);
env.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;
}
env.logger.printStatus(line, color: color);
}
env.logger.printStatus('');
}
// Log any newly generated and modified files.
if (generatedFiles.isNotEmpty) {
env.logger.printStatus('Newly generated android files:', emphasis: true);
for (final String filePath in generatedFiles) {
final String shortenedPath = filePath.substring(env.projectDir.parent.path.length + 1);
env.logger.printStatus(' - $shortenedPath', color: TerminalColor.grey);
}
env.logger.printStatus('');
}
if (modifiedFiles.isNotEmpty) {
env.logger.printStatus('Modified android files:', emphasis: true);
for (final String filePath in modifiedFiles) {
final String shortenedPath = filePath.substring(env.projectDir.parent.path.length + 1);
env.logger.printStatus(' - $shortenedPath', color: TerminalColor.grey);
}
env.logger.printStatus('');
}
if (generatedFiles.isNotEmpty || modifiedFiles.isNotEmpty) {
env.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 && !globals.platform.isWindows) {
env.logger.printStatus(r'''
The recommended changes can be quickly applied by running:
$ patch -p0 < build/setup_deferred_components.diff
''');
}
env.logger.printStatus('$_thinDivider\n');
}
// Log loading unit golden changes, if any.
if (loadingUnitComparisonResults != null) {
if ((loadingUnitComparisonResults['new'] as List<LoadingUnit>).isNotEmpty) {
env.logger.printStatus('New loading units were found:', emphasis: true);
for (final LoadingUnit unit in loadingUnitComparisonResults['new'] as List<LoadingUnit>) {
env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
}
env.logger.printStatus('');
}
if ((loadingUnitComparisonResults['missing'] as Set<LoadingUnit>).isNotEmpty) {
env.logger.printStatus('Previously existing loading units no longer exist:', emphasis: true);
for (final LoadingUnit unit in loadingUnitComparisonResults['missing'] as Set<LoadingUnit>) {
env.logger.printStatus(unit.toString(), color: TerminalColor.grey, indent: 2);
}
env.logger.printStatus('');
}
if (loadingUnitComparisonResults['match'] as bool) {
env.logger.printStatus('No change in generated loading units.\n');
} else {
env.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.
env.logger.printStatus('''
Setup verification can be skipped by passing the `--no-verify-deferred-components`
flag, however, doing so may put your app at risk of not functioning even if the
build is successful.
$_thickDivider''');
return;
}
env.logger.printStatus('$title passed.');
}
void attemptToolExit() {
if (exitOnFail && changesNeeded) {
throwToolExit('Setup for deferred components incomplete. See recommended actions.', exitCode: 1);
}
}
}
......@@ -250,6 +250,29 @@ abstract class Target {
}
}
/// Target that contains multiple other targets.
///
/// This target does not do anything in its own [build]
/// and acts as a wrapper around multiple other targets.
class CompositeTarget extends Target {
CompositeTarget(this.dependencies);
@override
final List<Target> dependencies;
@override
String get name => '_composite';
@override
Future<void> build(Environment environment) async { }
@override
List<Source> get inputs => <Source>[];
@override
List<Source> get outputs => <Source>[];
}
/// The [Environment] defines several constants for use during the build.
///
/// The environment contains configuration and file paths that are safe to
......
// 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.
// @dart = 2.8
import 'package:meta/meta.dart';
import '../../android/deferred_components_gen_snapshot_validator.dart';
import '../../base/deferred_component.dart';
import '../../project.dart';
import '../build_system.dart';
import '../depfile.dart';
/// Creates a [DeferredComponentsGenSnapshotValidator], runs the checks, and displays the validator
/// output to the developer if changes are recommended.
class DeferredComponentsGenSnapshotValidatorTarget extends Target {
/// Create an [AndroidAotDeferredComponentsBundle] implementation for a given [targetPlatform] and [buildMode].
DeferredComponentsGenSnapshotValidatorTarget({
@required this.dependency,
@required this.abis,
this.title,
this.exitOnFail = true,
String name = 'deferred_components_setup_validator',
}) : _name = name;
/// The [AndroidAotDeferredComponentsBundle] derived target instances this rule depends on packed
/// as a [CompositeTarget].
final CompositeTarget dependency;
/// The title of the [DeferredComponentsGenSnapshotValidator] that is
/// displayed to the developer when logging results.
final String title;
/// Whether to exit the tool if a recommended change is found by the
/// [DeferredComponentsGenSnapshotValidator].
final bool exitOnFail;
/// The abis to validate.
final List<String> abis;
@override
String get name => _name;
final String _name;
@override
List<Source> get inputs => const <Source>[];
@override
List<Source> get outputs => const <Source>[];
@override
List<String> get depfiles => <String>[
'flutter_$name.d',
];
@override
List<Target> get dependencies => dependency == null ? <Target>[] : <Target>[dependency];
@visibleForTesting
DeferredComponentsGenSnapshotValidator validator;
@override
Future<void> build(Environment environment) async {
final DepfileService depfileService = DepfileService(
fileSystem: environment.fileSystem,
logger: environment.logger,
);
validator = DeferredComponentsGenSnapshotValidator(
environment,
title: title,
exitOnFail: exitOnFail,
);
final List<LoadingUnit> generatedLoadingUnits = LoadingUnit.parseGeneratedLoadingUnits(
environment.outputDir,
environment.logger,
abis: abis
);
validator
..checkAppAndroidManifestComponentLoadingUnitMapping(
FlutterProject.current().manifest.deferredComponents,
generatedLoadingUnits,
)
..checkAgainstLoadingUnitsCache(generatedLoadingUnits)
..writeLoadingUnitsCache(generatedLoadingUnits);
validator.handleResults();
depfileService.writeToFile(
Depfile(validator.inputs, validator.outputs),
environment.buildDir.childFile('flutter_$name.d'),
);
}
}
......@@ -227,7 +227,7 @@ class AssembleCommand extends FlutterCommand {
@override
Future<FlutterCommandResult> runCommand() async {
final List<Target> targets = createTargets();
final Target target = targets.length == 1 ? targets.single : _CompositeTarget(targets);
final Target target = targets.length == 1 ? targets.single : CompositeTarget(targets);
final BuildResult result = await globals.buildSystem.build(
target,
createEnvironment(),
......@@ -308,22 +308,3 @@ void writePerformanceData(Iterable<PerformanceMeasurement> measurements, File ou
}
outFile.writeAsStringSync(json.encode(jsonData));
}
class _CompositeTarget extends Target {
_CompositeTarget(this.dependencies);
@override
final List<Target> dependencies;
@override
String get name => '_composite';
@override
Future<void> build(Environment environment) async { }
@override
List<Source> get inputs => <Source>[];
@override
List<Source> get outputs => <Source>[];
}
// 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.
// @dart = 2.8
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/deferred_components.dart';
import 'package:flutter_tools/src/build_system/targets/android.dart';
import 'package:flutter_tools/src/build_system/targets/common.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
// These tests perform a simple check to verify if the check/task was executed at all.
// Detailed per-check tests are in android/deferred_components_setup_validator_test.dart.
void main() {
FileSystem fileSystem;
BufferLogger logger;
setUp(() {
logger = BufferLogger.test();
fileSystem = MemoryFileSystem.test();
});
testUsingContext('checkAppAndroidManifestComponentLoadingUnitMapping checks runs', () async {
final Environment environment = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('out')..createSync(),
buildDir: fileSystem.directory('build')..createSync(),
projectDir: fileSystem.directory('project')..createSync(),
defines: <String, String>{
kSplitAot: 'true',
},
artifacts: null,
processManager: null,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
const AndroidAot androidAot = AndroidAot(TargetPlatform.android_arm64, BuildMode.release);
const AndroidAotBundle androidAotBundle = AndroidAotBundle(androidAot);
final CompositeTarget androidDefBundle = CompositeTarget(<Target>[androidAotBundle]);
final CompositeTarget compositeTarget = CompositeTarget(<Target>[androidDefBundle]);
final DeferredComponentsGenSnapshotValidatorTarget validatorTarget = DeferredComponentsGenSnapshotValidatorTarget(
dependency: compositeTarget,
abis: <String>['arm64-v8a'],
title: 'test checks',
exitOnFail: false,
);
await validatorTarget.build(environment);
// We check the inputs to determine if the task was executed.
expect(validatorTarget.validator.inputs.length, 3);
expect(validatorTarget.validator.inputs[0].path, 'project/pubspec.yaml');
expect(validatorTarget.validator.inputs[1].path, 'project/android/app/src/main/AndroidManifest.xml');
});
testUsingContext('checkAgainstLoadingUnitsCache checks runs', () async {
final Environment environment = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('out')..createSync(),
buildDir: fileSystem.directory('build')..createSync(),
projectDir: fileSystem.directory('project')..createSync(),
defines: <String, String>{
kSplitAot: 'true',
},
artifacts: null,
processManager: null,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
const AndroidAot androidAot = AndroidAot(TargetPlatform.android_arm64, BuildMode.release);
const AndroidAotBundle androidAotBundle = AndroidAotBundle(androidAot);
final CompositeTarget androidDefBundle = CompositeTarget(<Target>[androidAotBundle]);
final CompositeTarget compositeTarget = CompositeTarget(<Target>[androidDefBundle]);
final DeferredComponentsGenSnapshotValidatorTarget validatorTarget = DeferredComponentsGenSnapshotValidatorTarget(
dependency: compositeTarget,
abis: <String>['arm64-v8a'],
title: 'test checks',
exitOnFail: false,
);
await validatorTarget.build(environment);
// We check the inputs to determine if the task was executed.
expect(validatorTarget.validator.inputs.length, 3);
expect(validatorTarget.validator.inputs[2].path, 'project/deferred_components_loading_units.yaml');
});
testUsingContext('writeLoadingUnitsCache task runs', () async {
final Environment environment = Environment.test(
fileSystem.currentDirectory,
outputDir: fileSystem.directory('out')..createSync(),
buildDir: fileSystem.directory('build')..createSync(),
projectDir: fileSystem.directory('project')..createSync(),
defines: <String, String>{
kSplitAot: 'true',
},
artifacts: null,
processManager: null,
fileSystem: fileSystem,
logger: logger,
);
environment.buildDir.createSync(recursive: true);
const AndroidAot androidAot = AndroidAot(TargetPlatform.android_arm64, BuildMode.release);
const AndroidAotBundle androidAotBundle = AndroidAotBundle(androidAot);
final CompositeTarget androidDefBundle = CompositeTarget(<Target>[androidAotBundle]);
final CompositeTarget compositeTarget = CompositeTarget(<Target>[androidDefBundle]);
final DeferredComponentsGenSnapshotValidatorTarget validatorTarget = DeferredComponentsGenSnapshotValidatorTarget(
dependency: compositeTarget,
abis: <String>['arm64-v8a'],
title: 'test checks',
exitOnFail: false,
);
await validatorTarget.build(environment);
// We check the inputs to determine if the task was executed.
expect(validatorTarget.validator.outputs.length, 1);
expect(validatorTarget.validator.outputs[0].path, 'project/deferred_components_loading_units.yaml');
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment