build.dart 20.3 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 '../android/android_sdk.dart';
10
import '../artifacts.dart';
11
import '../build_info.dart';
12
import '../bundle.dart';
13
import '../cache.dart';
14 15
import '../compile.dart';
import '../dart/package_map.dart';
16
import '../globals.dart';
17
import '../ios/mac.dart';
18
import '../project.dart';
19
import 'context.dart';
20
import 'file_system.dart';
21
import 'fingerprint.dart';
22 23
import 'process.dart';

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

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

  final TargetPlatform platform;
  final BuildMode mode;
33 34 35

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

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

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

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

    final String snapshotterPath = getSnapshotterPath(snapshotType);
57 58 59 60 61 62

    // iOS gen_snapshot is a multi-arch binary. Running as an i386 binary will
    // generate armv7 code. Running as an x86_64 binary will generate arm64
    // code. /usr/bin/arch can be used to run binaries with the specified
    // architecture.
    if (snapshotType.platform == TargetPlatform.ios) {
63 64
      final String hostArch = iosArch == IOSArch.armv7 ? '-i386' : '-x86_64';
      return runCommandAndStreamOutput(<String>['/usr/bin/arch', hostArch, snapshotterPath]..addAll(args));
65
    }
66 67 68
    return runCommandAndStreamOutput(<String>[snapshotterPath]..addAll(args));
  }
}
69

70
class AOTSnapshotter {
71 72 73 74 75 76 77
  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;

78
  /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script.
79
  Future<int> build({
80 81 82 83 84
    @required TargetPlatform platform,
    @required BuildMode buildMode,
    @required String mainPath,
    @required String packagesPath,
    @required String outputPath,
85
    @required bool buildSharedLibrary,
86
    IOSArch iosArch,
87
    List<String> extraGenSnapshotOptions = const <String>[],
88
  }) async {
89 90
    FlutterProject flutterProject;
    if (fs.file('pubspec.yaml').existsSync()) {
91
      flutterProject = FlutterProject.current();
92
    }
93
    if (!_isValidAotPlatform(platform, buildMode)) {
94
      printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
95
      return 1;
96
    }
97 98
    // TODO(cbracken): replace IOSArch with TargetPlatform.ios_{armv7,arm64}.
    assert(platform != TargetPlatform.ios || iosArch != null);
99

100 101 102 103
    // buildSharedLibrary is ignored for iOS builds.
    if (platform == TargetPlatform.ios)
      buildSharedLibrary = false;

104 105
    if (buildSharedLibrary && androidSdk.ndk == null) {
      final String explanation = AndroidNdk.explainMissingNdk(androidSdk.directory);
106
      printError(
107 108 109 110
        'Could not find NDK in Android SDK at ${androidSdk.directory}:\n'
        '\n'
        '  $explanation\n'
        '\n'
111 112 113 114
        'Unable to build with --build-shared-library\n'
        'To install the NDK, see instructions at https://developer.android.com/ndk/guides/'
      );
      return 1;
115 116
    }

117
    final PackageMap packageMap = PackageMap(packagesPath);
118 119 120
    final String packageMapError = packageMap.checkValid();
    if (packageMapError != null) {
      printError(packageMapError);
121
      return 1;
122 123
    }

124 125 126
    final Directory outputDir = fs.directory(outputPath);
    outputDir.createSync(recursive: true);

127 128 129 130
    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');

131
    final List<String> inputPaths = <String>[uiPath, vmServicePath, mainPath];
132
    final Set<String> outputPaths = <String>{};
133

134
    final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d');
135
    final List<String> genSnapshotArgs = <String>[
136
      '--deterministic',
137
    ];
138 139
    if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
      printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
140 141 142
      genSnapshotArgs.addAll(extraGenSnapshotOptions);
    }

143
    final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
144
    if (buildSharedLibrary || platform == TargetPlatform.ios) {
145 146 147 148 149 150
      // Assembly AOT snapshot.
      outputPaths.add(assembly);
      genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');
      genSnapshotArgs.add('--assembly=$assembly');
    } else {
      // Blob AOT snapshot.
151 152
      final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data');
      final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');
153 154
      final String vmSnapshotInstructions = fs.path.join(outputDir.path, 'vm_snapshot_instr');
      final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');
155
      outputPaths.addAll(<String>[vmSnapshotData, isolateSnapshotData, vmSnapshotInstructions, isolateSnapshotInstructions]);
156 157
      genSnapshotArgs.addAll(<String>[
        '--snapshot_kind=app-aot-blobs',
158 159
        '--vm_snapshot_data=$vmSnapshotData',
        '--isolate_snapshot_data=$isolateSnapshotData',
160 161 162 163 164
        '--vm_snapshot_instructions=$vmSnapshotInstructions',
        '--isolate_snapshot_instructions=$isolateSnapshotInstructions',
      ]);
    }

165
    if (platform == TargetPlatform.android_arm || iosArch == IOSArch.armv7) {
166
      // Use softfp for Android armv7 devices.
Ian Hickson's avatar
Ian Hickson committed
167
      // This is the default for armv7 iOS builds, but harmless to set.
168
      // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489
169 170
      genSnapshotArgs.add('--no-sim-use-hardfp');

171 172
      // Not supported by the Pixel in 32-bit mode.
      genSnapshotArgs.add('--no-use-integer-division');
173 174
    }

175
    genSnapshotArgs.add(mainPath);
176

177 178 179 180
    // 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');
181
      return 1;
182 183 184
    }

    // If inputs and outputs have not changed since last run, skip the build.
185
    final Fingerprinter fingerprinter = Fingerprinter(
186 187 188 189 190 191
      fingerprintPath: '$depfilePath.fingerprint',
      paths: <String>[mainPath]..addAll(inputPaths)..addAll(outputPaths),
      properties: <String, String>{
        'buildMode': buildMode.toString(),
        'targetPlatform': platform.toString(),
        'entryPoint': mainPath,
192
        'sharedLib': buildSharedLibrary.toString(),
193
        'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
194
        'engineHash': Cache.instance.engineRevision,
195
        'buildersUsed': '${flutterProject != null ? flutterProject.hasBuilders : false}',
196
      },
197
      depfilePaths: <String>[],
198
    );
199 200 201 202 203
    // TODO(jonahwilliams): re-enable once this can be proved correct.
    // if (await fingerprinter.doesFingerprintMatch()) {
    //   printTrace('Skipping AOT snapshot build. Fingerprint match.');
    //   return 0;
    // }
204

205
    final SnapshotType snapshotType = SnapshotType(platform, buildMode);
206
    final int genSnapshotExitCode = await _timedStep('snapshot(CompileTime)', () => genSnapshot.run(
207
      snapshotType: snapshotType,
208
      additionalArgs: genSnapshotArgs,
209
      iosArch: iosArch,
210
    ));
211 212
    if (genSnapshotExitCode != 0) {
      printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
213
      return genSnapshotExitCode;
214 215 216 217
    }

    // Write path to gen_snapshot, since snapshots have to be re-generated when we roll
    // the Dart SDK.
218
    final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType);
219
    await outputDir.childFile('gen_snapshot.d').writeAsString('gen_snapshot.d: $genSnapshotPath\n');
220 221 222 223

    // On iOS, we use Xcode to compile the snapshot into a dynamic library that the
    // end-developer can link into their app.
    if (platform == TargetPlatform.ios) {
224
      final RunResult result = await _buildIosFramework(iosArch: iosArch, assemblyPath: assembly, outputPath: outputDir.path);
225 226
      if (result.exitCode != 0)
        return result.exitCode;
227
    } else if (buildSharedLibrary) {
228
      final RunResult result = await _buildAndroidSharedLibrary(assemblyPath: assembly, outputPath: outputDir.path);
229 230
      if (result.exitCode != 0) {
        printError('Failed to build AOT snapshot. Compiler terminated with exit code ${result.exitCode}');
231
        return result.exitCode;
232
      }
233 234 235
    }

    // Compute and record build fingerprint.
236
    await fingerprinter.writeFingerprint();
237 238 239
    return 0;
  }

240 241 242
  /// Builds an iOS framework at [outputPath]/App.framework from the assembly
  /// source at [assemblyPath].
  Future<RunResult> _buildIosFramework({
243
    @required IOSArch iosArch,
244 245 246
    @required String assemblyPath,
    @required String outputPath,
  }) async {
247 248 249
    final String targetArch = iosArch == IOSArch.armv7 ? 'armv7' : 'arm64';
    printStatus('Building App.framework for $targetArch...');
    final List<String> commonBuildOptions = <String>['-arch', targetArch, '-miphoneos-version-min=8.0'];
250 251 252

    final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o');
    final RunResult compileResult = await xcode.cc(commonBuildOptions.toList()..addAll(<String>['-c', assemblyPath, '-o', assemblyO]));
253 254
    if (compileResult.exitCode != 0) {
      printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
255
      return compileResult;
256
    }
257 258 259 260 261 262 263 264 265 266 267 268 269

    final String frameworkDir = fs.path.join(outputPath, 'App.framework');
    fs.directory(frameworkDir).createSync(recursive: true);
    final String appLib = fs.path.join(frameworkDir, 'App');
    final List<String> linkArgs = commonBuildOptions.toList()..addAll(<String>[
        '-dynamiclib',
        '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
        '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
        '-install_name', '@rpath/App.framework/App',
        '-o', appLib,
        assemblyO,
    ]);
    final RunResult linkResult = await xcode.clang(linkArgs);
270 271 272
    if (linkResult.exitCode != 0) {
      printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}');
    }
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
    return linkResult;
  }

  /// Builds an Android shared library at [outputPath]/app.so from the assembly
  /// source at [assemblyPath].
  Future<RunResult> _buildAndroidSharedLibrary({
    @required String assemblyPath,
    @required String outputPath,
  }) async {
    // A word of warning: Instead of compiling via two steps, to a .o file and
    // then to a .so file we use only one command. When using two commands
    // gcc will end up putting a .eh_frame and a .debug_frame into the shared
    // library. Without stripping .debug_frame afterwards, unwinding tools
    // based upon libunwind use just one and ignore the contents of the other
    // (which causes it to not look into the other section and therefore not
    // find the correct unwinding information).
    final String assemblySo = fs.path.join(outputPath, 'app.so');
290 291
    return await runCheckedAsync(<String>[androidSdk.ndk.compiler]
        ..addAll(androidSdk.ndk.compilerArgs)
292 293 294
        ..addAll(<String>[ '-shared', '-nostdlib', '-o', assemblySo, assemblyPath ]));
  }

295 296 297 298 299 300 301
  /// 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,
302
    @required String packagesPath,
303
    @required String outputPath,
304
    @required bool trackWidgetCreation,
305
    List<String> extraFrontEndOptions = const <String>[],
306
  }) async {
307
    final FlutterProject flutterProject = FlutterProject.current();
308 309 310 311 312 313 314 315
    final Directory outputDir = fs.directory(outputPath);
    outputDir.createSync(recursive: true);

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

    if ((extraFrontEndOptions != null) && extraFrontEndOptions.isNotEmpty)
      printTrace('Extra front-end options: $extraFrontEndOptions');

316
    final String depfilePath = fs.path.join(outputPath, 'kernel_compile.d');
317
    final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(flutterProject);
318
    final CompilerOutput compilerOutput = await _timedStep('frontend(CompileTime)', () => kernelCompiler.compile(
319
      sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath, mode: buildMode),
320
      mainPath: mainPath,
321
      packagesPath: packagesPath,
322 323 324 325
      outputFilePath: getKernelPathForTransformerOptions(
        fs.path.join(outputPath, 'app.dill'),
        trackWidgetCreation: trackWidgetCreation,
      ),
326
      depFilePath: depfilePath,
327 328 329
      extraFrontEndOptions: extraFrontEndOptions,
      linkPlatformKernelIn: true,
      aot: true,
330
      trackWidgetCreation: trackWidgetCreation,
331
      targetProductVm: buildMode == BuildMode.release,
332
    ));
333 334 335 336 337 338 339 340

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

    return compilerOutput?.outputFilename;
  }

341
  bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
342
    if (buildMode == BuildMode.debug)
343
      return false;
344 345 346 347 348 349 350
    return const <TargetPlatform>[
      TargetPlatform.android_arm,
      TargetPlatform.android_arm64,
      TargetPlatform.ios,
    ].contains(platform);
  }

351 352
  String _getPackagePath(PackageMap packageMap, String package) {
    return fs.path.dirname(fs.path.fromUri(packageMap.map[package]));
353
  }
354 355 356 357 358 359 360 361 362 363

  /// 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.
  Future<T> _timedStep<T>(String marker, FutureOr<T> Function() action) async {
    final Stopwatch sw = Stopwatch()..start();
    final T value = await action();
    if (reportTimings) {
364
      printStatus('$marker: ${sw.elapsedMilliseconds} ms.');
365 366 367
    }
    return value;
  }
368
}
369

370 371 372
class JITSnapshotter {
  /// Builds a JIT VM snapshot of the specified kernel. This snapshot includes
  /// data as well as either machine code or DBC, depending on build configuration.
373 374 375 376 377 378
  Future<int> build({
    @required TargetPlatform platform,
    @required BuildMode buildMode,
    @required String mainPath,
    @required String packagesPath,
    @required String outputPath,
379
    @required String compilationTraceFilePath,
380 381
    List<String> extraGenSnapshotOptions = const <String>[],
  }) async {
382 383
    if (!_isValidJitPlatform(platform)) {
      printError('${getNameForTargetPlatform(platform)} does not support JIT snapshotting.');
384 385 386 387 388 389
      return 1;
    }

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

390 391
    final String engineVmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: buildMode);
    final String engineIsolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: buildMode);
392 393 394 395 396 397
    final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');
    final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');

    final List<String> inputPaths = <String>[
      mainPath, compilationTraceFilePath, engineVmSnapshotData, engineIsolateSnapshotData,
    ];
398

399 400
    final String depfilePath = fs.path.join(outputDir.path, 'snapshot.d');
    final List<String> genSnapshotArgs = <String>[
401
      '--deterministic',
402 403 404 405 406 407 408 409 410
    ];
    if (buildMode == BuildMode.debug) {
      genSnapshotArgs.add('--enable_asserts');
    }
    if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
      printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
      genSnapshotArgs.addAll(extraGenSnapshotOptions);
    }

411
    final Set<String> outputPaths = <String>{};
412
    outputPaths.addAll(<String>[isolateSnapshotData, isolateSnapshotInstructions]);
413

414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
    // There are a couple special cases below where we create a snapshot
    // with only the data section, which only contains interpreted code.
    bool supportsAppJit = true;

    if (platform == TargetPlatform.android_x64 &&
        getCurrentHostPlatform() == HostPlatform.windows_x64) {
      supportsAppJit = false;
      printStatus('Android x64 dynamic build on Windows x64 will use purely interpreted '
                  'code for now (see  https://github.com/flutter/flutter/issues/17489).');
    }

    if (platform == TargetPlatform.android_x86) {
      supportsAppJit = false;
      printStatus('Android x86 dynamic build will use purely interpreted code for now. '
                  'To optimize performance, consider using --target-platform=android-x64.');
    }

431
    genSnapshotArgs.addAll(<String>[
432
      '--snapshot_kind=${supportsAppJit ? 'app-jit' : 'app'}',
433
      '--load_compilation_trace=$compilationTraceFilePath',
434 435 436
      '--load_vm_snapshot_data=$engineVmSnapshotData',
      '--load_isolate_snapshot_data=$engineIsolateSnapshotData',
      '--isolate_snapshot_data=$isolateSnapshotData',
437 438
    ]);

439
    genSnapshotArgs.add('--isolate_snapshot_instructions=$isolateSnapshotInstructions');
440

441 442
    if (platform == TargetPlatform.android_arm) {
      // Use softfp for Android armv7 devices.
443
      // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
      genSnapshotArgs.add('--no-sim-use-hardfp');

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

    genSnapshotArgs.add(mainPath);

    // 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');
      return 1;
    }

    // If inputs and outputs have not changed since last run, skip the build.
460
    final Fingerprinter fingerprinter = Fingerprinter(
461 462 463 464 465 466 467 468
      fingerprintPath: '$depfilePath.fingerprint',
      paths: <String>[mainPath]..addAll(inputPaths)..addAll(outputPaths),
      properties: <String, String>{
        'buildMode': buildMode.toString(),
        'targetPlatform': platform.toString(),
        'entryPoint': mainPath,
        'extraGenSnapshotOptions': extraGenSnapshotOptions.join(' '),
      },
469
      depfilePaths: <String>[],
470
    );
471 472 473 474 475
    // TODO(jonahwilliams): re-enable once this can be proved correct.
    // if (await fingerprinter.doesFingerprintMatch()) {
    //   printTrace('Skipping JIT snapshot build. Fingerprint match.');
    //   return 0;
    // }
476

477
    final SnapshotType snapshotType = SnapshotType(platform, buildMode);
478 479 480 481 482 483 484 485 486 487 488 489
    final int genSnapshotExitCode = await genSnapshot.run(
      snapshotType: snapshotType,
      additionalArgs: genSnapshotArgs,
    );
    if (genSnapshotExitCode != 0) {
      printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
      return genSnapshotExitCode;
    }

    // Write path to gen_snapshot, since snapshots have to be re-generated when we roll
    // the Dart SDK.
    final String genSnapshotPath = GenSnapshot.getSnapshotterPath(snapshotType);
490
    await outputDir.childFile('gen_snapshot.d').writeAsString('gen_snapshot.d: $genSnapshotPath\n');
491 492 493 494 495 496

    // Compute and record build fingerprint.
    await fingerprinter.writeFingerprint();
    return 0;
  }

497
  bool _isValidJitPlatform(TargetPlatform platform) {
498 499 500
    return const <TargetPlatform>[
      TargetPlatform.android_arm,
      TargetPlatform.android_arm64,
501 502
      TargetPlatform.android_x86,
      TargetPlatform.android_x64,
503 504 505
    ].contains(platform);
  }
}