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

import '../../artifacts.dart';
6
import '../../base/build.dart';
7 8
import '../../base/file_system.dart';
import '../../base/io.dart';
9
import '../../base/process.dart';
10
import '../../build_info.dart';
11
import '../../globals.dart' as globals show xcode;
12
import '../build_system.dart';
13
import '../depfile.dart';
14
import '../exceptions.dart';
15
import 'assets.dart';
16
import 'common.dart';
17
import 'icon_tree_shaker.dart';
18

19
/// Copy the macOS framework to the correct copy dir by invoking 'rsync'.
20
///
21
/// This class is abstract to share logic between the three concrete
22 23
/// implementations. The shelling out is done to avoid complications with
/// preserving special files (e.g., symbolic links) in the framework structure.
24
///
25 26 27 28 29
/// The real implementations are:
///   * [DebugUnpackMacOS]
///   * [ProfileUnpackMacOS]
///   * [ReleaseUnpackMacOS]
abstract class UnpackMacOS extends Target {
30
  const UnpackMacOS();
31

32 33 34 35 36 37
  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
  ];

  @override
38
  List<Source> get outputs => const <Source>[
39
    Source.pattern('{OUTPUT_DIR}/FlutterMacOS.framework/Versions/A/FlutterMacOS'),
40
  ];
41 42 43

  @override
  List<Target> get dependencies => <Target>[];
44

45
  @override
46
  Future<void> build(Environment environment) async {
47 48
    final String? buildModeEnvironment = environment.defines[kBuildMode];
    if (buildModeEnvironment == null) {
49 50
      throw MissingDefineException(kBuildMode, 'unpack_macos');
    }
51
    final BuildMode buildMode = getBuildModeForName(buildModeEnvironment);
52
    final String basePath = environment.artifacts.getArtifactPath(Artifact.flutterMacOSFramework, mode: buildMode);
53 54 55 56 57 58 59 60 61 62

    final ProcessResult result = environment.processManager.runSync(<String>[
      'rsync',
      '-av',
      '--delete',
      '--filter',
      '- .DS_Store/',
      basePath,
      environment.outputDir.path,
    ]);
63 64 65
    if (result.exitCode != 0) {
      throw Exception(
        'Failed to copy framework (exit ${result.exitCode}:\n'
66
            '${result.stdout}\n---\n${result.stderr}',
67 68
      );
    }
69

70 71 72 73 74
    final File frameworkBinary = environment.outputDir
      .childDirectory('FlutterMacOS.framework')
      .childDirectory('Versions')
      .childDirectory('A')
      .childFile('FlutterMacOS');
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    final String frameworkBinaryPath = frameworkBinary.path;
    if (!frameworkBinary.existsSync()) {
      throw Exception('Binary $frameworkBinaryPath does not exist, cannot thin');
    }
    _thinFramework(environment, frameworkBinaryPath);
  }

  void _thinFramework(Environment environment, String frameworkBinaryPath) {
    final String archs = environment.defines[kDarwinArchs] ?? 'x86_64';
    final List<String> archList = archs.split(' ').toList();
    final ProcessResult infoResult = environment.processManager.runSync(<String>[
      'lipo',
      '-info',
      frameworkBinaryPath,
    ]);
    final String lipoInfo = infoResult.stdout as String;

    final ProcessResult verifyResult = environment.processManager.runSync(<String>[
      'lipo',
      frameworkBinaryPath,
      '-verify_arch',
      ...archList
    ]);

    if (verifyResult.exitCode != 0) {
      throw Exception('Binary $frameworkBinaryPath does not contain $archs. Running lipo -info:\n$lipoInfo');
    }

    // Skip thinning for non-fat executables.
    if (lipoInfo.startsWith('Non-fat file:')) {
      environment.logger.printTrace('Skipping lipo for non-fat file $frameworkBinaryPath');
      return;
    }

    // Thin in-place.
    final ProcessResult extractResult = environment.processManager.runSync(<String>[
      'lipo',
      '-output',
      frameworkBinaryPath,
      for (final String arch in archList)
        ...<String>[
          '-extract',
          arch,
        ],
      ...<String>[frameworkBinaryPath],
    ]);

    if (extractResult.exitCode != 0) {
      throw Exception('Failed to extract $archs for $frameworkBinaryPath.\n${extractResult.stderr}\nRunning lipo -info:\n$lipoInfo');
    }
125 126
  }
}
127

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
/// Unpack the release prebuilt engine framework.
class ReleaseUnpackMacOS extends UnpackMacOS {
  const ReleaseUnpackMacOS();

  @override
  String get name => 'release_unpack_macos';

  @override
  List<Source> get inputs => <Source>[
    ...super.inputs,
    const Source.artifact(Artifact.flutterMacOSFramework, mode: BuildMode.release),
  ];
}

/// Unpack the profile prebuilt engine framework.
class ProfileUnpackMacOS extends UnpackMacOS {
  const ProfileUnpackMacOS();

  @override
  String get name => 'profile_unpack_macos';

  @override
  List<Source> get inputs => <Source>[
    ...super.inputs,
    const Source.artifact(Artifact.flutterMacOSFramework, mode: BuildMode.profile),
  ];
}

/// Unpack the debug prebuilt engine framework.
class DebugUnpackMacOS extends UnpackMacOS {
  const DebugUnpackMacOS();

  @override
  String get name => 'debug_unpack_macos';

  @override
  List<Source> get inputs => <Source>[
    ...super.inputs,
    const Source.artifact(Artifact.flutterMacOSFramework, mode: BuildMode.debug),
  ];
}

170 171 172 173 174 175 176 177 178 179 180 181
/// Create an App.framework for debug macOS targets.
///
/// This framework needs to exist for the Xcode project to link/bundle,
/// but it isn't actually executed. To generate something valid, we compile a trivial
/// constant.
class DebugMacOSFramework extends Target {
  const DebugMacOSFramework();

  @override
  String get name => 'debug_macos_framework';

  @override
182
  Future<void> build(Environment environment) async {
183
    final File outputFile = environment.fileSystem.file(environment.fileSystem.path.join(
184
        environment.buildDir.path, 'App.framework', 'App'));
185 186 187

    final Iterable<DarwinArch> darwinArchs = environment.defines[kDarwinArchs]
      ?.split(' ')
188
      .map(getDarwinArchForName)
189 190 191 192 193
      ?? <DarwinArch>[DarwinArch.x86_64];

    final Iterable<String> darwinArchArguments =
        darwinArchs.expand((DarwinArch arch) => <String>['-arch', getNameForDarwinArch(arch)]);

194 195 196 197 198
    outputFile.createSync(recursive: true);
    final File debugApp = environment.buildDir.childFile('debug_app.cc')
        ..writeAsStringSync(r'''
static const int Moo = 88;
''');
199
    final RunResult result = await globals.xcode!.clang(<String>[
200 201 202
      '-x',
      'c',
      debugApp.path,
203
      ...darwinArchArguments,
204 205 206 207
      '-dynamiclib',
      '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks',
      '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks',
      '-install_name', '@rpath/App.framework/App',
208
      '-o', outputFile.path,
209 210 211 212 213
    ]);
    if (result.exitCode != 0) {
      throw Exception('Failed to compile debug App.framework');
    }
  }
214 215

  @override
216
  List<Target> get dependencies => const <Target>[];
217 218 219

  @override
  List<Source> get inputs => const <Source>[
220
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
221 222 223 224
  ];

  @override
  List<Source> get outputs => const <Source>[
225
    Source.pattern('{BUILD_DIR}/App.framework/App'),
226
  ];
227 228
}

229 230 231 232 233 234 235
class CompileMacOSFramework extends Target {
  const CompileMacOSFramework();

  @override
  String get name => 'compile_macos_framework';

  @override
236
  Future<void> build(Environment environment) async {
237 238
    final String? buildModeEnvironment = environment.defines[kBuildMode];
    if (buildModeEnvironment == null) {
239 240
      throw MissingDefineException(kBuildMode, 'compile_macos_framework');
    }
241 242 243 244 245
    final String? targetPlatformEnvironment = environment.defines[kTargetPlatform];
    if (targetPlatformEnvironment == null) {
      throw MissingDefineException(kTargetPlatform, 'kernel_snapshot');
    }
    final BuildMode buildMode = getBuildModeForName(buildModeEnvironment);
246 247 248
    if (buildMode == BuildMode.debug) {
      throw Exception('precompiled macOS framework only supported in release/profile builds.');
    }
249
    final String buildOutputPath = environment.buildDir.path;
250 251
    final String? codeSizeDirectory = environment.defines[kCodeSizeDirectory];
    final String? splitDebugInfo = environment.defines[kSplitDebugInfo];
252
    final bool dartObfuscation = environment.defines[kDartObfuscation] == 'true';
253
    final List<String> extraGenSnapshotOptions = decodeCommaSeparated(environment.defines, kExtraGenSnapshotOptions);
254
    final TargetPlatform targetPlatform = getTargetPlatformForName(targetPlatformEnvironment);
255 256
    final List<DarwinArch> darwinArchs = environment.defines[kDarwinArchs]
      ?.split(' ')
257 258
      .map(getDarwinArchForName)
      .toList()
259 260 261
      ?? <DarwinArch>[DarwinArch.x86_64];
    if (targetPlatform != TargetPlatform.darwin) {
      throw Exception('compile_macos_framework is only supported for darwin TargetPlatform.');
262 263
    }

264
    final AOTSnapshotter snapshotter = AOTSnapshotter(
265 266
      fileSystem: environment.fileSystem,
      logger: environment.logger,
267
      xcode: globals.xcode!,
268 269
      artifacts: environment.artifacts,
      processManager: environment.processManager
270
    );
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314

    final List<Future<int>> pending = <Future<int>>[];
    for (final DarwinArch darwinArch in darwinArchs) {
      if (codeSizeDirectory != null) {
        final File codeSizeFile = environment.fileSystem
          .directory(codeSizeDirectory)
          .childFile('snapshot.${getNameForDarwinArch(darwinArch)}.json');
        final File precompilerTraceFile = environment.fileSystem
          .directory(codeSizeDirectory)
          .childFile('trace.${getNameForDarwinArch(darwinArch)}.json');
        extraGenSnapshotOptions.add('--write-v8-snapshot-profile-to=${codeSizeFile.path}');
        extraGenSnapshotOptions.add('--trace-precompiler-to=${precompilerTraceFile.path}');
      }

      pending.add(snapshotter.build(
        bitcode: false,
        buildMode: buildMode,
        mainPath: environment.buildDir.childFile('app.dill').path,
        outputPath: environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(darwinArch)),
        platform: TargetPlatform.darwin,
        darwinArch: darwinArch,
        splitDebugInfo: splitDebugInfo,
        dartObfuscation: dartObfuscation,
        extraGenSnapshotOptions: extraGenSnapshotOptions,
      ));
    }

    final List<int> results = await Future.wait(pending);
    if (results.any((int result) => result != 0)) {
      throw Exception('AOT snapshotter exited with code ${results.join()}');
    }

    final String resultPath = environment.fileSystem.path.join(environment.buildDir.path, 'App.framework', 'App');
    environment.fileSystem.directory(resultPath).parent.createSync(recursive: true);
    final ProcessResult result = await environment.processManager.run(<String>[
      'lipo',
      ...darwinArchs.map((DarwinArch iosArch) =>
          environment.fileSystem.path.join(buildOutputPath, getNameForDarwinArch(iosArch), 'App.framework', 'App')),
      '-create',
      '-output',
      resultPath,
    ]);
    if (result.exitCode != 0) {
      throw Exception('lipo exited with code ${result.exitCode}.\n${result.stderr}');
315 316 317 318 319 320 321 322 323 324 325 326
    }
  }

  @override
  List<Target> get dependencies => const <Target>[
    KernelSnapshot(),
  ];

  @override
  List<Source> get inputs => const <Source>[
    Source.pattern('{BUILD_DIR}/app.dill'),
    Source.pattern('{FLUTTER_ROOT}/packages/flutter_tools/lib/src/build_system/targets/macos.dart'),
327
    Source.artifact(Artifact.genSnapshot, mode: BuildMode.release, platform: TargetPlatform.darwin),
328 329 330 331 332 333 334 335 336 337 338
  ];

  @override
  List<Source> get outputs => const <Source>[
    Source.pattern('{BUILD_DIR}/App.framework/App'),
  ];
}

/// Bundle the flutter assets into the App.framework.
///
/// In debug mode, also include the app.dill and precompiled runtimes.
339 340 341
///
/// See https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html
/// for more information on Framework structure.
342 343
abstract class MacOSBundleFlutterAssets extends Target {
  const MacOSBundleFlutterAssets();
344 345

  @override
346
  List<Source> get inputs => const <Source>[
347
    Source.pattern('{BUILD_DIR}/App.framework/App'),
348
    ...IconTreeShaker.inputs,
349 350 351 352
  ];

  @override
  List<Source> get outputs => const <Source>[
353 354
    Source.pattern('{OUTPUT_DIR}/App.framework/Versions/A/App'),
    Source.pattern('{OUTPUT_DIR}/App.framework/Versions/A/Resources/Info.plist'),
355 356 357 358 359
  ];

  @override
  List<String> get depfiles => const <String>[
    'flutter_assets.d',
360
  ];
361 362

  @override
363
  Future<void> build(Environment environment) async {
364 365
    final String? buildModeEnvironment = environment.defines[kBuildMode];
    if (buildModeEnvironment == null) {
366 367
      throw MissingDefineException(kBuildMode, 'compile_macos_framework');
    }
368
    final BuildMode buildMode = getBuildModeForName(buildModeEnvironment);
369 370
    final Directory frameworkRootDirectory = environment
        .outputDir
371 372 373 374 375 376 377 378 379 380 381 382
        .childDirectory('App.framework');
    final Directory outputDirectory = frameworkRootDirectory
        .childDirectory('Versions')
        .childDirectory('A')
        ..createSync(recursive: true);

    // Copy App into framework directory.
    environment.buildDir
      .childDirectory('App.framework')
      .childFile('App')
      .copySync(outputDirectory.childFile('App').path);

383
    // Copy assets into asset directory.
384 385 386 387
    final Directory assetDirectory = outputDirectory
      .childDirectory('Resources')
      .childDirectory('flutter_assets');
    assetDirectory.createSync(recursive: true);
388

389 390 391
    final Depfile assetDepfile = await copyAssets(
      environment,
      assetDirectory,
392
      targetPlatform: TargetPlatform.darwin,
393
    );
394
    final DepfileService depfileService = DepfileService(
395 396
      fileSystem: environment.fileSystem,
      logger: environment.logger,
397 398
    );
    depfileService.writeToFile(
399
      assetDepfile,
400 401
      environment.buildDir.childFile('flutter_assets.d'),
    );
402

403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
    // Copy Info.plist template.
    assetDirectory.parent.childFile('Info.plist')
      ..createSync()
      ..writeAsStringSync(r'''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleExecutable</key>
	<string>App</string>
	<key>CFBundleIdentifier</key>
	<string>io.flutter.flutter.app</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>App</string>
	<key>CFBundlePackageType</key>
	<string>FMWK</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0</string>
	<key>CFBundleVersion</key>
	<string>1.0</string>
</dict>
</plist>

''');
431 432 433 434 435
    if (buildMode == BuildMode.debug) {
      // Copy dill file.
      try {
        final File sourceFile = environment.buildDir.childFile('app.dill');
        sourceFile.copySync(assetDirectory.childFile('kernel_blob.bin').path);
436
      } on Exception catch (err) {
437 438 439 440
        throw Exception('Failed to copy app.dill: $err');
      }
      // Copy precompiled runtimes.
      try {
441
        final String vmSnapshotData = environment.artifacts.getArtifactPath(Artifact.vmSnapshotData,
442
            platform: TargetPlatform.darwin, mode: BuildMode.debug);
443
        final String isolateSnapshotData = environment.artifacts.getArtifactPath(Artifact.isolateSnapshotData,
444
            platform: TargetPlatform.darwin, mode: BuildMode.debug);
445
        environment.fileSystem.file(vmSnapshotData).copySync(
446
            assetDirectory.childFile('vm_snapshot_data').path);
447
        environment.fileSystem.file(isolateSnapshotData).copySync(
448
            assetDirectory.childFile('isolate_snapshot_data').path);
449
      } on Exception catch (err) {
450 451
        throw Exception('Failed to copy precompiled runtimes: $err');
      }
452
    }
453 454 455
    // Create symlink to current version. These must be relative, from the
    // framework root for Resources/App and from the versions root for
    // Current.
456 457 458 459
    try {
      final Link currentVersion = outputDirectory.parent
          .childLink('Current');
      if (!currentVersion.existsSync()) {
460
        final String linkPath = environment.fileSystem.path.relative(outputDirectory.path,
461
            from: outputDirectory.parent.path);
462
        currentVersion.createSync(linkPath);
463 464 465 466 467
      }
      // Create symlink to current resources.
      final Link currentResources = frameworkRootDirectory
          .childLink('Resources');
      if (!currentResources.existsSync()) {
468
        final String linkPath = environment.fileSystem.path.relative(environment.fileSystem.path.join(currentVersion.path, 'Resources'),
469 470
            from: frameworkRootDirectory.path);
        currentResources.createSync(linkPath);
471 472 473 474 475
      }
      // Create symlink to current binary.
      final Link currentFramework = frameworkRootDirectory
          .childLink('App');
      if (!currentFramework.existsSync()) {
476
        final String linkPath = environment.fileSystem.path.relative(environment.fileSystem.path.join(currentVersion.path, 'App'),
477 478
            from: frameworkRootDirectory.path);
        currentFramework.createSync(linkPath);
479 480 481
      }
    } on FileSystemException {
      throw Exception('Failed to create symlinks for framework. try removing '
482
        'the "${environment.outputDir.path}" directory and rerunning');
483
    }
484
  }
485 486 487 488 489 490 491 492
}

/// Bundle the debug flutter assets into the App.framework.
class DebugMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets {
  const DebugMacOSBundleFlutterAssets();

  @override
  String get name => 'debug_macos_bundle_flutter_assets';
493 494 495 496

  @override
  List<Target> get dependencies => const <Target>[
    KernelSnapshot(),
497
    DebugMacOSFramework(),
498
    DebugUnpackMacOS(),
499 500 501
  ];

  @override
502 503 504
  List<Source> get inputs => <Source>[
    ...super.inputs,
    const Source.pattern('{BUILD_DIR}/app.dill'),
505 506
    const Source.artifact(Artifact.isolateSnapshotData, platform: TargetPlatform.darwin, mode: BuildMode.debug),
    const Source.artifact(Artifact.vmSnapshotData, platform: TargetPlatform.darwin, mode: BuildMode.debug),
507 508 509
  ];

  @override
510 511
  List<Source> get outputs => <Source>[
    ...super.outputs,
512 513 514
    const Source.pattern('{OUTPUT_DIR}/App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin'),
    const Source.pattern('{OUTPUT_DIR}/App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data'),
    const Source.pattern('{OUTPUT_DIR}/App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data'),
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
  ];
}

/// Bundle the profile flutter assets into the App.framework.
class ProfileMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets {
  const ProfileMacOSBundleFlutterAssets();

  @override
  String get name => 'profile_macos_bundle_flutter_assets';

  @override
  List<Target> get dependencies => const <Target>[
    CompileMacOSFramework(),
    ProfileUnpackMacOS(),
  ];
}


/// Bundle the release flutter assets into the App.framework.
class ReleaseMacOSBundleFlutterAssets extends MacOSBundleFlutterAssets {
  const ReleaseMacOSBundleFlutterAssets();

  @override
  String get name => 'release_macos_bundle_flutter_assets';

  @override
  List<Target> get dependencies => const <Target>[
    CompileMacOSFramework(),
    ReleaseUnpackMacOS(),
544 545
  ];
}