blob: 4dac90e60b805f6b187ffc79ce34be63ecac4a27 [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:meta/meta.dart';
import '../base/common.dart';
import '../base/file_system.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/dart.dart';
import '../build_system/targets/ios.dart';
import '../build_system/targets/linux.dart';
import '../build_system/targets/macos.dart';
import '../build_system/targets/web.dart';
import '../build_system/targets/windows.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../reporting/reporting.dart';
import '../runner/flutter_command.dart';
/// All currently implemented targets.
const List<Target> _kDefaultTargets = <Target>[
UnpackWindows(),
CopyAssets(),
KernelSnapshot(),
AotElfProfile(),
AotElfRelease(),
AotAssemblyProfile(),
AotAssemblyRelease(),
DebugMacOSFramework(),
DebugMacOSBundleFlutterAssets(),
ProfileMacOSBundleFlutterAssets(),
ReleaseMacOSBundleFlutterAssets(),
DebugBundleLinuxAssets(),
WebServiceWorker(),
DebugAndroidApplication(),
FastStartAndroidApplication(),
ProfileAndroidApplication(),
ReleaseAndroidApplication(),
// This is a one-off rule for bundle and aot compat.
CopyFlutterBundle(),
// Android ABI specific AOT rules.
androidArmProfileBundle,
androidArm64ProfileBundle,
androidx64ProfileBundle,
androidArmReleaseBundle,
androidArm64ReleaseBundle,
androidx64ReleaseBundle,
DebugIosApplicationBundle(),
ProfileIosApplicationBundle(),
ReleaseIosApplicationBundle(),
];
/// Assemble provides a low level API to interact with the flutter tool build
/// system.
class AssembleCommand extends FlutterCommand {
AssembleCommand() {
argParser.addMultiOption(
'define',
abbr: 'd',
help: 'Allows passing configuration to a target with --define=target=key=value.',
);
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 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('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.',
);
argParser.addOption(kExtraGenSnapshotOptions);
argParser.addOption(
'resource-pool-size',
help: 'The maximum number of concurrent tasks the build system will run.',
);
}
@override
String get description => 'Assemble and build flutter resources.';
@override
String get name => 'assemble';
@override
Future<Map<CustomDimensions, String>> get usageValues async {
final FlutterProject futterProject = FlutterProject.current();
if (futterProject == null) {
return const <CustomDimensions, String>{};
}
try {
final Environment localEnvironment = environment;
return <CustomDimensions, String>{
CustomDimensions.commandBuildBundleTargetPlatform: localEnvironment.defines['TargetPlatform'],
CustomDimensions.commandBuildBundleIsModule: '${futterProject.isModule}',
};
} catch (err) {
// We've failed to send usage.
}
return const <CustomDimensions, String>{};
}
/// The target(s) we are building.
List<Target> get targets {
if (argResults.rest.isEmpty) {
throwToolExit('missing target name for flutter assemble.');
}
final String name = argResults.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 argResults.rest)
if (targetMap.containsKey(targetName))
targetMap[targetName]
];
if (results.isEmpty) {
throwToolExit('No target named "$name" defined.');
}
return results;
}
/// The environmental configuration for a build invocation.
Environment get environment {
final FlutterProject flutterProject = FlutterProject.current();
String output = stringArg('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 Environment result = Environment(
outputDir: globals.fs.directory(output),
buildDir: flutterProject.directory
.childDirectory('.dart_tool')
.childDirectory('flutter_build'),
projectDir: flutterProject.directory,
defines: _parseDefines(stringsArg('define')),
cacheDir: globals.cache.getRoot(),
flutterRootDir: globals.fs.directory(Cache.flutterRoot),
);
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;
}
// Workaround for extraGenSnapshot formatting.
if (argResults.wasParsed(kExtraGenSnapshotOptions)) {
results[kExtraGenSnapshotOptions] = argResults[kExtraGenSnapshotOptions] as String;
}
return results;
}
@override
Future<FlutterCommandResult> runCommand() async {
final List<Target> targets = this.targets;
final Target target = targets.length == 1 ? targets.single : _CompositeTarget(targets);
final BuildResult result = await buildSystem.build(target, environment, buildSystemConfig: BuildSystemConfig(
resourcePoolSize: argResults.wasParsed('resource-pool-size')
? int.tryParse(stringArg('resource-pool-size'))
: null,
));
if (!result.success) {
for (final ExceptionMeasurement measurement in result.exceptions.values) {
globals.printError('Target ${measurement.target} failed: ${measurement.exception}',
stackTrace: measurement.fatal
? measurement.stackTrace
: null,
);
}
throwToolExit('build failed.');
}
globals.printTrace('build succeeded.');
if (argResults.wasParsed('build-inputs')) {
writeListIfChanged(result.inputFiles, stringArg('build-inputs'));
}
if (argResults.wasParsed('build-outputs')) {
writeListIfChanged(result.outputFiles, stringArg('build-outputs'));
}
if (argResults.wasParsed('depfile')) {
final File depfileFile = globals.fs.file(stringArg('depfile'));
final Depfile depfile = Depfile(result.inputFiles, result.outputFiles);
final DepfileService depfileService = DepfileService(
fileSystem: globals.fs,
logger: globals.logger,
platform: globals.platform,
);
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);
}
}
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>[];
}