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