blob: d37e6a2005fd0456346fadbd0ec4d8bdab954471 [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 'package:args/args.dart';
import 'package:meta/meta.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../build_info.dart';
import '../build_system/build_system.dart';
import '../build_system/depfile.dart';
import '../build_system/targets/android.dart';
import '../build_system/targets/assets.dart';
import '../build_system/targets/common.dart';
import '../build_system/targets/deferred_components.dart';
import '../build_system/targets/ios.dart';
import '../build_system/targets/linux.dart';
import '../build_system/targets/macos.dart';
import '../build_system/targets/windows.dart';
import '../cache.dart';
import '../convert.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../reporting/reporting.dart';
import '../runner/flutter_command.dart';
/// All currently implemented targets.
List<Target> _kDefaultTargets = <Target>[
// Shared targets
const CopyAssets(),
const KernelSnapshot(),
const AotElfProfile(TargetPlatform.android_arm),
const AotElfRelease(TargetPlatform.android_arm),
const AotAssemblyProfile(),
const AotAssemblyRelease(),
// macOS targets
const DebugMacOSFramework(),
const DebugMacOSBundleFlutterAssets(),
const ProfileMacOSBundleFlutterAssets(),
const ReleaseMacOSBundleFlutterAssets(),
// Linux targets
const DebugBundleLinuxAssets(TargetPlatform.linux_x64),
const DebugBundleLinuxAssets(TargetPlatform.linux_arm64),
const ProfileBundleLinuxAssets(TargetPlatform.linux_x64),
const ProfileBundleLinuxAssets(TargetPlatform.linux_arm64),
const ReleaseBundleLinuxAssets(TargetPlatform.linux_x64),
const ReleaseBundleLinuxAssets(TargetPlatform.linux_arm64),
const ReleaseAndroidApplication(),
// This is a one-off rule for bundle and aot compat.
const CopyFlutterBundle(),
// Android targets,
const DebugAndroidApplication(),
const ProfileAndroidApplication(),
// Android ABI specific AOT rules.
androidArmProfileBundle,
androidArm64ProfileBundle,
androidx64ProfileBundle,
androidArmReleaseBundle,
androidArm64ReleaseBundle,
androidx64ReleaseBundle,
// Deferred component enabled AOT rules
androidArmProfileDeferredComponentsBundle,
androidArm64ProfileDeferredComponentsBundle,
androidx64ProfileDeferredComponentsBundle,
androidArmReleaseDeferredComponentsBundle,
androidArm64ReleaseDeferredComponentsBundle,
androidx64ReleaseDeferredComponentsBundle,
// iOS targets
const DebugIosApplicationBundle(),
const ProfileIosApplicationBundle(),
const ReleaseIosApplicationBundle(),
// Windows targets
const UnpackWindows(),
const DebugBundleWindowsAssets(),
const ProfileBundleWindowsAssets(),
const ReleaseBundleWindowsAssets(),
];
/// Assemble provides a low level API to interact with the flutter tool build
/// system.
class AssembleCommand extends FlutterCommand {
AssembleCommand({ bool verboseHelp = false, required BuildSystem buildSystem })
: _buildSystem = buildSystem {
argParser.addMultiOption(
'define',
abbr: 'd',
valueHelp: 'target=key=value',
help: 'Allows passing configuration to a target, as in "--define=target=key=value".',
);
argParser.addOption(
'performance-measurement-file',
help: 'Output individual target performance to a JSON file.'
);
argParser.addMultiOption(
'input',
abbr: 'i',
help: 'Allows passing additional inputs with "--input=key=value". Unlike '
'defines, additional inputs do not generate a new configuration; instead '
'they are treated as dependencies of the targets that use them.'
);
argParser.addOption('depfile',
help: 'A file path where a depfile will be written. '
'This contains all build inputs and outputs in a Make-style syntax.'
);
argParser.addOption('build-inputs', help: 'A file path where a newline-separated '
'file containing all inputs used will be written after a build. '
'This file is not included as a build input or output. This file is not '
'written if the build fails for any reason.');
argParser.addOption('build-outputs', help: 'A file path where a newline-separated '
'file containing all outputs created will be written after a build. '
'This file is not included as a build input or output. This file is not '
'written if the build fails for any reason.');
argParser.addOption('output', abbr: 'o', help: 'A directory where output '
'files will be written. Must be either absolute or relative from the '
'root of the current Flutter project.',
);
usesExtraDartFlagOptions(verboseHelp: verboseHelp);
usesDartDefineOption();
argParser.addOption(
'resource-pool-size',
help: 'The maximum number of concurrent tasks the build system will run.',
);
}
final BuildSystem _buildSystem;
@override
String get description => 'Assemble and build Flutter resources.';
@override
String get name => 'assemble';
@override
String get category => FlutterCommandCategory.project;
@override
Future<CustomDimensions> get usageValues async {
final FlutterProject flutterProject = FlutterProject.current();
try {
return CustomDimensions(
commandBuildBundleTargetPlatform: environment.defines[kTargetPlatform],
commandBuildBundleIsModule: flutterProject.isModule,
);
} on Exception {
// We've failed to send usage.
}
return const CustomDimensions();
}
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
final String? platform = environment.defines[kTargetPlatform];
if (platform == null) {
return super.requiredArtifacts;
}
final TargetPlatform targetPlatform = getTargetPlatformForName(platform);
final DevelopmentArtifact? artifact = artifactFromTargetPlatform(targetPlatform);
if (artifact != null) {
return <DevelopmentArtifact>{artifact};
}
return super.requiredArtifacts;
}
/// The target(s) we are building.
List<Target> createTargets() {
final ArgResults argumentResults = argResults!;
if (argumentResults.rest.isEmpty) {
throwToolExit('missing target name for flutter assemble.');
}
final String name = argumentResults.rest.first;
final Map<String, Target> targetMap = <String, Target>{
for (final Target target in _kDefaultTargets)
target.name: target,
};
final List<Target> results = <Target>[
for (final String targetName in argumentResults.rest)
if (targetMap.containsKey(targetName))
targetMap[targetName]!,
];
if (results.isEmpty) {
throwToolExit('No target named "$name" defined.');
}
return results;
}
bool isDeferredComponentsTargets() {
for (final String targetName in argResults!.rest) {
if (deferredComponentsTargets.contains(targetName)) {
return true;
}
}
return false;
}
bool isDebug() {
for (final String targetName in argResults!.rest) {
if (targetName.contains('debug')) {
return true;
}
}
return false;
}
late final Environment environment = createEnvironment();
/// The environmental configuration for a build invocation.
Environment createEnvironment() {
final FlutterProject flutterProject = FlutterProject.current();
String? output = stringArgDeprecated('output');
if (output == null) {
throwToolExit('--output directory is required for assemble.');
}
// If path is relative, make it absolute from flutter project.
if (globals.fs.path.isRelative(output)) {
output = globals.fs.path.join(flutterProject.directory.path, output);
}
final Artifacts artifacts = globals.artifacts!;
final Environment result = Environment(
outputDir: globals.fs.directory(output),
buildDir: flutterProject.directory
.childDirectory('.dart_tool')
.childDirectory('flutter_build'),
projectDir: flutterProject.directory,
defines: _parseDefines(stringsArg('define')),
inputs: _parseDefines(stringsArg('input')),
cacheDir: globals.cache.getRoot(),
flutterRootDir: globals.fs.directory(Cache.flutterRoot),
artifacts: artifacts,
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
platform: globals.platform,
engineVersion: artifacts.isLocalEngine
? null
: globals.flutterVersion.engineRevision,
generateDartPluginRegistry: true,
);
return result;
}
Map<String, String> _parseDefines(List<String> values) {
final Map<String, String> results = <String, String>{};
for (final String chunk in values) {
final int indexEquals = chunk.indexOf('=');
if (indexEquals == -1) {
throwToolExit('Improperly formatted define flag: $chunk');
}
final String key = chunk.substring(0, indexEquals);
final String value = chunk.substring(indexEquals + 1);
results[key] = value;
}
final ArgResults argumentResults = argResults!;
if (argumentResults.wasParsed(FlutterOptions.kExtraGenSnapshotOptions)) {
results[kExtraGenSnapshotOptions] = (argumentResults[FlutterOptions.kExtraGenSnapshotOptions] as List<String>).join(',');
}
List<String> dartDefines = <String>[];
if (argumentResults.wasParsed(FlutterOptions.kDartDefinesOption)) {
dartDefines = argumentResults[FlutterOptions.kDartDefinesOption] as List<String>;
}
if (argumentResults.wasParsed(FlutterOptions.kDartDefineFromFileOption)) {
final String? configJsonPath = stringArg(FlutterOptions.kDartDefineFromFileOption);
if (configJsonPath != null && globals.fs.isFileSync(configJsonPath)) {
final String configJsonRaw = globals.fs.file(configJsonPath).readAsStringSync();
try {
(json.decode(configJsonRaw) as Map<String, dynamic>).forEach((String key, dynamic value) {
dartDefines.add('$key=$value');
});
} on FormatException catch (err) {
throwToolExit('Json config define file "--${FlutterOptions.kDartDefineFromFileOption}=$configJsonPath" format err, '
'please fix first! format err:\n$err');
}
}
}
if(dartDefines.isNotEmpty){
results[kDartDefines] = dartDefines.join(',');
}
results[kDeferredComponents] = 'false';
if (FlutterProject.current().manifest.deferredComponents != null && isDeferredComponentsTargets() && !isDebug()) {
results[kDeferredComponents] = 'true';
}
if (argumentResults.wasParsed(FlutterOptions.kExtraFrontEndOptions)) {
results[kExtraFrontEndOptions] = (argumentResults[FlutterOptions.kExtraFrontEndOptions] as List<String>).join(',');
}
return results;
}
@override
Future<FlutterCommandResult> runCommand() async {
final List<Target> targets = createTargets();
final List<Target> nonDeferredTargets = <Target>[];
final List<Target> deferredTargets = <AndroidAotDeferredComponentsBundle>[];
for (final Target target in targets) {
if (deferredComponentsTargets.contains(target.name)) {
deferredTargets.add(target);
} else {
nonDeferredTargets.add(target);
}
}
Target? target;
List<String> decodedDefines;
try {
decodedDefines = decodeDartDefines(environment.defines, kDartDefines);
} on FormatException {
throwToolExit(
'Error parsing assemble command: your generated configuration may be out of date. '
"Try re-running 'flutter build ios' or the appropriate build command."
);
}
if (FlutterProject.current().manifest.deferredComponents != null
&& decodedDefines.contains('validate-deferred-components=true')
&& deferredTargets.isNotEmpty
&& !isDebug()) {
// Add deferred components validation target that require loading units.
target = DeferredComponentsGenSnapshotValidatorTarget(
deferredComponentsDependencies: deferredTargets.cast<AndroidAotDeferredComponentsBundle>(),
nonDeferredComponentsDependencies: nonDeferredTargets,
title: 'Deferred components gen_snapshot validation',
);
} else if (targets.length > 1) {
target = CompositeTarget(targets);
} else if (targets.isNotEmpty) {
target = targets.single;
}
final ArgResults argumentResults = argResults!;
final BuildResult result = await _buildSystem.build(
target!,
environment,
buildSystemConfig: BuildSystemConfig(
resourcePoolSize: argumentResults.wasParsed('resource-pool-size')
? int.tryParse(stringArgDeprecated('resource-pool-size')!)
: null,
),
);
if (!result.success) {
for (final ExceptionMeasurement measurement in result.exceptions.values) {
if (measurement.fatal || globals.logger.isVerbose) {
globals.printError('Target ${measurement.target} failed: ${measurement.exception}',
stackTrace: globals.logger.isVerbose ? measurement.stackTrace : null,
);
}
}
throwToolExit('');
}
globals.printTrace('build succeeded.');
if (argumentResults.wasParsed('build-inputs')) {
writeListIfChanged(result.inputFiles, stringArgDeprecated('build-inputs')!);
}
if (argumentResults.wasParsed('build-outputs')) {
writeListIfChanged(result.outputFiles, stringArgDeprecated('build-outputs')!);
}
if (argumentResults.wasParsed('performance-measurement-file')) {
final File outFile = globals.fs.file(argumentResults['performance-measurement-file']);
writePerformanceData(result.performance.values, outFile);
}
if (argumentResults.wasParsed('depfile')) {
final File depfileFile = globals.fs.file(stringArgDeprecated('depfile'));
final Depfile depfile = Depfile(result.inputFiles, result.outputFiles);
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
);
depfileService.writeToFile(depfile, globals.fs.file(depfileFile));
}
return FlutterCommandResult.success();
}
}
@visibleForTesting
void writeListIfChanged(List<File> files, String path) {
final File file = globals.fs.file(path);
final StringBuffer buffer = StringBuffer();
// These files are already sorted.
for (final File file in files) {
buffer.writeln(file.path);
}
final String newContents = buffer.toString();
if (!file.existsSync()) {
file.writeAsStringSync(newContents);
}
final String currentContents = file.readAsStringSync();
if (currentContents != newContents) {
file.writeAsStringSync(newContents);
}
}
/// Output performance measurement data in [outFile].
@visibleForTesting
void writePerformanceData(Iterable<PerformanceMeasurement> measurements, File outFile) {
final Map<String, Object> jsonData = <String, Object>{
'targets': <Object>[
for (final PerformanceMeasurement measurement in measurements)
<String, Object>{
'name': measurement.analyticsName,
'skipped': measurement.skipped,
'succeeded': measurement.succeeded,
'elapsedMilliseconds': measurement.elapsedMilliseconds,
},
],
};
if (!outFile.parent.existsSync()) {
outFile.parent.createSync(recursive: true);
}
outFile.writeAsStringSync(json.encode(jsonData));
}