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 '../../artifacts.dart';
6import '../../base/build.dart';
7import '../../base/deferred_component.dart';
8import '../../base/file_system.dart';
9import '../../build_info.dart';
10import '../../devfs.dart';
11import '../../globals.dart' as globals show xcode;
12import '../../project.dart';
13import '../build_system.dart';
14import '../depfile.dart';
15import '../exceptions.dart';
16import 'assets.dart';
17import 'common.dart';
18import 'icon_tree_shaker.dart';
19import 'native_assets.dart';
20
21/// Prepares the asset bundle in the format expected by flutter.gradle.
22///
23/// The vm_snapshot_data, isolate_snapshot_data, and kernel_blob.bin are
24/// expected to be in the root output directory.
25///
26/// All assets and manifests are included from flutter_assets/**.
27abstract class AndroidAssetBundle extends Target {
28 const AndroidAssetBundle();
29
30 @override
31 List get inputs => const [
32 Source.pattern('{BUILD_DIR}/app.dill'),
33 ...IconTreeShaker.inputs,
34 ];
35
36 @override
37 List get outputs => const [];
38
39 @override
40 List get depfiles => ['flutter_assets.d'];
41
42 @override
43 Future build(Environment environment) async {
44 final String? buildModeEnvironment = environment.defines[kBuildMode];
45 if (buildModeEnvironment == null) {
46 throw MissingDefineException(kBuildMode, name);
47 }
48
49 final buildMode = BuildMode.fromCliName(buildModeEnvironment);
50 final Directory outputDirectory = environment.outputDir.childDirectory('flutter_assets')
51 ..createSync(recursive: true);
52
53 // Only copy the prebuilt runtimes and kernel blob in debug mode.
54 if (buildMode == BuildMode.debug) {
55 final String vmSnapshotData = environment.artifacts.getArtifactPath(
56 Artifact.vmSnapshotData,
57 mode: BuildMode.debug,
58 );
59 final String isolateSnapshotData = environment.artifacts.getArtifactPath(
60 Artifact.isolateSnapshotData,
61 mode: BuildMode.debug,
62 );
63 environment.buildDir
64 .childFile('app.dill')
65 .copySync(outputDirectory.childFile('kernel_blob.bin').path);
66 environment.fileSystem
67 .file(vmSnapshotData)
68 .copySync(outputDirectory.childFile('vm_snapshot_data').path);
69 environment.fileSystem
70 .file(isolateSnapshotData)
71 .copySync(outputDirectory.childFile('isolate_snapshot_data').path);
72 }
73 final Depfile assetDepfile = await copyAssets(
74 environment,
75 outputDirectory,
76 targetPlatform: TargetPlatform.android,
77 buildMode: buildMode,
78 flavor: environment.defines[kFlavor],
79 additionalContent: {
80 'NativeAssetsManifest.json': DevFSFileContent(
81 environment.buildDir.childFile('native_assets.json'),
82 ),
83 },
84 );
85 environment.depFileService.writeToFile(
86 assetDepfile,
87 environment.buildDir.childFile('flutter_assets.d'),
88 );
89 }
90
91 @override
92 List get dependencies => const [KernelSnapshot(), InstallCodeAssets()];
93}
94
95/// An implementation of [AndroidAssetBundle] that includes dependencies on vm
96/// and isolate data.
97class DebugAndroidApplication extends AndroidAssetBundle {
98 const DebugAndroidApplication();
99
100 @override
101 String get name => 'debug_android_application';
102
103 @override
104 List get inputs => [
105 ...super.inputs,
106 const Source.artifact(Artifact.vmSnapshotData, mode: BuildMode.debug),
107 const Source.artifact(Artifact.isolateSnapshotData, mode: BuildMode.debug),
108 ];
109
110 @override
111 List get outputs => [
112 ...super.outputs,
113 const Source.pattern('{OUTPUT_DIR}/flutter_assets/vm_snapshot_data'),
114 const Source.pattern('{OUTPUT_DIR}/flutter_assets/isolate_snapshot_data'),
115 const Source.pattern('{OUTPUT_DIR}/flutter_assets/kernel_blob.bin'),
116 ];
117}
118
119/// An implementation of [AndroidAssetBundle] that only includes assets.
120class AotAndroidAssetBundle extends AndroidAssetBundle {
121 const AotAndroidAssetBundle();
122
123 @override
124 String get name => 'aot_android_asset_bundle';
125}
126
127/// Build a profile android application's Dart artifacts.
128class ProfileAndroidApplication extends CopyFlutterAotBundle {
129 const ProfileAndroidApplication();
130
131 @override
132 String get name => 'profile_android_application';
133
134 @override
135 List get dependencies => const [
136 AotElfProfile(TargetPlatform.android_arm),
137 AotAndroidAssetBundle(),
138 ];
139}
140
141/// Build a release android application's Dart artifacts.
142class ReleaseAndroidApplication extends CopyFlutterAotBundle {
143 const ReleaseAndroidApplication();
144
145 @override
146 String get name => 'release_android_application';
147
148 @override
149 List get dependencies => const [
150 AotElfRelease(TargetPlatform.android_arm),
151 AotAndroidAssetBundle(),
152 ];
153}
154
155/// Generate an ELF binary from a dart kernel file in release mode.
156///
157/// This rule implementation outputs the generated so to a unique location
158/// based on the Android ABI. This allows concurrent invocations of gen_snapshot
159/// to run simultaneously.
160///
161/// The name of an instance of this rule would be 'android_aot_profile_android-x64'
162/// and is relied upon by flutter.gradle to match the correct rule.
163///
164/// It will produce an 'app.so` in the build directory under a folder named with
165/// the matching Android ABI.
166class AndroidAot extends AotElfBase {
167 /// Create an [AndroidAot] implementation for a given [targetPlatform] and [buildMode].
168 const AndroidAot(this.targetPlatform, this.buildMode);
169
170 /// The name of the produced Android ABI.
171 String get _androidAbiName {
172 return getAndroidArchForName(getNameForTargetPlatform(targetPlatform)).archName;
173 }
174
175 @override
176 String get name =>
177 'android_aot_${buildMode.cliName}_'
178 '${getNameForTargetPlatform(targetPlatform)}';
179
180 /// The specific Android ABI we are building for.
181 final TargetPlatform targetPlatform;
182
183 /// The selected build mode.
184 ///
185 /// Build mode is restricted to [BuildMode.profile] or [BuildMode.release] for AOT builds.
186 final BuildMode buildMode;
187
188 @override
189 List get inputs => [
190 const Source.pattern(
191 '{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/android.dart',
192 ),
193 const Source.pattern('{BUILD_DIR}/app.dill'),
194 const Source.artifact(Artifact.engineDartBinary),
195 const Source.artifact(Artifact.skyEnginePath),
196 Source.artifact(Artifact.genSnapshot, mode: buildMode, platform: targetPlatform),
197 ];
198
199 @override
200 List get outputs => [Source.pattern('{BUILD_DIR}/$_androidAbiName/app.so')];
201
202 @override
203 List get depfiles => ['flutter_$name.d'];
204
205 @override
206 List get dependencies => const [KernelSnapshot()];
207
208 @override
209 Future build(Environment environment) async {
210 final snapshotter = AOTSnapshotter(
211 fileSystem: environment.fileSystem,
212 logger: environment.logger,
213 xcode: globals.xcode!,
214 processManager: environment.processManager,
215 artifacts: environment.artifacts,
216 );
217 final Directory output = environment.buildDir.childDirectory(_androidAbiName);
218 final String? buildModeEnvironment = environment.defines[kBuildMode];
219 if (buildModeEnvironment == null) {
220 throw MissingDefineException(kBuildMode, 'aot_elf');
221 }
222 if (!output.existsSync()) {
223 output.createSync(recursive: true);
224 }
225 final List extraGenSnapshotOptions = decodeCommaSeparated(
226 environment.defines,
227 kExtraGenSnapshotOptions,
228 );
229 final outputs = []; // outputs for the depfile
230 final manifestPath = '${output.path}${environment.platform.pathSeparator}manifest.json';
231 if (environment.defines[kDeferredComponents] == 'true') {
232 extraGenSnapshotOptions.add('--loading_unit_manifest=$manifestPath');
233 outputs.add(environment.fileSystem.file(manifestPath));
234 }
235 final buildMode = BuildMode.fromCliName(buildModeEnvironment);
236 final dartObfuscation = environment.defines[kDartObfuscation] == 'true';
237 final String? codeSizeDirectory = environment.defines[kCodeSizeDirectory];
238
239 if (codeSizeDirectory != null) {
240 final File codeSizeFile = environment.fileSystem
241 .directory(codeSizeDirectory)
242 .childFile('snapshot.$_androidAbiName.json');
243 final File precompilerTraceFile = environment.fileSystem
244 .directory(codeSizeDirectory)
245 .childFile('trace.$_androidAbiName.json');
246 extraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}');
247 extraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}');
248 }
249
250 final String? splitDebugInfo = environment.defines[kSplitDebugInfo];
251 final int snapshotExitCode = await snapshotter.build(
252 platform: targetPlatform,
253 buildMode: buildMode,
254 mainPath: environment.buildDir.childFile('app.dill').path,
255 outputPath: output.path,
256 extraGenSnapshotOptions: extraGenSnapshotOptions,
257 splitDebugInfo: splitDebugInfo,
258 dartObfuscation: dartObfuscation,
259 );
260 if (snapshotExitCode != 0) {
261 throw Exception('AOT snapshotter exited with code $snapshotExitCode');
262 }
263 if (environment.defines[kDeferredComponents] == 'true') {
264 // Parse the manifest for .so paths
265 final List loadingUnits = LoadingUnit.parseLoadingUnitManifest(
266 environment.fileSystem.file(manifestPath),
267 environment.logger,
268 );
269 for (final unit in loadingUnits) {
270 outputs.add(environment.fileSystem.file(unit.path));
271 }
272 }
273 environment.depFileService.writeToFile(
274 Depfile([], outputs),
275 environment.buildDir.childFile('flutter_$name.d'),
276 writeEmpty: true,
277 );
278 }
279}
280
281// AndroidAot instances used by the bundle rules below.
282const androidArmProfile = AndroidAot(TargetPlatform.android_arm, BuildMode.profile);
283const androidArm64Profile = AndroidAot(TargetPlatform.android_arm64, BuildMode.profile);
284const androidx64Profile = AndroidAot(TargetPlatform.android_x64, BuildMode.profile);
285const androidArmRelease = AndroidAot(TargetPlatform.android_arm, BuildMode.release);
286const androidArm64Release = AndroidAot(TargetPlatform.android_arm64, BuildMode.release);
287const androidx64Release = AndroidAot(TargetPlatform.android_x64, BuildMode.release);
288
289/// A rule paired with [AndroidAot] that copies the produced so file and manifest.json (if present) into the output directory.
290class AndroidAotBundle extends Target {
291 /// Create an [AndroidAotBundle] implementation for a given [targetPlatform] and [buildMode].
292 const AndroidAotBundle(this.dependency);
293
294 /// The [AndroidAot] instance this bundle rule depends on.
295 final AndroidAot dependency;
296
297 /// The name of the produced Android ABI.
298 String get _androidAbiName {
299 return getAndroidArchForName(getNameForTargetPlatform(dependency.targetPlatform)).archName;
300 }
301
302 @override
303 String get name =>
304 'android_aot_bundle_${dependency.buildMode.cliName}_'
305 '${getNameForTargetPlatform(dependency.targetPlatform)}';
306
307 TargetPlatform get targetPlatform => dependency.targetPlatform;
308
309 /// The selected build mode.
310 ///
311 /// This is restricted to [BuildMode.profile] or [BuildMode.release].
312 BuildMode get buildMode => dependency.buildMode;
313
314 @override
315 List get inputs => [Source.pattern('{BUILD_DIR}/$_androidAbiName/app.so')];
316
317 // flutter.gradle has been updated to correctly consume it.
318 @override
319 List get outputs => [Source.pattern('{OUTPUT_DIR}/$_androidAbiName/app.so')];
320
321 @override
322 List get depfiles => ['flutter_$name.d'];
323
324 @override
325 List get dependencies => [dependency, const AotAndroidAssetBundle()];
326
327 @override
328 Future build(Environment environment) async {
329 final Directory buildDir = environment.buildDir.childDirectory(_androidAbiName);
330 final Directory outputDirectory = environment.outputDir.childDirectory(_androidAbiName);
331 if (!outputDirectory.existsSync()) {
332 outputDirectory.createSync(recursive: true);
333 }
334 final File outputLibFile = buildDir.childFile('app.so');
335 outputLibFile.copySync(outputDirectory.childFile('app.so').path);
336
337 final inputs = [];
338 final outputs = [];
339 final File manifestFile = buildDir.childFile('manifest.json');
340 if (manifestFile.existsSync()) {
341 final File destinationFile = outputDirectory.childFile('manifest.json');
342 manifestFile.copySync(destinationFile.path);
343 inputs.add(manifestFile);
344 outputs.add(destinationFile);
345 }
346 environment.depFileService.writeToFile(
347 Depfile(inputs, outputs),
348 environment.buildDir.childFile('flutter_$name.d'),
349 writeEmpty: true,
350 );
351 }
352}
353
354// AndroidBundleAot instances.
355const androidArmProfileBundle = AndroidAotBundle(androidArmProfile);
356const androidArm64ProfileBundle = AndroidAotBundle(androidArm64Profile);
357const androidx64ProfileBundle = AndroidAotBundle(androidx64Profile);
358const androidArmReleaseBundle = AndroidAotBundle(androidArmRelease);
359const androidArm64ReleaseBundle = AndroidAotBundle(androidArm64Release);
360const androidx64ReleaseBundle = AndroidAotBundle(androidx64Release);
361
362// Rule that copies split aot library files to the intermediate dirs of each deferred component.
363class AndroidAotDeferredComponentsBundle extends Target {
364 /// Create an [AndroidAotDeferredComponentsBundle] implementation for a given [targetPlatform] and [BuildInfo.mode].
365 ///
366 /// If [components] is not provided, it will be read from the `pubspec.yaml` manifest.
367 AndroidAotDeferredComponentsBundle(this.dependency, {List? components})
368 : _components = components;
369
370 /// The [AndroidAotBundle] instance this bundle rule depends on.
371 final AndroidAotBundle dependency;
372
373 List? _components;
374
375 /// The name of the produced Android ABI.
376 String get _androidAbiName {
377 return getAndroidArchForName(getNameForTargetPlatform(dependency.targetPlatform)).archName;
378 }
379
380 @override
381 String get name =>
382 'android_aot_deferred_components_bundle_${dependency.buildMode.cliName}_'
383 '${getNameForTargetPlatform(dependency.targetPlatform)}';
384
385 TargetPlatform get targetPlatform => dependency.targetPlatform;
386
387 @override
388 List get inputs => [
389 // Tracking app.so is enough to invalidate the dynamically named
390 // loading unit libs as changes to loading units guarantee
391 // changes to app.so as well. This task does not actually
392 // copy app.so.
393 Source.pattern('{OUTPUT_DIR}/$_androidAbiName/app.so'),
394 const Source.pattern('{PROJECT_DIR}/pubspec.yaml'),
395 ];
396
397 @override
398 List get outputs => const [];
399
400 @override
401 List get depfiles => ['flutter_$name.d'];
402
403 @override
404 List get dependencies => [dependency];
405
406 @override
407 Future build(Environment environment) async {
408 _components ??= FlutterProject.current().manifest.deferredComponents ?? [];
409 final abis = [_androidAbiName];
410 final List generatedLoadingUnits = LoadingUnit.parseGeneratedLoadingUnits(
411 environment.outputDir,
412 environment.logger,
413 abis: abis,
414 );
415 for (final DeferredComponent component in _components!) {
416 component.assignLoadingUnits(generatedLoadingUnits);
417 }
418 final Depfile libDepfile = copyDeferredComponentSoFiles(
419 environment,
420 _components!,
421 generatedLoadingUnits,
422 environment.projectDir.childDirectory('build'),
423 abis,
424 dependency.buildMode,
425 );
426
427 final File manifestFile = environment.outputDir
428 .childDirectory(_androidAbiName)
429 .childFile('manifest.json');
430 if (manifestFile.existsSync()) {
431 libDepfile.inputs.add(manifestFile);
432 }
433
434 environment.depFileService.writeToFile(
435 libDepfile,
436 environment.buildDir.childFile('flutter_$name.d'),
437 writeEmpty: true,
438 );
439 }
440}
441
442Target androidArmProfileDeferredComponentsBundle = AndroidAotDeferredComponentsBundle(
443 androidArmProfileBundle,
444);
445Target androidArm64ProfileDeferredComponentsBundle = AndroidAotDeferredComponentsBundle(
446 androidArm64ProfileBundle,
447);
448Target androidx64ProfileDeferredComponentsBundle = AndroidAotDeferredComponentsBundle(
449 androidx64ProfileBundle,
450);
451Target androidArmReleaseDeferredComponentsBundle = AndroidAotDeferredComponentsBundle(
452 androidArmReleaseBundle,
453);
454Target androidArm64ReleaseDeferredComponentsBundle = AndroidAotDeferredComponentsBundle(
455 androidArm64ReleaseBundle,
456);
457Target androidx64ReleaseDeferredComponentsBundle = AndroidAotDeferredComponentsBundle(
458 androidx64ReleaseBundle,
459);
460
461/// A set of all target names that build deferred component apps.
462var deferredComponentsTargets = {
463 androidArmProfileDeferredComponentsBundle.name,
464 androidArm64ProfileDeferredComponentsBundle.name,
465 androidx64ProfileDeferredComponentsBundle.name,
466 androidArmReleaseDeferredComponentsBundle.name,
467 androidArm64ReleaseDeferredComponentsBundle.name,
468 androidx64ReleaseDeferredComponentsBundle.name,
469};
470
471/// Utility method to copy and rename the required .so shared libs from the build output
472/// to the correct component intermediate directory.
473///
474/// The [DeferredComponent]s passed to this method must have had loading units assigned.
475/// Assigned components are components that have determined which loading units contains
476/// the dart libraries it has via the DeferredComponent.assignLoadingUnits method.
477Depfile copyDeferredComponentSoFiles(
478 Environment env,
479 List components,
480 List loadingUnits,
481 Directory buildDir, // generally `/build`
482 List abis,
483 BuildMode buildMode,
484) {
485 final inputs = [];
486 final outputs = [];
487 final usedLoadingUnits = {};
488 // Copy all .so files for loading units that are paired with a deferred component.
489 for (final abi in abis) {
490 for (final component in components) {
491 final Set? loadingUnits = component.loadingUnits;
492 if (loadingUnits == null || !component.assigned) {
493 env.logger.printError('Deferred component require loading units to be assigned.');
494 return Depfile(inputs, outputs);
495 }
496 for (final LoadingUnit unit in loadingUnits) {
497 // ensure the abi for the unit is one of the abis we build for.
498 final List? splitPath = unit.path?.split(env.fileSystem.path.separator);
499 if (splitPath == null || splitPath[splitPath.length - 2] != abi) {
500 continue;
501 }
502 usedLoadingUnits.add(unit.id);
503 // the deferred_libs directory is added as a source set for the component.
504 final File destination = buildDir
505 .childDirectory(component.name)
506 .childDirectory('intermediates')
507 .childDirectory('flutter')
508 .childDirectory(buildMode.cliName)
509 .childDirectory('deferred_libs')
510 .childDirectory(abi)
511 .childFile('libapp.so-${unit.id}.part.so');
512 if (!destination.existsSync()) {
513 destination.createSync(recursive: true);
514 }
515 final File source = env.fileSystem.file(unit.path);
516 source.copySync(destination.path);
517 inputs.add(source);
518 outputs.add(destination);
519 }
520 }
521 }
522 // Copy unused loading units, which are included in the base module.
523 for (final abi in abis) {
524 for (final unit in loadingUnits) {
525 if (usedLoadingUnits.contains(unit.id)) {
526 continue;
527 }
528 // ensure the abi for the unit is one of the abis we build for.
529 final List? splitPath = unit.path?.split(env.fileSystem.path.separator);
530 if (splitPath == null || splitPath[splitPath.length - 2] != abi) {
531 continue;
532 }
533 final File destination = env.outputDir
534 .childDirectory(abi)
535 // Omit 'lib' prefix here as it is added by the gradle task that adds 'lib' to 'app.so'.
536 .childFile('app.so-${unit.id}.part.so');
537 if (!destination.existsSync()) {
538 destination.createSync(recursive: true);
539 }
540 final File source = env.fileSystem.file(unit.path);
541 source.copySync(destination.path);
542 inputs.add(source);
543 outputs.add(destination);
544 }
545 }
546 return Depfile(inputs, outputs);
547}
548