// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:process/process.dart'; import '../artifacts.dart'; import '../build_info.dart'; import '../macos/xcode.dart'; import 'file_system.dart'; import 'logger.dart'; import 'process.dart'; /// A snapshot build configuration. class SnapshotType { SnapshotType(this.platform, this.mode); final TargetPlatform? platform; final BuildMode mode; @override String toString() => '$platform $mode'; } /// Interface to the gen_snapshot command-line tool. class GenSnapshot { GenSnapshot({ required Artifacts artifacts, required ProcessManager processManager, required Logger logger, }) : _artifacts = artifacts, _processUtils = ProcessUtils(logger: logger, processManager: processManager); final Artifacts _artifacts; final ProcessUtils _processUtils; String getSnapshotterPath(SnapshotType snapshotType) { return _artifacts.getArtifactPath( Artifact.genSnapshot, platform: snapshotType.platform, mode: snapshotType.mode); } /// Ignored warning messages from gen_snapshot. static const Set kIgnoredWarnings = { // --strip on elf snapshot. 'Warning: Generating ELF library without DWARF debugging information.', // --strip on ios-assembly snapshot. 'Warning: Generating assembly code without DWARF debugging information.', // A fun two-part message with spaces for obfuscation. 'Warning: This VM has been configured to obfuscate symbol information which violates the Dart standard.', ' See dartbug.com/30524 for more information.', }; Future run({ required SnapshotType snapshotType, DarwinArch? darwinArch, Iterable additionalArgs = const [], }) { assert(darwinArch != DarwinArch.armv7); assert(snapshotType.platform != TargetPlatform.ios || darwinArch != null); final List args = [ ...additionalArgs, ]; String snapshotterPath = getSnapshotterPath(snapshotType); // iOS and macOS have separate gen_snapshot binaries for each target // architecture (iOS: armv7, arm64; macOS: x86_64, arm64). Select the right // one for the target architecture in question. if (snapshotType.platform == TargetPlatform.ios || snapshotType.platform == TargetPlatform.darwin) { snapshotterPath += '_${darwinArch!.dartName}'; } return _processUtils.stream( [snapshotterPath, ...args], mapFunction: (String line) => kIgnoredWarnings.contains(line) ? null : line, ); } } class AOTSnapshotter { AOTSnapshotter({ this.reportTimings = false, required Logger logger, required FileSystem fileSystem, required Xcode xcode, required ProcessManager processManager, required Artifacts artifacts, }) : _logger = logger, _fileSystem = fileSystem, _xcode = xcode, _genSnapshot = GenSnapshot( artifacts: artifacts, processManager: processManager, logger: logger, ); final Logger _logger; final FileSystem _fileSystem; final Xcode _xcode; final GenSnapshot _genSnapshot; /// 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; /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script. Future build({ required TargetPlatform platform, required BuildMode buildMode, required String mainPath, required String outputPath, DarwinArch? darwinArch, String? sdkRoot, List extraGenSnapshotOptions = const [], String? splitDebugInfo, required bool dartObfuscation, bool quiet = false, }) async { assert(platform != TargetPlatform.ios || darwinArch != null); if (!_isValidAotPlatform(platform, buildMode)) { _logger.printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.'); return 1; } final Directory outputDir = _fileSystem.directory(outputPath); outputDir.createSync(recursive: true); final List genSnapshotArgs = [ '--deterministic', ]; final bool targetingApplePlatform = platform == TargetPlatform.ios || platform == TargetPlatform.darwin; _logger.printTrace('targetingApplePlatform = $targetingApplePlatform'); final bool extractAppleDebugSymbols = buildMode == BuildMode.profile || buildMode == BuildMode.release; _logger.printTrace('extractAppleDebugSymbols = $extractAppleDebugSymbols'); // We strip snapshot by default, but allow to suppress this behavior // by supplying --no-strip in extraGenSnapshotOptions. bool shouldStrip = true; if (extraGenSnapshotOptions.isNotEmpty) { _logger.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions'); for (final String option in extraGenSnapshotOptions) { if (option == '--no-strip') { shouldStrip = false; continue; } genSnapshotArgs.add(option); } } final String assembly = _fileSystem.path.join(outputDir.path, 'snapshot_assembly.S'); if (targetingApplePlatform) { genSnapshotArgs.addAll([ '--snapshot_kind=app-aot-assembly', '--assembly=$assembly', ]); } else { final String aotSharedLibrary = _fileSystem.path.join(outputDir.path, 'app.so'); genSnapshotArgs.addAll([ '--snapshot_kind=app-aot-elf', '--elf=$aotSharedLibrary', ]); } // When building for iOS and splitting out debug info, we want to strip // manually after the dSYM export, instead of in the `gen_snapshot`. final bool stripAfterBuild; if (targetingApplePlatform) { stripAfterBuild = shouldStrip; if (stripAfterBuild) { _logger.printTrace('Will strip AOT snapshot manually after build and dSYM generation.'); } } else { stripAfterBuild = false; if (shouldStrip) { genSnapshotArgs.add('--strip'); _logger.printTrace('Will strip AOT snapshot during build.'); } } if (platform == TargetPlatform.android_arm) { // Use softfp for Android armv7 devices. // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489 genSnapshotArgs.add('--no-sim-use-hardfp'); // Not supported by the Pixel in 32-bit mode. genSnapshotArgs.add('--no-use-integer-division'); } // The name of the debug file must contain additional information about // the architecture, since a single build command may produce // multiple debug files. final String archName = getNameForTargetPlatform(platform, darwinArch: darwinArch); final String debugFilename = 'app.$archName.symbols'; final bool shouldSplitDebugInfo = splitDebugInfo?.isNotEmpty ?? false; if (shouldSplitDebugInfo) { _fileSystem.directory(splitDebugInfo) .createSync(recursive: true); } // Debugging information. genSnapshotArgs.addAll([ if (shouldSplitDebugInfo) ...[ '--dwarf-stack-traces', '--resolve-dwarf-paths', '--save-debugging-info=${_fileSystem.path.join(splitDebugInfo!, debugFilename)}', ], if (dartObfuscation) '--obfuscate', ]); genSnapshotArgs.add(mainPath); final SnapshotType snapshotType = SnapshotType(platform, buildMode); final int genSnapshotExitCode = await _genSnapshot.run( snapshotType: snapshotType, additionalArgs: genSnapshotArgs, darwinArch: darwinArch, ); if (genSnapshotExitCode != 0) { _logger.printError('Dart snapshot generator failed with exit code $genSnapshotExitCode'); return genSnapshotExitCode; } // On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the // end-developer can link into their app. if (targetingApplePlatform) { return _buildFramework( appleArch: darwinArch!, isIOS: platform == TargetPlatform.ios, sdkRoot: sdkRoot, assemblyPath: assembly, outputPath: outputDir.path, quiet: quiet, stripAfterBuild: stripAfterBuild, extractAppleDebugSymbols: extractAppleDebugSymbols ); } else { return 0; } } /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly /// source at [assemblyPath]. Future _buildFramework({ required DarwinArch appleArch, required bool isIOS, String? sdkRoot, required String assemblyPath, required String outputPath, required bool quiet, required bool stripAfterBuild, required bool extractAppleDebugSymbols }) async { final String targetArch = appleArch.name; if (!quiet) { _logger.printStatus('Building App.framework for $targetArch...'); } final List commonBuildOptions = [ '-arch', targetArch, if (isIOS) // When the minimum version is updated, remember to update // template MinimumOSVersion. // https://github.com/flutter/flutter/pull/62902 '-miphoneos-version-min=11.0', if (sdkRoot != null) ...[ '-isysroot', sdkRoot, ], ]; final String assemblyO = _fileSystem.path.join(outputPath, 'snapshot_assembly.o'); final RunResult compileResult = await _xcode.cc([ ...commonBuildOptions, '-c', assemblyPath, '-o', assemblyO, ]); if (compileResult.exitCode != 0) { _logger.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}'); return compileResult.exitCode; } final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework'); _fileSystem.directory(frameworkDir).createSync(recursive: true); final String appLib = _fileSystem.path.join(frameworkDir, 'App'); final List linkArgs = [ ...commonBuildOptions, '-dynamiclib', '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks', '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks', '-fapplication-extension', '-install_name', '@rpath/App.framework/App', '-o', appLib, assemblyO, ]; final RunResult linkResult = await _xcode.clang(linkArgs); if (linkResult.exitCode != 0) { _logger.printError('Failed to link AOT snapshot. Linker terminated with exit code ${linkResult.exitCode}'); return linkResult.exitCode; } if (extractAppleDebugSymbols) { final RunResult dsymResult = await _xcode.dsymutil(['-o', '$frameworkDir.dSYM', appLib]); if (dsymResult.exitCode != 0) { _logger.printError('Failed to generate dSYM - dsymutil terminated with exit code ${dsymResult.exitCode}'); return dsymResult.exitCode; } if (stripAfterBuild) { // See https://www.unix.com/man-page/osx/1/strip/ for arguments final RunResult stripResult = await _xcode.strip(['-x', appLib, '-o', appLib]); if (stripResult.exitCode != 0) { _logger.printError('Failed to strip debugging symbols from the generated AOT snapshot - strip terminated with exit code ${stripResult.exitCode}'); return stripResult.exitCode; } } } else { assert(!stripAfterBuild); } return 0; } bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) { if (buildMode == BuildMode.debug) { return false; } return const [ TargetPlatform.android_arm, TargetPlatform.android_arm64, TargetPlatform.android_x64, TargetPlatform.ios, TargetPlatform.darwin, TargetPlatform.linux_x64, TargetPlatform.linux_arm64, TargetPlatform.windows_x64, ].contains(platform); } }