build.dart 12.8 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6

7
import 'package:meta/meta.dart';
8

9
import '../artifacts.dart';
10
import '../build_info.dart';
11
import '../bundle.dart';
12 13
import '../compile.dart';
import '../dart/package_map.dart';
14
import '../globals.dart';
15
import '../macos/xcode.dart';
16
import '../project.dart';
17
import '../reporting/reporting.dart';
18

19
import 'context.dart';
20
import 'file_system.dart';
21 22
import 'process.dart';

23
GenSnapshot get genSnapshot => context.get<GenSnapshot>();
24 25 26

/// A snapshot build configuration.
class SnapshotType {
27 28
  SnapshotType(this.platform, this.mode)
    : assert(mode != null);
29 30 31

  final TargetPlatform platform;
  final BuildMode mode;
32 33 34

  @override
  String toString() => '$platform $mode';
35 36 37 38 39 40
}

/// Interface to the gen_snapshot command-line tool.
class GenSnapshot {
  const GenSnapshot();

41 42
  static String getSnapshotterPath(SnapshotType snapshotType) {
    return artifacts.getArtifactPath(
43
        Artifact.genSnapshot, platform: snapshotType.platform, mode: snapshotType.mode);
44 45
  }

46 47
  Future<int> run({
    @required SnapshotType snapshotType,
48
    DarwinArch darwinArch,
49
    Iterable<String> additionalArgs = const <String>[],
50 51 52
  }) {
    final List<String> args = <String>[
      '--causal_async_stacks',
53 54
      ...additionalArgs,
    ];
55

56
    String snapshotterPath = getSnapshotterPath(snapshotType);
57

58 59
    // iOS has a separate gen_snapshot for armv7 and arm64 in the same,
    // directory. So we need to select the right one.
60
    if (snapshotType.platform == TargetPlatform.ios) {
61
      snapshotterPath += '_' + getNameForDarwinArch(darwinArch);
62 63
    }

64 65 66 67 68 69 70 71
    StringConverter outputFilter;
    if (additionalArgs.contains('--strip')) {
      // Filter out gen_snapshot's warning message about stripping debug symbols
      // from ELF library snapshots.
      const String kStripWarning = 'Warning: Generating ELF library without DWARF debugging information.';
      outputFilter = (String line) => line != kStripWarning ? line : null;
    }

72 73 74 75
    return processUtils.stream(
      <String>[snapshotterPath, ...args],
      mapFunction: outputFilter,
    );
76 77
  }
}
78

79
class AOTSnapshotter {
80 81 82 83 84 85 86
  AOTSnapshotter({this.reportTimings = false});

  /// If true then AOTSnapshotter would report timings for individual building
  /// steps (Dart front-end parsing and snapshot generation) in a stable
  /// machine readable form. See [AOTSnapshotter._timedStep].
  final bool reportTimings;

87
  /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script.
88
  Future<int> build({
89 90 91 92 93
    @required TargetPlatform platform,
    @required BuildMode buildMode,
    @required String mainPath,
    @required String packagesPath,
    @required String outputPath,
94
    DarwinArch darwinArch,
95
    List<String> extraGenSnapshotOptions = const <String>[],
96
    @required bool bitcode,
97
  }) async {
98 99 100 101 102
    if (bitcode && platform != TargetPlatform.ios) {
      printError('Bitcode is only supported for iOS.');
      return 1;
    }

103
    if (!_isValidAotPlatform(platform, buildMode)) {
104
      printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
105
      return 1;
106
    }
107
    // TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
108
    assert(platform != TargetPlatform.ios || darwinArch != null);
109

110
    final PackageMap packageMap = PackageMap(packagesPath);
111 112 113
    final String packageMapError = packageMap.checkValid();
    if (packageMapError != null) {
      printError(packageMapError);
114
      return 1;
115 116
    }

117 118 119
    final Directory outputDir = fs.directory(outputPath);
    outputDir.createSync(recursive: true);

120 121 122 123
    final String skyEnginePkg = _getPackagePath(packageMap, 'sky_engine');
    final String uiPath = fs.path.join(skyEnginePkg, 'lib', 'ui', 'ui.dart');
    final String vmServicePath = fs.path.join(skyEnginePkg, 'sdk_ext', 'vmservice_io.dart');

124
    final List<String> inputPaths = <String>[uiPath, vmServicePath, mainPath];
125
    final Set<String> outputPaths = <String>{};
126
    final List<String> genSnapshotArgs = <String>[
127
      '--deterministic',
128
    ];
129 130
    if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
      printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
131 132 133
      genSnapshotArgs.addAll(extraGenSnapshotOptions);
    }

134
    final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
135
    if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin_x64) {
136 137 138 139
      // Assembly AOT snapshot.
      outputPaths.add(assembly);
      genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');
      genSnapshotArgs.add('--assembly=$assembly');
140
    } else {
141 142 143
      final String aotSharedLibrary = fs.path.join(outputDir.path, 'app.so');
      outputPaths.add(aotSharedLibrary);
      genSnapshotArgs.add('--snapshot_kind=app-aot-elf');
144
      genSnapshotArgs.add('--elf=$aotSharedLibrary');
145
      genSnapshotArgs.add('--strip');
146 147
    }

148
    if (platform == TargetPlatform.android_arm || darwinArch == DarwinArch.armv7) {
149
      // Use softfp for Android armv7 devices.
Ian Hickson's avatar
Ian Hickson committed
150
      // This is the default for armv7 iOS builds, but harmless to set.
151
      // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489
152 153
      genSnapshotArgs.add('--no-sim-use-hardfp');

154 155
      // Not supported by the Pixel in 32-bit mode.
      genSnapshotArgs.add('--no-use-integer-division');
156 157
    }

158
    genSnapshotArgs.add(mainPath);
159

160 161 162 163
    // Verify that all required inputs exist.
    final Iterable<String> missingInputs = inputPaths.where((String p) => !fs.isFileSync(p));
    if (missingInputs.isNotEmpty) {
      printError('Missing input files: $missingInputs from $inputPaths');
164
      return 1;
165 166
    }

167
    final SnapshotType snapshotType = SnapshotType(platform, buildMode);
168 169 170
    final int genSnapshotExitCode =
      await _timedStep('snapshot(CompileTime)', 'aot-snapshot',
        () => genSnapshot.run(
171
      snapshotType: snapshotType,
172
      additionalArgs: genSnapshotArgs,
173
      darwinArch: darwinArch,
174
    ));
175 176
    if (genSnapshotExitCode != 0) {
      printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
177
      return genSnapshotExitCode;
178 179
    }

180 181 182 183
    // TODO(dnfield): This should be removed when https://github.com/dart-lang/sdk/issues/37560
    // is resolved.
    // The DWARF section confuses Xcode tooling, so this strips it. Ideally,
    // gen_snapshot would provide an argument to do this automatically.
184 185
    final bool stripSymbols = platform == TargetPlatform.ios && buildMode == BuildMode.release && bitcode;
    if (stripSymbols) {
186
      final IOSink sink = fs.file('$assembly.stripped.S').openWrite();
187
      for (String line in fs.file(assembly).readAsLinesSync()) {
188 189 190 191 192
        if (line.startsWith('.section __DWARF')) {
          break;
        }
        sink.writeln(line);
      }
193
      await sink.flush();
194 195 196
      await sink.close();
    }

197 198
    // Write path to gen_snapshot, since snapshots have to be re-generated when we roll
    // the Dart SDK.
199
    final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType);
200
    outputDir.childFile('gen_snapshot.d').writeAsStringSync('gen_snapshot.d: $genSnapshotPath\n');
201

202
    // On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the
203
    // end-developer can link into their app.
204 205 206
    if (platform == TargetPlatform.ios || platform == TargetPlatform.darwin_x64) {
      final RunResult result = await _buildFramework(
        appleArch: darwinArch,
207
        isIOS: platform == TargetPlatform.ios,
208
        assemblyPath: stripSymbols ? '$assembly.stripped.S' : assembly,
209 210 211
        outputPath: outputDir.path,
        bitcode: bitcode,
      );
212
      if (result.exitCode != 0) {
213
        return result.exitCode;
214
      }
215 216 217 218
    }
    return 0;
  }

219
  /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly
220
  /// source at [assemblyPath].
221 222
  Future<RunResult> _buildFramework({
    @required DarwinArch appleArch,
223
    @required bool isIOS,
224 225
    @required String assemblyPath,
    @required String outputPath,
226
    @required bool bitcode,
227
  }) async {
228
    final String targetArch = getNameForDarwinArch(appleArch);
229
    printStatus('Building App.framework for $targetArch...');
230

231 232
    final List<String> commonBuildOptions = <String>[
      '-arch', targetArch,
233
      if (isIOS)
234 235
        '-miphoneos-version-min=8.0',
    ];
236

237
    const String embedBitcodeArg = '-fembed-bitcode';
238
    final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o');
239
    final RunResult compileResult = await xcode.cc(<String>[
240 241
      '-arch', targetArch,
      if (bitcode) embedBitcodeArg,
242 243 244 245 246
      '-c',
      assemblyPath,
      '-o',
      assemblyO,
    ]);
247 248
    if (compileResult.exitCode != 0) {
      printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
249
      return compileResult;
250
    }
251 252 253 254

    final String frameworkDir = fs.path.join(outputPath, 'App.framework');
    fs.directory(frameworkDir).createSync(recursive: true);
    final String appLib = fs.path.join(frameworkDir, 'App');
255 256 257 258 259 260
    final List<String> linkArgs = <String>[
      ...commonBuildOptions,
      '-dynamiclib',
      '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
      '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
      '-install_name', '@rpath/App.framework/App',
261 262
      if (bitcode) embedBitcodeArg,
      if (bitcode && isIOS) ...<String>[embedBitcodeArg, '-isysroot', await xcode.iPhoneSdkLocation()],
263 264 265
      '-o', appLib,
      assemblyO,
    ];
266
    final RunResult linkResult = await xcode.clang(linkArgs);
267 268 269
    if (linkResult.exitCode != 0) {
      printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
    }
270
    return linkResult;
271 272
  }

273 274 275 276 277 278 279
  /// Compiles a Dart file to kernel.
  ///
  /// Returns the output kernel file path, or null on failure.
  Future<String> compileKernel({
    @required TargetPlatform platform,
    @required BuildMode buildMode,
    @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 287 288 289 290
    final Directory outputDir = fs.directory(outputPath);
    outputDir.createSync(recursive: true);

    printTrace('Compiling Dart to kernel: $mainPath');

291
    if ((extraFrontEndOptions != null) && extraFrontEndOptions.isNotEmpty) {
292
      printTrace('Extra front-end options: $extraFrontEndOptions');
293
    }
294

295
    final String depfilePath = fs.path.join(outputPath, 'kernel_compile.d');
296
    final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(flutterProject);
297 298 299
    final CompilerOutput compilerOutput =
      await _timedStep('frontend(CompileTime)', 'aot-kernel',
        () => kernelCompiler.compile(
300
      sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
301
      mainPath: mainPath,
302
      packagesPath: packagesPath,
303 304 305 306
      outputFilePath: getKernelPathForTransformerOptions(
        fs.path.join(outputPath, 'app.dill'),
        trackWidgetCreation: trackWidgetCreation,
      ),
307
      depFilePath: depfilePath,
308 309 310
      extraFrontEndOptions: extraFrontEndOptions,
      linkPlatformKernelIn: true,
      aot: true,
311
      buildMode: buildMode,
312
      trackWidgetCreation: trackWidgetCreation,
313
    ));
314 315 316

    // Write path to frontend_server, since things need to be re-generated when that changes.
    final String frontendPath = artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk);
317
    fs.directory(outputPath).childFile('frontend_server.d').writeAsStringSync('frontend_server.d: $frontendPath\n');
318 319 320 321

    return compilerOutput?.outputFilename;
  }

322
  bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
323
    if (buildMode == BuildMode.debug) {
324
      return false;
325
    }
326 327 328
    return const <TargetPlatform>[
      TargetPlatform.android_arm,
      TargetPlatform.android_arm64,
329
      TargetPlatform.android_x64,
330
      TargetPlatform.ios,
331
      TargetPlatform.darwin_x64,
332 333 334
    ].contains(platform);
  }

335 336
  String _getPackagePath(PackageMap packageMap, String package) {
    return fs.path.dirname(fs.path.fromUri(packageMap.map[package]));
337
  }
338 339 340 341 342 343

  /// This method is used to measure duration of an action and emit it into
  /// verbose output from flutter_tool for other tools (e.g. benchmark runner)
  /// to find.
  /// Important: external performance tracking tools expect format of this
  /// output to be stable.
344
  Future<T> _timedStep<T>(String marker, String analyticsVar, FutureOr<T> Function() action) async {
345 346 347
    final Stopwatch sw = Stopwatch()..start();
    final T value = await action();
    if (reportTimings) {
348
      printStatus('$marker: ${sw.elapsedMilliseconds} ms.');
349
    }
350
    flutterUsage.sendTiming('build', analyticsVar, Duration(milliseconds: sw.elapsedMilliseconds));
351 352
    return value;
  }
353
}