• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium 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 'dart:async';
6
7import 'package:meta/meta.dart';
8
9import '../artifacts.dart';
10import '../build_info.dart';
11import '../bundle.dart';
12import '../compile.dart';
13import '../dart/package_map.dart';
14import '../globals.dart';
15import '../macos/xcode.dart';
16import '../project.dart';
17import '../reporting/reporting.dart';
18
19import 'context.dart';
20import 'file_system.dart';
21import 'process.dart';
22
23GenSnapshot get genSnapshot => context.get<GenSnapshot>();
24
25/// A snapshot build configuration.
26class SnapshotType {
27  SnapshotType(this.platform, this.mode)
28    : assert(mode != null);
29
30  final TargetPlatform platform;
31  final BuildMode mode;
32
33  @override
34  String toString() => '$platform $mode';
35}
36
37/// Interface to the gen_snapshot command-line tool.
38class GenSnapshot {
39  const GenSnapshot();
40
41  static String getSnapshotterPath(SnapshotType snapshotType) {
42    return artifacts.getArtifactPath(
43        Artifact.genSnapshot, platform: snapshotType.platform, mode: snapshotType.mode);
44  }
45
46  Future<int> run({
47    @required SnapshotType snapshotType,
48    DarwinArch darwinArch,
49    Iterable<String> additionalArgs = const <String>[],
50  }) {
51    final List<String> args = <String>[
52      '--causal_async_stacks',
53      ...additionalArgs,
54    ];
55
56    String snapshotterPath = getSnapshotterPath(snapshotType);
57
58    // iOS has a separate gen_snapshot for armv7 and arm64 in the same,
59    // directory. So we need to select the right one.
60    if (snapshotType.platform == TargetPlatform.ios) {
61      snapshotterPath += '_' + getNameForDarwinArch(darwinArch);
62    }
63
64    StringConverter outputFilter;
65    if (additionalArgs.contains('--strip')) {
66      // Filter out gen_snapshot's warning message about stripping debug symbols
67      // from ELF library snapshots.
68      const String kStripWarning = 'Warning: Generating ELF library without DWARF debugging information.';
69      outputFilter = (String line) => line != kStripWarning ? line : null;
70    }
71
72    return runCommandAndStreamOutput(<String>[snapshotterPath, ...args], mapFunction: outputFilter);
73  }
74}
75
76class AOTSnapshotter {
77  AOTSnapshotter({this.reportTimings = false});
78
79  /// If true then AOTSnapshotter would report timings for individual building
80  /// steps (Dart front-end parsing and snapshot generation) in a stable
81  /// machine readable form. See [AOTSnapshotter._timedStep].
82  final bool reportTimings;
83
84  /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script.
85  Future<int> build({
86    @required TargetPlatform platform,
87    @required BuildMode buildMode,
88    @required String mainPath,
89    @required String packagesPath,
90    @required String outputPath,
91    DarwinArch darwinArch,
92    List<String> extraGenSnapshotOptions = const <String>[],
93    @required bool bitcode,
94  }) async {
95    if (bitcode && platform != TargetPlatform.ios) {
96      printError('Bitcode is only supported for iOS.');
97      return 1;
98    }
99
100    if (!_isValidAotPlatform(platform, buildMode)) {
101      printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
102      return 1;
103    }
104    // TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
105    assert(platform != TargetPlatform.ios || darwinArch != null);
106
107    final PackageMap packageMap = PackageMap(packagesPath);
108    final String packageMapError = packageMap.checkValid();
109    if (packageMapError != null) {
110      printError(packageMapError);
111      return 1;
112    }
113
114    final Directory outputDir = fs.directory(outputPath);
115    outputDir.createSync(recursive: true);
116
117    final String skyEnginePkg = _getPackagePath(packageMap, 'sky_engine');
118    final String uiPath = fs.path.join(skyEnginePkg, 'lib', 'ui', 'ui.dart');
119    final String vmServicePath = fs.path.join(skyEnginePkg, 'sdk_ext', 'vmservice_io.dart');
120
121    final List<String> inputPaths = <String>[uiPath, vmServicePath, mainPath];
122    final Set<String> outputPaths = <String>{};
123    final List<String> genSnapshotArgs = <String>[
124      '--deterministic',
125    ];
126    if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
127      printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
128      genSnapshotArgs.addAll(extraGenSnapshotOptions);
129    }
130
131    final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
132    if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin_x64) {
133      // Assembly AOT snapshot.
134      outputPaths.add(assembly);
135      genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');
136      genSnapshotArgs.add('--assembly=$assembly');
137    } else {
138      final String aotSharedLibrary = fs.path.join(outputDir.path, 'app.so');
139      outputPaths.add(aotSharedLibrary);
140      genSnapshotArgs.add('--snapshot_kind=app-aot-elf');
141      genSnapshotArgs.add('--elf=$aotSharedLibrary');
142      genSnapshotArgs.add('--strip');
143    }
144
145    if (platform == TargetPlatform.android_arm || darwinArch == DarwinArch.armv7) {
146      // Use softfp for Android armv7 devices.
147      // This is the default for armv7 iOS builds, but harmless to set.
148      // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489
149      genSnapshotArgs.add('--no-sim-use-hardfp');
150
151      // Not supported by the Pixel in 32-bit mode.
152      genSnapshotArgs.add('--no-use-integer-division');
153    }
154
155    genSnapshotArgs.add(mainPath);
156
157    // Verify that all required inputs exist.
158    final Iterable<String> missingInputs = inputPaths.where((String p) => !fs.isFileSync(p));
159    if (missingInputs.isNotEmpty) {
160      printError('Missing input files: $missingInputs from $inputPaths');
161      return 1;
162    }
163
164    final SnapshotType snapshotType = SnapshotType(platform, buildMode);
165    final int genSnapshotExitCode =
166      await _timedStep('snapshot(CompileTime)', 'aot-snapshot',
167        () => genSnapshot.run(
168      snapshotType: snapshotType,
169      additionalArgs: genSnapshotArgs,
170      darwinArch: darwinArch,
171    ));
172    if (genSnapshotExitCode != 0) {
173      printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
174      return genSnapshotExitCode;
175    }
176
177    // TODO(dnfield): This should be removed when https://github.com/dart-lang/sdk/issues/37560
178    // is resolved.
179    // The DWARF section confuses Xcode tooling, so this strips it. Ideally,
180    // gen_snapshot would provide an argument to do this automatically.
181    if (platform == TargetPlatform.ios && bitcode) {
182      final IOSink sink = fs.file('$assembly.bitcode').openWrite();
183      for (String line in await fs.file(assembly).readAsLines()) {
184        if (line.startsWith('.section __DWARF')) {
185          break;
186        }
187        sink.writeln(line);
188      }
189      await sink.close();
190    }
191
192    // Write path to gen_snapshot, since snapshots have to be re-generated when we roll
193    // the Dart SDK.
194    final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType);
195    await outputDir.childFile('gen_snapshot.d').writeAsString('gen_snapshot.d: $genSnapshotPath\n');
196
197    // On iOS, we use Xcode to compile the snapshot into a dynamic library that the
198    // end-developer can link into their app.
199    if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin_x64) {
200      final RunResult result = await _buildFramework(
201        appleArch: darwinArch,
202        assemblyPath: bitcode ? '$assembly.bitcode' : assembly,
203        outputPath: outputDir.path,
204        bitcode: bitcode,
205      );
206      if (result.exitCode != 0)
207        return result.exitCode;
208    }
209    return 0;
210  }
211
212  /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly
213  /// source at [assemblyPath].
214  Future<RunResult> _buildFramework({
215    @required DarwinArch appleArch,
216    @required String assemblyPath,
217    @required String outputPath,
218    @required bool bitcode,
219  }) async {
220    final String targetArch = getNameForDarwinArch(appleArch);
221    printStatus('Building App.framework for $targetArch...');
222    final List<String> commonBuildOptions = <String>[
223      '-arch', targetArch,
224      if (appleArch == DarwinArch.arm64 || appleArch == DarwinArch.armv7)
225        '-miphoneos-version-min=8.0',
226    ];
227
228    final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o');
229    final RunResult compileResult = await xcode.cc(<String>[
230      ...commonBuildOptions,
231      '-c',
232      assemblyPath,
233      '-o',
234      assemblyO,
235      if (bitcode) '-fembed-bitcode',
236    ]);
237    if (compileResult.exitCode != 0) {
238      printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
239      return compileResult;
240    }
241
242    final String frameworkDir = fs.path.join(outputPath, 'App.framework');
243    fs.directory(frameworkDir).createSync(recursive: true);
244    final String appLib = fs.path.join(frameworkDir, 'App');
245    final List<String> linkArgs = <String>[
246      ...commonBuildOptions,
247      '-dynamiclib',
248      '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
249      '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
250      '-install_name', '@rpath/App.framework/App',
251      if (bitcode) '-fembed-bitcode',
252      '-o', appLib,
253      assemblyO,
254    ];
255    final RunResult linkResult = await xcode.clang(linkArgs);
256    if (linkResult.exitCode != 0) {
257      printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
258      return linkResult;
259    }
260    // See https://github.com/flutter/flutter/issues/22560
261    // These have to be placed in a .noindex folder to prevent Xcode from
262    // using Spotlight to find them and potentially attach the wrong ones.
263    final RunResult dsymResult = await xcode.dsymutil(<String>[
264      appLib,
265      '-o', fs.path.join(outputPath, 'App.framework.dSYM.noindex'),
266    ]);
267    if (dsymResult.exitCode != 0) {
268      printError('Failed to extract dSYM out of dynamic lib');
269    }
270    return dsymResult;
271  }
272
273  /// Compiles a Dart file to kernel.
274  ///
275  /// Returns the output kernel file path, or null on failure.
276  Future<String> compileKernel({
277    @required TargetPlatform platform,
278    @required BuildMode buildMode,
279    @required String mainPath,
280    @required String packagesPath,
281    @required String outputPath,
282    @required bool trackWidgetCreation,
283    List<String> extraFrontEndOptions = const <String>[],
284  }) async {
285    final FlutterProject flutterProject = FlutterProject.current();
286    final Directory outputDir = fs.directory(outputPath);
287    outputDir.createSync(recursive: true);
288
289    printTrace('Compiling Dart to kernel: $mainPath');
290
291    if ((extraFrontEndOptions != null) && extraFrontEndOptions.isNotEmpty)
292      printTrace('Extra front-end options: $extraFrontEndOptions');
293
294    final String depfilePath = fs.path.join(outputPath, 'kernel_compile.d');
295    final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(flutterProject);
296    final CompilerOutput compilerOutput =
297      await _timedStep('frontend(CompileTime)', 'aot-kernel',
298        () => kernelCompiler.compile(
299      sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
300      mainPath: mainPath,
301      packagesPath: packagesPath,
302      outputFilePath: getKernelPathForTransformerOptions(
303        fs.path.join(outputPath, 'app.dill'),
304        trackWidgetCreation: trackWidgetCreation,
305      ),
306      depFilePath: depfilePath,
307      extraFrontEndOptions: extraFrontEndOptions,
308      linkPlatformKernelIn: true,
309      aot: true,
310      trackWidgetCreation: trackWidgetCreation,
311      targetProductVm: buildMode == BuildMode.release,
312    ));
313
314    // Write path to frontend_server, since things need to be re-generated when that changes.
315    final String frontendPath = artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk);
316    await fs.directory(outputPath).childFile('frontend_server.d').writeAsString('frontend_server.d: $frontendPath\n');
317
318    return compilerOutput?.outputFilename;
319  }
320
321  bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
322    if (buildMode == BuildMode.debug)
323      return false;
324    return const <TargetPlatform>[
325      TargetPlatform.android_arm,
326      TargetPlatform.android_arm64,
327      TargetPlatform.ios,
328      TargetPlatform.darwin_x64,
329    ].contains(platform);
330  }
331
332  String _getPackagePath(PackageMap packageMap, String package) {
333    return fs.path.dirname(fs.path.fromUri(packageMap.map[package]));
334  }
335
336  /// This method is used to measure duration of an action and emit it into
337  /// verbose output from flutter_tool for other tools (e.g. benchmark runner)
338  /// to find.
339  /// Important: external performance tracking tools expect format of this
340  /// output to be stable.
341  Future<T> _timedStep<T>(String marker, String analyticsVar, FutureOr<T> Function() action) async {
342    final Stopwatch sw = Stopwatch()..start();
343    final T value = await action();
344    if (reportTimings) {
345      printStatus('$marker: ${sw.elapsedMilliseconds} ms.');
346    }
347    flutterUsage.sendTiming('build', analyticsVar, Duration(milliseconds: sw.elapsedMilliseconds));
348    return value;
349  }
350}
351