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

import 'dart:async';

import 'package:file/file.dart';
8
import 'package:meta/meta.dart';
9
import 'package:platform/platform.dart';
xster's avatar
xster committed
10 11 12 13 14 15 16 17 18 19 20

import '../aot.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../build_system/targets/ios.dart';
import '../bundle.dart';
21
import '../cache.dart';
22
import '../globals.dart' as globals;
xster's avatar
xster committed
23 24 25 26 27
import '../macos/cocoapod_utils.dart';
import '../macos/xcode.dart';
import '../plugins.dart';
import '../project.dart';
import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
28
import '../version.dart';
xster's avatar
xster committed
29 30 31 32 33 34 35
import 'build.dart';

/// Produces a .framework for integration into a host iOS app. The .framework
/// contains the Flutter engine and framework code as well as plugins. It can
/// be integrated into plain Xcode projects without using or other package
/// managers.
class BuildIOSFrameworkCommand extends BuildSubCommand {
36 37 38 39
  BuildIOSFrameworkCommand({
    FlutterVersion flutterVersion, // Instantiating FlutterVersion kicks off networking, so delay until it's needed, but allow test injection.
    @required AotBuilder aotBuilder,
    @required BundleBuilder bundleBuilder,
40 41
    Cache cache,
    Platform platform
42 43 44
  }) : _flutterVersion = flutterVersion,
       _aotBuilder = aotBuilder,
       _bundleBuilder = bundleBuilder,
45 46
       _injectedCache = cache,
       _injectedPlatform = platform {
47
    addTreeShakeIconsFlag();
xster's avatar
xster committed
48 49 50
    usesTargetOption();
    usesFlavorOption();
    usesPubOption();
51
    usesDartDefineOption();
52 53
    addSplitDebugInfoOption();
    addDartObfuscationOption();
54
    usesExtraFrontendOptions();
xster's avatar
xster committed
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
    argParser
      ..addFlag('debug',
        negatable: true,
        defaultsTo: true,
        help: 'Whether to produce a framework for the debug build configuration. '
              'By default, all build configurations are built.'
      )
      ..addFlag('profile',
        negatable: true,
        defaultsTo: true,
        help: 'Whether to produce a framework for the profile build configuration. '
              'By default, all build configurations are built.'
      )
      ..addFlag('release',
        negatable: true,
        defaultsTo: true,
        help: 'Whether to produce a framework for the release build configuration. '
              'By default, all build configurations are built.'
      )
      ..addFlag('universal',
        help: 'Produce universal frameworks that include all valid architectures. '
              'This is true by default.',
        defaultsTo: true,
        negatable: true
      )
      ..addFlag('xcframework',
        help: 'Produce xcframeworks that include all valid architectures (Xcode 11 or later).',
      )
83 84 85
      ..addFlag('cocoapods',
        help: 'Produce a Flutter.podspec instead of an engine Flutter.framework (recomended if host app uses CocoaPods).',
      )
xster's avatar
xster committed
86 87 88 89
      ..addOption('output',
        abbr: 'o',
        valueHelp: 'path/to/directory/',
        help: 'Location to write the frameworks.',
90 91 92 93 94
      )
      ..addFlag('force',
        abbr: 'f',
        help: 'Force Flutter.podspec creation on the master channel. For testing only.',
        hide: true
xster's avatar
xster committed
95 96 97
      );
  }

98 99
  final AotBuilder _aotBuilder;
  final BundleBuilder _bundleBuilder;
100 101 102 103 104 105

  Cache get _cache => _injectedCache ?? globals.cache;
  final Cache _injectedCache;

  Platform get _platform => _injectedPlatform ?? globals.platform;
  final Platform _injectedPlatform;
106 107

  FlutterVersion _flutterVersion;
xster's avatar
xster committed
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

  @override
  final String name = 'ios-framework';

  @override
  final String description = 'Produces a .framework directory for a Flutter module '
      'and its plugins for integration into existing, plain Xcode projects.\n'
      'This can only be run on macOS hosts.';

  @override
  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
    DevelopmentArtifact.iOS,
  };

  FlutterProject _project;

124 125
  List<BuildInfo> get buildInfos {
    final List<BuildInfo> buildModes = <BuildInfo>[];
xster's avatar
xster committed
126

127
    if (boolArg('debug')) {
128
      buildModes.add(BuildInfo.debug);
xster's avatar
xster committed
129
    }
130
    if (boolArg('profile')) {
131
      buildModes.add(BuildInfo.profile);
xster's avatar
xster committed
132
    }
133
    if (boolArg('release')) {
134
      buildModes.add(BuildInfo.release);
xster's avatar
xster committed
135 136 137 138 139 140 141 142 143 144 145 146 147
    }

    return buildModes;
  }

  @override
  Future<void> validateCommand() async {
    await super.validateCommand();
    _project = FlutterProject.current();
    if (!_project.isModule) {
      throwToolExit('Building frameworks for iOS is only supported from a module.');
    }

148
    if (!_platform.isMacOS) {
xster's avatar
xster committed
149 150 151
      throwToolExit('Building frameworks for iOS is only supported on the Mac.');
    }

152
    if (!boolArg('universal') && !boolArg('xcframework')) {
xster's avatar
xster committed
153 154
      throwToolExit('--universal or --xcframework is required.');
    }
155
    if (boolArg('xcframework') && globals.xcode.majorVersion < 11) {
xster's avatar
xster committed
156 157
      throwToolExit('--xcframework requires Xcode 11.');
    }
158
    if (buildInfos.isEmpty) {
xster's avatar
xster committed
159 160 161 162 163 164
      throwToolExit('At least one of "--debug" or "--profile", or "--release" is required.');
    }
  }

  @override
  Future<FlutterCommandResult> runCommand() async {
165 166
    Cache.releaseLockEarly();

167
    final String outputArgument = stringArg('output')
168
        ?? globals.fs.path.join(globals.fs.currentDirectory.path, 'build', 'ios', 'framework');
xster's avatar
xster committed
169 170 171 172 173

    if (outputArgument.isEmpty) {
      throwToolExit('--output is required.');
    }

174 175
    if (!_project.ios.existsSync()) {
      throwToolExit('Module does not support iOS');
xster's avatar
xster committed
176 177
    }

178
    final Directory outputDirectory = globals.fs.directory(globals.fs.path.absolute(globals.fs.path.normalize(outputArgument)));
xster's avatar
xster committed
179

180
    final String productBundleIdentifier = await _project.ios.productBundleIdentifier;
181 182 183
    for (final BuildInfo buildInfo in buildInfos) {
      globals.printStatus('Building frameworks for $productBundleIdentifier in ${getNameForBuildMode(buildInfo.mode)} mode...');
      final String xcodeBuildConfiguration = toTitleCase(getNameForBuildMode(buildInfo.mode));
xster's avatar
xster committed
184
      final Directory modeDirectory = outputDirectory.childDirectory(xcodeBuildConfiguration);
185 186 187 188

      if (modeDirectory.existsSync()) {
        modeDirectory.deleteSync(recursive: true);
      }
xster's avatar
xster committed
189 190 191
      final Directory iPhoneBuildOutput = modeDirectory.childDirectory('iphoneos');
      final Directory simulatorBuildOutput = modeDirectory.childDirectory('iphonesimulator');

192 193
      if (boolArg('cocoapods')) {
        // FlutterVersion.instance kicks off git processing which can sometimes fail, so don't try it until needed.
194
        _flutterVersion ??= globals.flutterVersion;
195
        produceFlutterPodspec(buildInfo.mode, modeDirectory, force: boolArg('force'));
196 197
      } else {
        // Copy Flutter.framework.
198
        await _produceFlutterFramework(buildInfo, modeDirectory);
199
      }
xster's avatar
xster committed
200 201

      // Build aot, create module.framework and copy.
202
      await _produceAppFramework(buildInfo, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory);
xster's avatar
xster committed
203 204

      // Build and copy plugins.
205
      await processPodsIfNeeded(_project.ios, getIosBuildDirectory(), buildInfo.mode);
xster's avatar
xster committed
206
      if (hasPlugins(_project)) {
207
        await _producePlugins(buildInfo.mode, xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory, outputDirectory);
xster's avatar
xster committed
208 209
      }

210 211
      final Status status = globals.logger.startProgress(
        ' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}', timeout: timeoutConfiguration.slowOperation);
212 213 214 215 216 217 218 219 220 221 222
      try {
        // Delete the intermediaries since they would have been copied into our
        // output frameworks.
        if (iPhoneBuildOutput.existsSync()) {
          iPhoneBuildOutput.deleteSync(recursive: true);
        }
        if (simulatorBuildOutput.existsSync()) {
          simulatorBuildOutput.deleteSync(recursive: true);
        }
      } finally {
        status.stop();
xster's avatar
xster committed
223 224 225
      }
    }

226
    globals.printStatus('Frameworks written to ${outputDirectory.path}.');
227
    return FlutterCommandResult.success();
xster's avatar
xster committed
228 229
  }

230 231 232
  /// Create podspec that will download and unzip remote engine assets so host apps can leverage CocoaPods
  /// vendored framework caching.
  @visibleForTesting
233
  void produceFlutterPodspec(BuildMode mode, Directory modeDirectory, { bool force = false }) {
234
    final Status status = globals.logger.startProgress(' ├─Creating Flutter.podspec...', timeout: timeoutConfiguration.fastOperation);
235
    try {
236
      final GitTagVersion gitTagVersion = _flutterVersion.gitTagVersion;
237
      if (!force && (gitTagVersion.x == null || gitTagVersion.y == null || gitTagVersion.z == null || gitTagVersion.commits != 0)) {
238
        throwToolExit(
239
            '--cocoapods is only supported on the dev, beta, or stable channels. Detected version is ${_flutterVersion.frameworkVersion}');
240 241 242 243 244 245 246 247
      }

      // Podspecs use semantic versioning, which don't support hotfixes.
      // Fake out a semantic version with major.minor.(patch * 100) + hotfix.
      // A real increasing version is required to prompt CocoaPods to fetch
      // new artifacts when the source URL changes.
      final int minorHotfixVersion = gitTagVersion.z * 100 + (gitTagVersion.hotfix ?? 0);

248
      final File license = _cache.getLicenseFile();
249 250 251 252 253 254 255 256 257
      if (!license.existsSync()) {
        throwToolExit('Could not find license at ${license.path}');
      }
      final String licenseSource = license.readAsStringSync();
      final String artifactsMode = mode == BuildMode.debug ? 'ios' : 'ios-${mode.name}';

      final String podspecContents = '''
Pod::Spec.new do |s|
  s.name                  = 'Flutter'
258
  s.version               = '${gitTagVersion.x}.${gitTagVersion.y}.$minorHotfixVersion' # ${_flutterVersion.frameworkVersion}
259 260 261 262 263 264 265 266 267 268 269 270
  s.summary               = 'Flutter Engine Framework'
  s.description           = <<-DESC
Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.
This pod vends the iOS Flutter engine framework. It is compatible with application frameworks created with this version of the engine and tools.
The pod version matches Flutter version major.minor.(patch * 100) + hotfix.
DESC
  s.homepage              = 'https://flutter.dev'
  s.license               = { :type => 'MIT', :text => <<-LICENSE
$licenseSource
LICENSE
  }
  s.author                = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
271
  s.source                = { :http => '${_cache.storageBaseUrl}/flutter_infra/flutter/${_cache.engineRevision}/$artifactsMode/artifacts.zip' }
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
  s.documentation_url     = 'https://flutter.dev/docs'
  s.platform              = :ios, '8.0'
  s.vendored_frameworks   = 'Flutter.framework'
  s.prepare_command       = <<-CMD
unzip Flutter.framework -d Flutter.framework
CMD
end
''';

      final File podspec = modeDirectory.childFile('Flutter.podspec')..createSync(recursive: true);
      podspec.writeAsStringSync(podspecContents);
    } finally {
      status.stop();
    }
  }

288
  Future<void> _produceFlutterFramework(
289
    BuildInfo buildInfo,
290 291 292 293 294 295 296 297 298
    Directory modeDirectory,
  ) async {
    final Status status = globals.logger.startProgress(
      ' ├─Populating Flutter.framework...',
      timeout: timeoutConfiguration.slowOperation,
    );
    final String engineCacheFlutterFrameworkDirectory = globals.artifacts.getArtifactPath(
      Artifact.flutterFramework,
      platform: TargetPlatform.ios,
299
      mode: buildInfo.mode,
300 301 302 303 304 305 306
    );
    final String flutterFrameworkFileName = globals.fs.path.basename(
      engineCacheFlutterFrameworkDirectory,
    );
    final Directory fatFlutterFrameworkCopy = modeDirectory.childDirectory(
      flutterFrameworkFileName,
    );
307

308
    try {
309
      // Copy universal engine cache framework to mode directory.
310
      globals.fsUtils.copyDirectorySync(
311 312 313
        globals.fs.directory(engineCacheFlutterFrameworkDirectory),
        fatFlutterFrameworkCopy,
      );
314

315
      if (buildInfo.mode != BuildMode.debug) {
316 317
        final File fatFlutterFrameworkBinary = fatFlutterFrameworkCopy.childFile('Flutter');

318 319
        // Remove simulator architecture in profile and release mode.
        final List<String> lipoCommand = <String>[
320
          'xcrun',
321 322 323 324 325 326
          'lipo',
          fatFlutterFrameworkBinary.path,
          '-remove',
          'x86_64',
          '-output',
          fatFlutterFrameworkBinary.path
327
        ];
328
        final RunResult lipoResult = await processUtils.run(
329
          lipoCommand,
330 331 332
          allowReentrantFlutter: false,
        );

333 334
        if (lipoResult.exitCode != 0) {
          throwToolExit(
335
            'Unable to remove simulator architecture in ${buildInfo.mode}: ${lipoResult.stderr}',
336
          );
337 338 339 340
        }
      }
    } finally {
      status.stop();
xster's avatar
xster committed
341
    }
342

343
    await _produceXCFramework(buildInfo, fatFlutterFrameworkCopy);
xster's avatar
xster committed
344 345
  }

346
  Future<void> _produceAppFramework(BuildInfo buildInfo, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory modeDirectory) async {
xster's avatar
xster committed
347 348 349
    const String appFrameworkName = 'App.framework';
    final Directory destinationAppFrameworkDirectory = modeDirectory.childDirectory(appFrameworkName);

350
    if (buildInfo.mode == BuildMode.debug) {
351
      final Status status = globals.logger.startProgress(' ├─Adding placeholder App.framework for debug...', timeout: timeoutConfiguration.fastOperation);
352
      try {
353
        destinationAppFrameworkDirectory.createSync(recursive: true);
354
        await _produceStubAppFrameworkIfNeeded(buildInfo, iPhoneBuildOutput, simulatorBuildOutput, destinationAppFrameworkDirectory);
355 356 357
      } finally {
        status.stop();
      }
xster's avatar
xster committed
358
    } else {
359
      await _produceAotAppFrameworkIfNeeded(buildInfo, modeDirectory);
xster's avatar
xster committed
360 361 362 363 364 365 366
    }

    final File sourceInfoPlist = _project.ios.hostAppRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist');
    final File destinationInfoPlist = destinationAppFrameworkDirectory.childFile('Info.plist')..createSync(recursive: true);

    destinationInfoPlist.writeAsBytesSync(sourceInfoPlist.readAsBytesSync());

367 368
    final Status status = globals.logger.startProgress(
      ' ├─Assembling Flutter resources for App.framework...', timeout: timeoutConfiguration.slowOperation);
369
    try {
370
      await _bundleBuilder.build(
371
        platform: TargetPlatform.ios,
372
        buildInfo: buildInfo,
373
        // Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978.
374
        mainPath: globals.fs.path.absolute(targetFile),
375
        assetDirPath: destinationAppFrameworkDirectory.childDirectory('flutter_assets').path,
376
        precompiledSnapshot: buildInfo.mode != BuildMode.debug,
377
        treeShakeIcons: boolArg('tree-shake-icons')
378 379 380 381
      );
    } finally {
      status.stop();
    }
382
    await _produceXCFramework(buildInfo, destinationAppFrameworkDirectory);
xster's avatar
xster committed
383 384
  }

385 386
  Future<void> _produceStubAppFrameworkIfNeeded(BuildInfo buildInfo, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory destinationAppFrameworkDirectory) async {
    if (buildInfo.mode != BuildMode.debug) {
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
      return;
    }
    const String appFrameworkName = 'App.framework';
    const String binaryName = 'App';

    final Directory iPhoneAppFrameworkDirectory = iPhoneBuildOutput.childDirectory(appFrameworkName);
    final File iPhoneAppFrameworkFile = iPhoneAppFrameworkDirectory.childFile(binaryName);
    await createStubAppFramework(iPhoneAppFrameworkFile, SdkType.iPhone);

    final Directory simulatorAppFrameworkDirectory = simulatorBuildOutput.childDirectory(appFrameworkName);
    final File simulatorAppFrameworkFile = simulatorAppFrameworkDirectory.childFile(binaryName);
    await createStubAppFramework(simulatorAppFrameworkFile, SdkType.iPhoneSimulator);

    final List<String> lipoCommand = <String>[
      'xcrun',
      'lipo',
      '-create',
      iPhoneAppFrameworkFile.path,
      simulatorAppFrameworkFile.path,
      '-output',
      destinationAppFrameworkDirectory.childFile(binaryName).path
    ];

410
    final RunResult lipoResult = await processUtils.run(
411 412 413
      lipoCommand,
      allowReentrantFlutter: false,
    );
414 415 416 417

    if (lipoResult.exitCode != 0) {
      throwToolExit('Unable to create compiled dart universal framework: ${lipoResult.stderr}');
    }
418 419
  }

420
  Future<void> _produceAotAppFrameworkIfNeeded(
421
    BuildInfo buildInfo,
422
    Directory destinationDirectory,
423
  ) async {
424
    if (buildInfo.mode == BuildMode.debug) {
xster's avatar
xster committed
425 426
      return;
    }
427
    final Status status = globals.logger.startProgress(
428 429 430
      ' ├─Building Dart AOT for App.framework...',
      timeout: timeoutConfiguration.slowOperation,
    );
431
    try {
432
      await _aotBuilder.build(
433
        platform: TargetPlatform.ios,
434
        outputPath: destinationDirectory.path,
435
        buildInfo: buildInfo,
436
        // Relative paths show noise in the compiler https://github.com/dart-lang/sdk/issues/37978.
437
        mainDartFile: globals.fs.path.absolute(targetFile),
438 439 440 441 442 443 444
        quiet: true,
        bitcode: true,
        iosBuildArchs: <DarwinArch>[DarwinArch.armv7, DarwinArch.arm64],
      );
    } finally {
      status.stop();
    }
xster's avatar
xster committed
445 446 447
  }

  Future<void> _producePlugins(
448
    BuildMode mode,
xster's avatar
xster committed
449 450 451 452 453 454
    String xcodeBuildConfiguration,
    Directory iPhoneBuildOutput,
    Directory simulatorBuildOutput,
    Directory modeDirectory,
    Directory outputDirectory,
  ) async {
455 456
    final Status status = globals.logger.startProgress(
      ' ├─Building plugins...', timeout: timeoutConfiguration.slowOperation);
457
    try {
458 459 460 461 462 463 464 465 466
      // Regardless of the last "flutter build" build mode,
      // copy the corresponding engine.
      // A plugin framework built with bitcode must link against the bitcode version
      // of Flutter.framework (Release).
      _project.ios.copyEngineArtifactToProject(mode);

      final String bitcodeGenerationMode = mode == BuildMode.release ?
          'bitcode' : 'marker'; // In release, force bitcode embedding without archiving.

467 468 469 470 471 472 473 474
      List<String> pluginsBuildCommand = <String>[
        'xcrun',
        'xcodebuild',
        '-alltargets',
        '-sdk',
        'iphoneos',
        '-configuration',
        xcodeBuildConfiguration,
475
        '-destination generic/platform=iOS',
476
        'SYMROOT=${iPhoneBuildOutput.path}',
477
        'BITCODE_GENERATION_MODE=$bitcodeGenerationMode',
478 479
        'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
        'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
480
      ];
xster's avatar
xster committed
481

482
      RunResult buildPluginsResult = await processUtils.run(
483 484 485 486
        pluginsBuildCommand,
        workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path,
        allowReentrantFlutter: false,
      );
xster's avatar
xster committed
487

488 489 490
      if (buildPluginsResult.exitCode != 0) {
        throwToolExit('Unable to build plugin frameworks: ${buildPluginsResult.stderr}');
      }
xster's avatar
xster committed
491

492 493 494 495 496 497 498 499 500
      if (mode == BuildMode.debug) {
        pluginsBuildCommand = <String>[
          'xcrun',
          'xcodebuild',
          '-alltargets',
          '-sdk',
          'iphonesimulator',
          '-configuration',
          xcodeBuildConfiguration,
501
          '-destination generic/platform=iOS',
502 503
          'SYMROOT=${simulatorBuildOutput.path}',
          'ARCHS=x86_64',
504 505
          'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
          'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
506
        ];
xster's avatar
xster committed
507

508
        buildPluginsResult = await processUtils.run(
509 510
          pluginsBuildCommand,
          workingDirectory: _project.ios.hostAppRoot
511 512
            .childDirectory('Pods')
            .path,
513 514
          allowReentrantFlutter: false,
        );
xster's avatar
xster committed
515

516
        if (buildPluginsResult.exitCode != 0) {
517 518 519
          throwToolExit(
            'Unable to build plugin frameworks for simulator: ${buildPluginsResult.stderr}',
          );
520
        }
521 522
      }

523 524 525 526 527 528
      final Directory iPhoneBuildConfiguration = iPhoneBuildOutput.childDirectory(
        '$xcodeBuildConfiguration-iphoneos',
      );
      final Directory simulatorBuildConfiguration = simulatorBuildOutput.childDirectory(
        '$xcodeBuildConfiguration-iphonesimulator',
      );
529

530 531 532 533
      final Iterable<Directory> products = iPhoneBuildConfiguration
        .listSync(followLinks: false)
        .whereType<Directory>();
      for (final Directory builtProduct in products) {
534
        for (final FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) {
535
          final String podFrameworkName = podProduct.basename;
536 537 538 539 540
          if (globals.fs.path.extension(podFrameworkName) != '.framework') {
            continue;
          }
          final String binaryName = globals.fs.path.basenameWithoutExtension(podFrameworkName);
          if (boolArg('universal')) {
541
            globals.fsUtils.copyDirectorySync(
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
              podProduct as Directory,
              modeDirectory.childDirectory(podFrameworkName),
            );
            final List<String> lipoCommand = <String>[
              'xcrun',
              'lipo',
              '-create',
              globals.fs.path.join(podProduct.path, binaryName),
              if (mode == BuildMode.debug)
                simulatorBuildConfiguration
                  .childDirectory(binaryName)
                  .childDirectory(podFrameworkName)
                  .childFile(binaryName)
                  .path,
              '-output',
              modeDirectory.childDirectory(podFrameworkName).childFile(binaryName).path
            ];

560
            final RunResult pluginsLipoResult = await processUtils.run(
561 562 563 564 565 566 567 568
              lipoCommand,
              workingDirectory: outputDirectory.path,
              allowReentrantFlutter: false,
            );

            if (pluginsLipoResult.exitCode != 0) {
              throwToolExit(
                'Unable to create universal $binaryName.framework: ${buildPluginsResult.stderr}',
569 570
              );
            }
571
          }
572

573 574 575 576 577 578 579 580
          if (boolArg('xcframework')) {
            final List<String> xcframeworkCommand = <String>[
              'xcrun',
              'xcodebuild',
              '-create-xcframework',
              '-framework',
              podProduct.path,
              if (mode == BuildMode.debug)
581
                '-framework',
582 583 584 585 586 587 588 589 590
              if (mode == BuildMode.debug)
                simulatorBuildConfiguration
                  .childDirectory(binaryName)
                  .childDirectory(podFrameworkName)
                  .path,
              '-output',
              modeDirectory.childFile('$binaryName.xcframework').path
            ];

591
            final RunResult xcframeworkResult = await processUtils.run(
592 593 594 595 596 597 598 599
              xcframeworkCommand,
              workingDirectory: outputDirectory.path,
              allowReentrantFlutter: false,
            );

            if (xcframeworkResult.exitCode != 0) {
              throwToolExit(
                'Unable to create $binaryName.xcframework: ${xcframeworkResult.stderr}',
600 601
              );
            }
xster's avatar
xster committed
602 603 604
          }
        }
      }
605 606
    } finally {
      status.stop();
xster's avatar
xster committed
607 608
    }
  }
609

610
  Future<void> _produceXCFramework(BuildInfo buildInfo, Directory fatFramework) async {
611
    if (boolArg('xcframework')) {
612
      final String frameworkBinaryName = globals.fs.path.basenameWithoutExtension(
613 614
          fatFramework.basename);

615 616
      final Status status = globals.logger.startProgress(
        ' ├─Creating $frameworkBinaryName.xcframework...',
617
        timeout: timeoutConfiguration.slowOperation,
618
      );
619
      try {
620
        if (buildInfo.mode == BuildMode.debug) {
621
          await _produceDebugXCFramework(fatFramework, frameworkBinaryName);
622
        } else {
623
          await _produceNonDebugXCFramework(buildInfo, fatFramework, frameworkBinaryName);
624 625 626 627 628 629 630 631 632 633 634
        }
      } finally {
        status.stop();
      }
    }

    if (!boolArg('universal')) {
      fatFramework.deleteSync(recursive: true);
    }
  }

635
  Future<void> _produceDebugXCFramework(Directory fatFramework, String frameworkBinaryName) async {
636 637
    final String frameworkFileName = fatFramework.basename;
    final File fatFlutterFrameworkBinary = fatFramework.childFile(
638 639
      frameworkBinaryName,
    );
640
    final Directory temporaryOutput = globals.fs.systemTempDirectory.createTempSync(
641 642
      'flutter_tool_build_ios_framework.',
    );
643 644 645
    try {
      // Copy universal framework to variant directory.
      final Directory iPhoneBuildOutput = temporaryOutput.childDirectory(
646 647
        'ios',
      )..createSync(recursive: true);
648
      final Directory simulatorBuildOutput = temporaryOutput.childDirectory(
649 650
        'simulator',
      )..createSync(recursive: true);
651
      final Directory armFlutterFrameworkDirectory = iPhoneBuildOutput
652
        .childDirectory(frameworkFileName);
653
      final File armFlutterFrameworkBinary = armFlutterFrameworkDirectory
654
        .childFile(frameworkBinaryName);
655
      globals.fsUtils.copyDirectorySync(fatFramework, armFlutterFrameworkDirectory);
656 657 658 659 660 661 662 663 664 665 666 667

      // Create iOS framework.
      List<String> lipoCommand = <String>[
        'xcrun',
        'lipo',
        fatFlutterFrameworkBinary.path,
        '-remove',
        'x86_64',
        '-output',
        armFlutterFrameworkBinary.path
      ];

668
      RunResult lipoResult = await processUtils.run(
669 670 671 672 673 674 675 676 677 678
        lipoCommand,
        allowReentrantFlutter: false,
      );

      if (lipoResult.exitCode != 0) {
        throwToolExit('Unable to create ARM framework: ${lipoResult.stderr}');
      }

      // Create simulator framework.
      final Directory simulatorFlutterFrameworkDirectory = simulatorBuildOutput
679
        .childDirectory(frameworkFileName);
680
      final File simulatorFlutterFrameworkBinary = simulatorFlutterFrameworkDirectory
681
        .childFile(frameworkBinaryName);
682
      globals.fsUtils.copyDirectorySync(fatFramework, simulatorFlutterFrameworkDirectory);
683 684 685 686 687 688 689 690 691 692 693

      lipoCommand = <String>[
        'xcrun',
        'lipo',
        fatFlutterFrameworkBinary.path,
        '-thin',
        'x86_64',
        '-output',
        simulatorFlutterFrameworkBinary.path
      ];

694
      lipoResult = await processUtils.run(
695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715
        lipoCommand,
        allowReentrantFlutter: false,
      );

      if (lipoResult.exitCode != 0) {
        throwToolExit(
            'Unable to create simulator framework: ${lipoResult.stderr}');
      }

      // Create XCFramework from iOS and simulator frameworks.
      final List<String> xcframeworkCommand = <String>[
        'xcrun',
        'xcodebuild',
        '-create-xcframework',
        '-framework', armFlutterFrameworkDirectory.path,
        '-framework', simulatorFlutterFrameworkDirectory.path,
        '-output', fatFramework.parent
            .childFile('$frameworkBinaryName.xcframework')
            .path
      ];

716
      final RunResult xcframeworkResult = await processUtils.run(
717 718 719 720 721 722
        xcframeworkCommand,
        allowReentrantFlutter: false,
      );

      if (xcframeworkResult.exitCode != 0) {
        throwToolExit(
723 724
          'Unable to create XCFramework: ${xcframeworkResult.stderr}',
        );
725 726 727 728 729 730
      }
    } finally {
      temporaryOutput.deleteSync(recursive: true);
    }
  }

731
  Future<void> _produceNonDebugXCFramework(
732
    BuildInfo buildInfo,
733 734
    Directory fatFramework,
    String frameworkBinaryName,
735
  ) async {
736 737 738 739 740 741 742 743 744 745 746 747
    // Simulator is only supported in Debug mode.
    // "Fat" framework here must only contain arm.
    final List<String> xcframeworkCommand = <String>[
      'xcrun',
      'xcodebuild',
      '-create-xcframework',
      '-framework', fatFramework.path,
      '-output', fatFramework.parent
          .childFile('$frameworkBinaryName.xcframework')
          .path
    ];

748
    final RunResult xcframeworkResult = await processUtils.run(
749 750 751 752 753 754 755 756 757
      xcframeworkCommand,
      allowReentrantFlutter: false,
    );

    if (xcframeworkResult.exitCode != 0) {
      throwToolExit(
          'Unable to create XCFramework: ${xcframeworkResult.stderr}');
    }
  }
xster's avatar
xster committed
758
}