1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:args/args.dart';
6import 'package:meta/meta.dart';
7import 'package:unified_analytics/unified_analytics.dart';
8
9import '../artifacts.dart';
10import '../base/common.dart';
11import '../base/file_system.dart';
12import '../build_info.dart';
13import '../build_system/build_system.dart';
14import '../build_system/depfile.dart';
15import '../build_system/targets/android.dart';
16import '../build_system/targets/assets.dart';
17import '../build_system/targets/common.dart';
18import '../build_system/targets/deferred_components.dart';
19import '../build_system/targets/ios.dart';
20import '../build_system/targets/linux.dart';
21import '../build_system/targets/macos.dart';
22import '../build_system/targets/windows.dart';
23import '../cache.dart';
24import '../convert.dart';
25import '../globals.dart' as globals;
26import '../project.dart';
27import '../runner/flutter_command.dart';
28
29/// All currently implemented targets.
30var _kDefaultTargets = <Target>[
31 // Shared targets
32 const CopyAssets(),
33 const KernelSnapshot(),
34 const AotElfProfile(TargetPlatform.android_arm),
35 const AotElfRelease(TargetPlatform.android_arm),
36 const AotAssemblyProfile(),
37 const AotAssemblyRelease(),
38 // macOS targets
39 const DebugMacOSFramework(),
40 const DebugMacOSBundleFlutterAssets(),
41 const ProfileMacOSBundleFlutterAssets(),
42 const ReleaseMacOSBundleFlutterAssets(),
43 const DebugUnpackMacOS(),
44 const ProfileUnpackMacOS(),
45 const ReleaseUnpackMacOS(),
46 // Linux targets
47 const DebugBundleLinuxAssets(TargetPlatform.linux_x64),
48 const DebugBundleLinuxAssets(TargetPlatform.linux_arm64),
49 const ProfileBundleLinuxAssets(TargetPlatform.linux_x64),
50 const ProfileBundleLinuxAssets(TargetPlatform.linux_arm64),
51 const ReleaseBundleLinuxAssets(TargetPlatform.linux_x64),
52 const ReleaseBundleLinuxAssets(TargetPlatform.linux_arm64),
53 const ReleaseAndroidApplication(),
54 // This is a one-off rule for bundle and aot compat.
55 const CopyFlutterBundle(),
56 // Android targets,
57 const DebugAndroidApplication(),
58 const ProfileAndroidApplication(),
59 // Android ABI specific AOT rules.
60 androidArmProfileBundle,
61 androidArm64ProfileBundle,
62 androidx64ProfileBundle,
63 androidArmReleaseBundle,
64 androidArm64ReleaseBundle,
65 androidx64ReleaseBundle,
66 // Deferred component enabled AOT rules
67 androidArmProfileDeferredComponentsBundle,
68 androidArm64ProfileDeferredComponentsBundle,
69 androidx64ProfileDeferredComponentsBundle,
70 androidArmReleaseDeferredComponentsBundle,
71 androidArm64ReleaseDeferredComponentsBundle,
72 androidx64ReleaseDeferredComponentsBundle,
73 // iOS targets
74 const DebugIosApplicationBundle(),
75 const ProfileIosApplicationBundle(),
76 const ReleaseIosApplicationBundle(),
77 const DebugUnpackIOS(),
78 const ProfileUnpackIOS(),
79 const ReleaseUnpackIOS(),
80 // Windows targets
81 const UnpackWindows(TargetPlatform.windows_x64),
82 const UnpackWindows(TargetPlatform.windows_arm64),
83 const DebugBundleWindowsAssets(TargetPlatform.windows_x64),
84 const DebugBundleWindowsAssets(TargetPlatform.windows_arm64),
85 const ProfileBundleWindowsAssets(TargetPlatform.windows_x64),
86 const ProfileBundleWindowsAssets(TargetPlatform.windows_arm64),
87 const ReleaseBundleWindowsAssets(TargetPlatform.windows_x64),
88 const ReleaseBundleWindowsAssets(TargetPlatform.windows_arm64),
89];
90
91/// Assemble provides a low level API to interact with the flutter tool build
92/// system.
93class AssembleCommand extends FlutterCommand {
94 AssembleCommand({bool verboseHelp = false, required BuildSystem buildSystem})
95 : _buildSystem = buildSystem {
96 argParser.addMultiOption(
97 'define',
98 abbr: 'd',
99 valueHelp: 'target=key=value',
100 help: 'Allows passing configuration to a target, as in "--define=target=key=value".',
101 );
102 argParser.addOption(
103 'performance-measurement-file',
104 help: 'Output individual target performance to a JSON file.',
105 );
106 argParser.addMultiOption(
107 'input',
108 abbr: 'i',
109 help:
110 'Allows passing additional inputs with "--input=key=value". Unlike '
111 'defines, additional inputs do not generate a new configuration; instead '
112 'they are treated as dependencies of the targets that use them.',
113 );
114 argParser.addOption(
115 'depfile',
116 help:
117 'A file path where a depfile will be written. '
118 'This contains all build inputs and outputs in a Make-style syntax.',
119 );
120 argParser.addOption(
121 'build-inputs',
122 help:
123 'A file path where a newline-separated '
124 'file containing all inputs used will be written after a build. '
125 'This file is not included as a build input or output. This file is not '
126 'written if the build fails for any reason.',
127 );
128 argParser.addOption(
129 'build-outputs',
130 help:
131 'A file path where a newline-separated '
132 'file containing all outputs created will be written after a build. '
133 'This file is not included as a build input or output. This file is not '
134 'written if the build fails for any reason.',
135 );
136 argParser.addOption(
137 'output',
138 abbr: 'o',
139 help:
140 'A directory where output '
141 'files will be written. Must be either absolute or relative from the '
142 'root of the current Flutter project.',
143 );
144 usesExtraDartFlagOptions(verboseHelp: verboseHelp);
145 usesDartDefineOption();
146 argParser.addOption(
147 'resource-pool-size',
148 help: 'The maximum number of concurrent tasks the build system will run.',
149 );
150 }
151
152 final BuildSystem _buildSystem;
153
154 late final FlutterProject _flutterProject = FlutterProject.current();
155
156 @override
157 String get description => 'Assemble and build Flutter resources.';
158
159 @override
160 String get name => 'assemble';
161
162 @override
163 String get category => FlutterCommandCategory.project;
164
165 @override
166 Future<Event> unifiedAnalyticsUsageValues(String commandPath) async => Event.commandUsageValues(
167 workflow: commandPath,
168 commandHasTerminal: hasTerminal,
169 buildBundleTargetPlatform: _environment.defines[kTargetPlatform],
170 buildBundleIsModule: _flutterProject.isModule,
171 );
172
173 @override
174 Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
175 final String? platform = _environment.defines[kTargetPlatform];
176 if (platform == null) {
177 return super.requiredArtifacts;
178 }
179
180 final TargetPlatform targetPlatform = getTargetPlatformForName(platform);
181 final DevelopmentArtifact? artifact = artifactFromTargetPlatform(targetPlatform);
182 if (artifact != null) {
183 return <DevelopmentArtifact>{artifact};
184 }
185 return super.requiredArtifacts;
186 }
187
188 /// The target(s) we are building.
189 List<Target> createTargets() {
190 final ArgResults argumentResults = argResults!;
191 if (argumentResults.rest.isEmpty) {
192 throwToolExit('missing target name for flutter assemble.');
193 }
194 final String name = argumentResults.rest.first;
195 final targetMap = <String, Target>{
196 for (final Target target in _kDefaultTargets) target.name: target,
197 };
198 final results = <Target>[
199 for (final String targetName in argumentResults.rest)
200 if (targetMap.containsKey(targetName)) targetMap[targetName]!,
201 ];
202 if (results.isEmpty) {
203 throwToolExit('No target named "$name" defined.');
204 }
205 return results;
206 }
207
208 bool isDeferredComponentsTargets() {
209 for (final String targetName in argResults!.rest) {
210 if (deferredComponentsTargets.contains(targetName)) {
211 return true;
212 }
213 }
214 return false;
215 }
216
217 bool isDebug() {
218 for (final String targetName in argResults!.rest) {
219 if (targetName.contains('debug')) {
220 return true;
221 }
222 }
223 return false;
224 }
225
226 late final Environment _environment = _createEnvironment();
227
228 /// The environmental configuration for a build invocation.
229 Environment _createEnvironment() {
230 String? output = stringArg('output');
231 if (output == null) {
232 throwToolExit('--output directory is required for assemble.');
233 }
234 // If path is relative, make it absolute from flutter project.
235 if (globals.fs.path.isRelative(output)) {
236 output = globals.fs.path.join(_flutterProject.directory.path, output);
237 }
238 final Artifacts artifacts = globals.artifacts!;
239 final result = Environment(
240 outputDir: globals.fs.directory(output),
241 buildDir: _flutterProject.directory
242 .childDirectory('.dart_tool')
243 .childDirectory('flutter_build'),
244 projectDir: _flutterProject.directory,
245 packageConfigPath: packageConfigPath(),
246 defines: _parseDefines(stringsArg('define')),
247 inputs: _parseDefines(stringsArg('input')),
248 cacheDir: globals.cache.getRoot(),
249 flutterRootDir: globals.fs.directory(Cache.flutterRoot),
250 artifacts: artifacts,
251 fileSystem: globals.fs,
252 logger: globals.logger,
253 processManager: globals.processManager,
254 analytics: globals.analytics,
255 platform: globals.platform,
256 engineVersion: artifacts.usesLocalArtifacts ? null : globals.flutterVersion.engineRevision,
257 generateDartPluginRegistry: true,
258 );
259 return result;
260 }
261
262 Map<String, String> _parseDefines(List<String> values) {
263 final results = <String, String>{};
264 for (final chunk in values) {
265 final int indexEquals = chunk.indexOf('=');
266 if (indexEquals == -1) {
267 throwToolExit('Improperly formatted define flag: $chunk');
268 }
269 final String key = chunk.substring(0, indexEquals);
270 final String value = chunk.substring(indexEquals + 1);
271 results[key] = value;
272 }
273 final ArgResults argumentResults = argResults!;
274 if (argumentResults.wasParsed(FlutterOptions.kExtraGenSnapshotOptions)) {
275 results[kExtraGenSnapshotOptions] =
276 (argumentResults[FlutterOptions.kExtraGenSnapshotOptions] as List<String>).join(',');
277 }
278
279 final Map<String, Object?> defineConfigJsonMap = extractDartDefineConfigJsonMap();
280 final List<String> dartDefines = extractDartDefines(defineConfigJsonMap: defineConfigJsonMap);
281 if (dartDefines.isNotEmpty) {
282 results[kDartDefines] = dartDefines.join(',');
283 }
284
285 results[kDeferredComponents] = 'false';
286 if (_flutterProject.manifest.deferredComponents != null &&
287 isDeferredComponentsTargets() &&
288 !isDebug()) {
289 results[kDeferredComponents] = 'true';
290 }
291 if (argumentResults.wasParsed(FlutterOptions.kExtraFrontEndOptions)) {
292 results[kExtraFrontEndOptions] =
293 (argumentResults[FlutterOptions.kExtraFrontEndOptions] as List<String>).join(',');
294 }
295 return results;
296 }
297
298 @override
299 Future<FlutterCommandResult> runCommand() async {
300 final List<Target> targets = createTargets();
301 final nonDeferredTargets = <Target>[];
302 final List<Target> deferredTargets = <AndroidAotDeferredComponentsBundle>[];
303 for (final target in targets) {
304 if (deferredComponentsTargets.contains(target.name)) {
305 deferredTargets.add(target);
306 } else {
307 nonDeferredTargets.add(target);
308 }
309 }
310 Target? target;
311 List<String> decodedDefines;
312 try {
313 decodedDefines = decodeDartDefines(_environment.defines, kDartDefines);
314 } on FormatException {
315 throwToolExit(
316 'Error parsing assemble command: your generated configuration may be out of date. '
317 "Try re-running 'flutter build ios' or the appropriate build command.",
318 );
319 }
320 if (deferredTargets.isNotEmpty) {
321 // Record to analytics that DeferredComponents is being used.
322 globals.analytics.send(
323 Event.flutterBuildInfo(
324 label: 'assemble-deferred-components',
325 buildType: 'android',
326 settings: deferredTargets.map((Target t) => t.name).join(','),
327 ),
328 );
329 }
330 if (_flutterProject.manifest.deferredComponents != null &&
331 decodedDefines.contains('validate-deferred-components=true') &&
332 deferredTargets.isNotEmpty &&
333 !isDebug()) {
334 // Add deferred components validation target that require loading units.
335 target = DeferredComponentsGenSnapshotValidatorTarget(
336 deferredComponentsDependencies: deferredTargets.cast<AndroidAotDeferredComponentsBundle>(),
337 nonDeferredComponentsDependencies: nonDeferredTargets,
338 title: 'Deferred components gen_snapshot validation',
339 );
340 } else if (targets.length > 1) {
341 target = CompositeTarget(targets);
342 } else if (targets.isNotEmpty) {
343 target = targets.single;
344 }
345 final ArgResults argumentResults = argResults!;
346 final BuildResult result = await _buildSystem.build(
347 target!,
348 _environment,
349 buildSystemConfig: BuildSystemConfig(
350 resourcePoolSize: argumentResults.wasParsed('resource-pool-size')
351 ? int.tryParse(stringArg('resource-pool-size')!)
352 : null,
353 ),
354 );
355 if (!result.success) {
356 for (final ExceptionMeasurement measurement in result.exceptions.values) {
357 if (measurement.fatal || globals.logger.isVerbose) {
358 globals.printError(
359 'Target ${measurement.target} failed: ${measurement.exception}',
360 stackTrace: globals.logger.isVerbose ? measurement.stackTrace : null,
361 );
362 }
363 }
364 throwToolExit('');
365 }
366 globals.printTrace('build succeeded.');
367
368 if (argumentResults.wasParsed('build-inputs')) {
369 writeListIfChanged(result.inputFiles, stringArg('build-inputs')!);
370 }
371 if (argumentResults.wasParsed('build-outputs')) {
372 writeListIfChanged(result.outputFiles, stringArg('build-outputs')!);
373 }
374 if (argumentResults.wasParsed('performance-measurement-file')) {
375 final File outFile = globals.fs.file(argumentResults['performance-measurement-file']);
376 writePerformanceData(result.performance.values, outFile);
377 }
378 if (argumentResults.wasParsed('depfile')) {
379 final File depfileFile = globals.fs.file(stringArg('depfile'));
380 final depfile = Depfile(result.inputFiles, result.outputFiles);
381 _environment.depFileService.writeToFile(depfile, globals.fs.file(depfileFile));
382 }
383 return FlutterCommandResult.success();
384 }
385}
386
387@visibleForTesting
388void writeListIfChanged(List<File> files, String path) {
389 final File file = globals.fs.file(path);
390 final buffer = StringBuffer();
391 // These files are already sorted.
392 for (final file in files) {
393 buffer.writeln(file.path);
394 }
395 final newContents = buffer.toString();
396 if (!file.existsSync()) {
397 file.writeAsStringSync(newContents);
398 }
399 final String currentContents = file.readAsStringSync();
400 if (currentContents != newContents) {
401 file.writeAsStringSync(newContents);
402 }
403}
404
405/// Output performance measurement data in [outFile].
406@visibleForTesting
407void writePerformanceData(Iterable<PerformanceMeasurement> measurements, File outFile) {
408 final jsonData = <String, Object>{
409 'targets': <Object>[
410 for (final PerformanceMeasurement measurement in measurements)
411 <String, Object>{
412 'name': measurement.analyticsName,
413 'skipped': measurement.skipped,
414 'succeeded': measurement.succeeded,
415 'elapsedMilliseconds': measurement.elapsedMilliseconds,
416 },
417 ],
418 };
419 if (!outFile.parent.existsSync()) {
420 outFile.parent.createSync(recursive: true);
421 }
422 outFile.writeAsStringSync(json.encode(jsonData));
423}
424