build.dart 11.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:process/process.dart';
6

7
import '../artifacts.dart';
8
import '../build_info.dart';
9
import '../macos/xcode.dart';
10

11
import 'file_system.dart';
12
import 'logger.dart';
13 14 15 16
import 'process.dart';

/// A snapshot build configuration.
class SnapshotType {
17 18
  SnapshotType(this.platform, this.mode)
    : assert(mode != null);
19

20
  final TargetPlatform? platform;
21
  final BuildMode mode;
22 23 24

  @override
  String toString() => '$platform $mode';
25 26 27 28
}

/// Interface to the gen_snapshot command-line tool.
class GenSnapshot {
29
  GenSnapshot({
30 31 32
    required Artifacts artifacts,
    required ProcessManager processManager,
    required Logger logger,
33 34 35 36 37 38 39 40
  }) : _artifacts = artifacts,
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);

  final Artifacts _artifacts;
  final ProcessUtils _processUtils;

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

44 45 46 47 48 49 50 51 52 53
  /// Ignored warning messages from gen_snapshot.
  static const Set<String> kIgnoredWarnings = <String>{
    // --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.',
  };
54

55
  Future<int> run({
56 57
    required SnapshotType snapshotType,
    DarwinArch? darwinArch,
58
    Iterable<String> additionalArgs = const <String>[],
59
  }) {
60
    assert(darwinArch != DarwinArch.armv7);
61
    assert(snapshotType.platform != TargetPlatform.ios || darwinArch != null);
62
    final List<String> args = <String>[
63 64
      ...additionalArgs,
    ];
65

66
    String snapshotterPath = getSnapshotterPath(snapshotType);
67

68 69 70 71 72 73
    // 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 += '_${getDartNameForDarwinArch(darwinArch!)}';
74 75
    }

76
    return _processUtils.stream(
77
      <String>[snapshotterPath, ...args],
78
      mapFunction: (String line) =>  kIgnoredWarnings.contains(line) ? null : line,
79
    );
80 81
  }
}
82

83
class AOTSnapshotter {
84 85
  AOTSnapshotter({
    this.reportTimings = false,
86 87 88 89 90
    required Logger logger,
    required FileSystem fileSystem,
    required Xcode xcode,
    required ProcessManager processManager,
    required Artifacts artifacts,
91 92 93 94 95 96 97 98 99 100 101 102 103
  }) : _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;
104 105 106 107 108 109

  /// 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;

110
  /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script.
111
  Future<int> build({
112 113 114 115 116 117
    required TargetPlatform platform,
    required BuildMode buildMode,
    required String mainPath,
    required String outputPath,
    DarwinArch? darwinArch,
    String? sdkRoot,
118
    List<String> extraGenSnapshotOptions = const <String>[],
119 120
    String? splitDebugInfo,
    required bool dartObfuscation,
xster's avatar
xster committed
121
    bool quiet = false,
122
  }) async {
123
    assert(platform != TargetPlatform.ios || darwinArch != null);
124

125
    if (!_isValidAotPlatform(platform, buildMode)) {
126
      _logger.printError('${getNameForTargetPlatform(platform)} does not support AOT compilation.');
127
      return 1;
128 129
    }

130
    final Directory outputDir = _fileSystem.directory(outputPath);
131 132
    outputDir.createSync(recursive: true);

133
    final List<String> genSnapshotArgs = <String>[
134
      '--deterministic',
135
    ];
136

137 138 139 140 141 142 143 144
    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');

145 146 147
    // We strip snapshot by default, but allow to suppress this behavior
    // by supplying --no-strip in extraGenSnapshotOptions.
    bool shouldStrip = true;
148
    if (extraGenSnapshotOptions != null && extraGenSnapshotOptions.isNotEmpty) {
149
      _logger.printTrace('Extra gen_snapshot options: $extraGenSnapshotOptions');
150 151 152 153 154 155 156
      for (final String option in extraGenSnapshotOptions) {
        if (option == '--no-strip') {
          shouldStrip = false;
          continue;
        }
        genSnapshotArgs.add(option);
      }
157 158
    }

159
    final String assembly = _fileSystem.path.join(outputDir.path, 'snapshot_assembly.S');
160
    if (targetingApplePlatform) {
161 162 163 164
      genSnapshotArgs.addAll(<String>[
        '--snapshot_kind=app-aot-assembly',
        '--assembly=$assembly',
      ]);
165
    } else {
166 167 168 169 170
      final String aotSharedLibrary = _fileSystem.path.join(outputDir.path, 'app.so');
      genSnapshotArgs.addAll(<String>[
        '--snapshot_kind=app-aot-elf',
        '--elf=$aotSharedLibrary',
      ]);
171 172
    }

173 174 175 176 177 178
    // When buiding 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) {
179
        _logger.printTrace('Will strip AOT snapshot manually after build and dSYM generation.');
180 181 182 183 184 185 186
      }
    } else {
      stripAfterBuild = false;
      if (shouldStrip) {
        genSnapshotArgs.add('--strip');
        _logger.printTrace('Will strip AOT snapshot during build.');
      }
187 188
    }

189
    if (platform == TargetPlatform.android_arm) {
190
      // Use softfp for Android armv7 devices.
191
      // TODO(cbracken): eliminate this when we fix https://github.com/flutter/flutter/issues/17489
192 193
      genSnapshotArgs.add('--no-sim-use-hardfp');

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

198
    // The name of the debug file must contain additional information about
199 200 201 202
    // 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';
203 204
    final bool shouldSplitDebugInfo = splitDebugInfo?.isNotEmpty ?? false;
    if (shouldSplitDebugInfo) {
205
      _fileSystem.directory(splitDebugInfo)
206 207 208
        .createSync(recursive: true);
    }

209 210 211
    // Optimization arguments.
    genSnapshotArgs.addAll(<String>[
      // Faster async/await
212
      if (shouldSplitDebugInfo) ...<String>[
213
        '--dwarf-stack-traces',
214
        '--save-debugging-info=${_fileSystem.path.join(splitDebugInfo!, debugFilename)}',
215 216 217
      ],
      if (dartObfuscation)
        '--obfuscate',
218 219
    ]);

220
    genSnapshotArgs.add(mainPath);
221

222
    final SnapshotType snapshotType = SnapshotType(platform, buildMode);
223
    final int genSnapshotExitCode = await _genSnapshot.run(
224
      snapshotType: snapshotType,
225
      additionalArgs: genSnapshotArgs,
226
      darwinArch: darwinArch,
227
    );
228
    if (genSnapshotExitCode != 0) {
229
      _logger.printError('Dart snapshot generator failed with exit code $genSnapshotExitCode');
230
      return genSnapshotExitCode;
231 232
    }

233
    // On iOS and macOS, we use Xcode to compile the snapshot into a dynamic library that the
234
    // end-developer can link into their app.
235 236
    if (targetingApplePlatform) {
      return _buildFramework(
237
        appleArch: darwinArch!,
238
        isIOS: platform == TargetPlatform.ios,
239
        sdkRoot: sdkRoot,
240
        assemblyPath: assembly,
241
        outputPath: outputDir.path,
xster's avatar
xster committed
242
        quiet: quiet,
243 244
        stripAfterBuild: stripAfterBuild,
        extractAppleDebugSymbols: extractAppleDebugSymbols
245
      );
246 247
    } else {
      return 0;
248 249 250
    }
  }

251
  /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly
252
  /// source at [assemblyPath].
253
  Future<int> _buildFramework({
254 255 256 257 258
    required DarwinArch appleArch,
    required bool isIOS,
    String? sdkRoot,
    required String assemblyPath,
    required String outputPath,
259 260 261
    required bool quiet,
    required bool stripAfterBuild,
    required bool extractAppleDebugSymbols
262
  }) async {
263
    final String targetArch = getNameForDarwinArch(appleArch);
xster's avatar
xster committed
264
    if (!quiet) {
265
      _logger.printStatus('Building App.framework for $targetArch...');
xster's avatar
xster committed
266
    }
267

268
    final List<String> commonBuildOptions = <String>[
269 270
      '-arch',
      targetArch,
271
      if (isIOS)
272
        // When the minimum version is updated, remember to update
273
        // template MinimumOSVersion.
274
        // https://github.com/flutter/flutter/pull/62902
275
        '-miphoneos-version-min=11.0',
276 277 278 279
      if (sdkRoot != null) ...<String>[
        '-isysroot',
        sdkRoot,
      ],
280
    ];
281

282
    final String assemblyO = _fileSystem.path.join(outputPath, 'snapshot_assembly.o');
283

284
    final RunResult compileResult = await _xcode.cc(<String>[
285
      ...commonBuildOptions,
286 287 288 289 290
      '-c',
      assemblyPath,
      '-o',
      assemblyO,
    ]);
291
    if (compileResult.exitCode != 0) {
292
      _logger.printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}');
293
      return compileResult.exitCode;
294
    }
295

296 297 298
    final String frameworkDir = _fileSystem.path.join(outputPath, 'App.framework');
    _fileSystem.directory(frameworkDir).createSync(recursive: true);
    final String appLib = _fileSystem.path.join(frameworkDir, 'App');
299 300 301 302 303 304 305 306 307
    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',
      '-o', appLib,
      assemblyO,
    ];
308

309
    final RunResult linkResult = await _xcode.clang(linkArgs);
310
    if (linkResult.exitCode != 0) {
311 312 313 314 315 316 317 318 319 320 321 322 323
      _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(<String>['-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
324
        final RunResult stripResult = await _xcode.strip(<String>['-x', appLib, '-o', appLib]);
325 326 327 328 329 330 331
        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 == false);
332
    }
333 334

    return 0;
335 336
  }

337
  bool _isValidAotPlatform(TargetPlatform platform, BuildMode buildMode) {
338
    if (buildMode == BuildMode.debug) {
339
      return false;
340
    }
341 342 343
    return const <TargetPlatform>[
      TargetPlatform.android_arm,
      TargetPlatform.android_arm64,
344
      TargetPlatform.android_x64,
345
      TargetPlatform.ios,
346
      TargetPlatform.darwin,
347
      TargetPlatform.linux_x64,
348
      TargetPlatform.linux_arm64,
349
      TargetPlatform.windows_x64,
350 351
    ].contains(platform);
  }
352
}