build_ios_framework.dart 25.7 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
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:file/file.dart';
6
import 'package:meta/meta.dart';
xster's avatar
xster committed
7 8 9 10 11

import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
12
import '../base/platform.dart';
xster's avatar
xster committed
13 14 15
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
16
import '../build_system/build_system.dart';
17
import '../build_system/targets/common.dart';
18
import '../build_system/targets/icon_tree_shaker.dart';
xster's avatar
xster committed
19
import '../build_system/targets/ios.dart';
20
import '../cache.dart';
21
import '../convert.dart';
22
import '../globals.dart' as globals;
xster's avatar
xster committed
23 24 25 26
import '../macos/cocoapod_utils.dart';
import '../plugins.dart';
import '../project.dart';
import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
27
import '../version.dart';
xster's avatar
xster committed
28 29 30 31 32 33 34
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 {
35 36
  BuildIOSFrameworkCommand({
    FlutterVersion flutterVersion, // Instantiating FlutterVersion kicks off networking, so delay until it's needed, but allow test injection.
37
    @required BuildSystem buildSystem,
38
    @required bool verboseHelp,
39 40
    Cache cache,
    Platform platform
41
  }) : _flutterVersion = flutterVersion,
42
       _buildSystem = buildSystem,
43 44
       _injectedCache = cache,
       _injectedPlatform = platform {
45
    addTreeShakeIconsFlag();
xster's avatar
xster committed
46 47 48
    usesTargetOption();
    usesFlavorOption();
    usesPubOption();
49
    usesDartDefineOption();
50 51
    addSplitDebugInfoOption();
    addDartObfuscationOption();
52
    usesExtraFrontendOptions();
53 54 55
    addNullSafetyModeOptions(hide: !verboseHelp);
    addEnableExperimentation(hide: !verboseHelp);

xster's avatar
xster committed
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 83
    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).',
      )
84 85 86
      ..addFlag('cocoapods',
        help: 'Produce a Flutter.podspec instead of an engine Flutter.framework (recomended if host app uses CocoaPods).',
      )
xster's avatar
xster committed
87 88 89 90
      ..addOption('output',
        abbr: 'o',
        valueHelp: 'path/to/directory/',
        help: 'Location to write the frameworks.',
91 92 93 94 95
      )
      ..addFlag('force',
        abbr: 'f',
        help: 'Force Flutter.podspec creation on the master channel. For testing only.',
        hide: true
xster's avatar
xster committed
96 97 98
      );
  }

99 100
  final BuildSystem _buildSystem;
  BuildSystem get buildSystem => _buildSystem ?? globals.buildSystem;
101 102 103 104 105 106

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

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

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

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

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

128
    if (boolArg('debug')) {
129
      buildInfos.add(getBuildInfo(forcedBuildMode: BuildMode.debug));
xster's avatar
xster committed
130
    }
131
    if (boolArg('profile')) {
132
      buildInfos.add(getBuildInfo(forcedBuildMode: BuildMode.profile));
xster's avatar
xster committed
133
    }
134
    if (boolArg('release')) {
135
      buildInfos.add(getBuildInfo(forcedBuildMode: BuildMode.release));
xster's avatar
xster committed
136 137
    }

138
    return buildInfos;
xster's avatar
xster committed
139 140 141 142 143 144 145 146 147 148
  }

  @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.');
    }

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

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

  @override
  Future<FlutterCommandResult> runCommand() async {
166
    final String outputArgument = stringArg('output')
167
        ?? globals.fs.path.join(globals.fs.currentDirectory.path, 'build', 'ios', 'framework');
xster's avatar
xster committed
168 169 170 171 172

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

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

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

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

      if (modeDirectory.existsSync()) {
        modeDirectory.deleteSync(recursive: true);
      }
xster's avatar
xster committed
188

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

      // Build aot, create module.framework and copy.
199
      await _produceAppFramework(buildInfo, modeDirectory);
xster's avatar
xster committed
200 201

      // Build and copy plugins.
202
      await processPodsIfNeeded(_project.ios, getIosBuildDirectory(), buildInfo.mode);
203 204
      final Directory iPhoneBuildOutput = modeDirectory.childDirectory('iphoneos');
      final Directory simulatorBuildOutput = modeDirectory.childDirectory('iphonesimulator');
xster's avatar
xster committed
205
      if (hasPlugins(_project)) {
206
        await _producePlugins(buildInfo.mode, xcodeBuildConfiguration, iPhoneBuildOutput, simulatorBuildOutput, modeDirectory, outputDirectory);
xster's avatar
xster committed
207 208
      }

209 210
      final Status status = globals.logger.startProgress(
        ' └─Moving to ${globals.fs.path.relative(modeDirectory.path)}', timeout: timeoutConfiguration.slowOperation);
211 212 213 214 215 216 217 218 219 220 221
      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
222 223 224
      }
    }

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

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

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

247
      final File license = _cache.getLicenseFile();
248 249 250 251 252 253 254 255 256
      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'
257
  s.version               = '${gitTagVersion.x}.${gitTagVersion.y}.$minorHotfixVersion' # ${_flutterVersion.frameworkVersion}
258 259 260 261 262 263 264 265 266 267 268 269
  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' }
270
  s.source                = { :http => '${_cache.storageBaseUrl}/flutter_infra/flutter/${_cache.engineRevision}/$artifactsMode/artifacts.zip' }
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
  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();
    }
  }

287
  Future<void> _produceFlutterFramework(
288
    BuildInfo buildInfo,
289 290 291 292 293 294 295 296 297
    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,
298
      mode: buildInfo.mode,
299 300 301 302 303 304 305
    );
    final String flutterFrameworkFileName = globals.fs.path.basename(
      engineCacheFlutterFrameworkDirectory,
    );
    final Directory fatFlutterFrameworkCopy = modeDirectory.childDirectory(
      flutterFrameworkFileName,
    );
306

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

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

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

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

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

345
  Future<void> _produceAppFramework(BuildInfo buildInfo, Directory modeDirectory) async {
xster's avatar
xster committed
346 347
    const String appFrameworkName = 'App.framework';

348
    final Status status = globals.logger.startProgress(
349
      ' ├─Building App.framework...',
350 351
      timeout: timeoutConfiguration.slowOperation,
    );
352
    try {
353 354 355 356 357 358 359 360 361
      Target target;
      if (buildInfo.isDebug) {
        target = const DebugIosApplicationBundle();
      } else if (buildInfo.isProfile) {
        target = const ProfileIosApplicationBundle();
      } else {
        target = const ReleaseIosApplicationBundle();
      }

362 363
      final Environment environment = Environment(
        projectDir: globals.fs.currentDirectory,
364
        outputDir: modeDirectory,
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
        buildDir: _project.dartTool.childDirectory('flutter_build'),
        cacheDir: null,
        flutterRootDir: globals.fs.directory(Cache.flutterRoot),
        defines: <String, String>{
          kTargetFile: targetFile,
          kBuildMode: getNameForBuildMode(buildInfo.mode),
          kTargetPlatform: getNameForTargetPlatform(TargetPlatform.ios),
          kIconTreeShakerFlag: buildInfo.treeShakeIcons.toString(),
          kDartDefines: jsonEncode(buildInfo.dartDefines),
          kBitcodeFlag: 'true',
          if (buildInfo?.extraGenSnapshotOptions?.isNotEmpty ?? false)
            kExtraGenSnapshotOptions: buildInfo.extraGenSnapshotOptions.join(','),
          if (buildInfo?.extraFrontEndOptions?.isNotEmpty ?? false)
            kExtraFrontEndOptions: buildInfo.extraFrontEndOptions.join(','),
          kIosArchs: <DarwinArch>[DarwinArch.armv7, DarwinArch.arm64]
            .map(getNameForDarwinArch).join(' '),
        },
        artifacts: globals.artifacts,
        fileSystem: globals.fs,
        logger: globals.logger,
        processManager: globals.processManager,
386 387 388
        engineVersion: globals.artifacts.isLocalEngine
          ? null
          : globals.flutterVersion.engineRevision,
389
      );
390 391 392 393 394
      final BuildResult result = await buildSystem.build(target, environment);
      if (!result.success) {
        for (final ExceptionMeasurement measurement in result.exceptions.values) {
          globals.printError(measurement.exception.toString());
        }
395
        throwToolExit('The App.framework build failed.');
396
      }
397 398 399
    } finally {
      status.stop();
    }
400 401 402

    final Directory destinationAppFrameworkDirectory = modeDirectory.childDirectory(appFrameworkName);
    await _produceXCFramework(buildInfo, destinationAppFrameworkDirectory);
xster's avatar
xster committed
403 404 405
  }

  Future<void> _producePlugins(
406
    BuildMode mode,
xster's avatar
xster committed
407 408 409 410 411 412
    String xcodeBuildConfiguration,
    Directory iPhoneBuildOutput,
    Directory simulatorBuildOutput,
    Directory modeDirectory,
    Directory outputDirectory,
  ) async {
413 414
    final Status status = globals.logger.startProgress(
      ' ├─Building plugins...', timeout: timeoutConfiguration.slowOperation);
415
    try {
416 417 418 419 420 421 422 423 424
      // 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.

425 426 427 428 429 430 431 432
      List<String> pluginsBuildCommand = <String>[
        'xcrun',
        'xcodebuild',
        '-alltargets',
        '-sdk',
        'iphoneos',
        '-configuration',
        xcodeBuildConfiguration,
433
        '-destination generic/platform=iOS',
434
        'SYMROOT=${iPhoneBuildOutput.path}',
435
        'BITCODE_GENERATION_MODE=$bitcodeGenerationMode',
436 437
        'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
        'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
438
      ];
xster's avatar
xster committed
439

440
      RunResult buildPluginsResult = await processUtils.run(
441 442 443 444
        pluginsBuildCommand,
        workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path,
        allowReentrantFlutter: false,
      );
xster's avatar
xster committed
445

446 447 448
      if (buildPluginsResult.exitCode != 0) {
        throwToolExit('Unable to build plugin frameworks: ${buildPluginsResult.stderr}');
      }
xster's avatar
xster committed
449

450 451 452 453 454 455 456 457 458
      if (mode == BuildMode.debug) {
        pluginsBuildCommand = <String>[
          'xcrun',
          'xcodebuild',
          '-alltargets',
          '-sdk',
          'iphonesimulator',
          '-configuration',
          xcodeBuildConfiguration,
459
          '-destination generic/platform=iOS',
460 461
          'SYMROOT=${simulatorBuildOutput.path}',
          'ARCHS=x86_64',
462 463
          'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
          'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
464
        ];
xster's avatar
xster committed
465

466
        buildPluginsResult = await processUtils.run(
467 468
          pluginsBuildCommand,
          workingDirectory: _project.ios.hostAppRoot
469 470
            .childDirectory('Pods')
            .path,
471 472
          allowReentrantFlutter: false,
        );
xster's avatar
xster committed
473

474
        if (buildPluginsResult.exitCode != 0) {
475 476 477
          throwToolExit(
            'Unable to build plugin frameworks for simulator: ${buildPluginsResult.stderr}',
          );
478
        }
479 480
      }

481 482 483 484 485 486
      final Directory iPhoneBuildConfiguration = iPhoneBuildOutput.childDirectory(
        '$xcodeBuildConfiguration-iphoneos',
      );
      final Directory simulatorBuildConfiguration = simulatorBuildOutput.childDirectory(
        '$xcodeBuildConfiguration-iphonesimulator',
      );
487

488 489 490 491
      final Iterable<Directory> products = iPhoneBuildConfiguration
        .listSync(followLinks: false)
        .whereType<Directory>();
      for (final Directory builtProduct in products) {
492
        for (final FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) {
493
          final String podFrameworkName = podProduct.basename;
494 495 496 497 498
          if (globals.fs.path.extension(podFrameworkName) != '.framework') {
            continue;
          }
          final String binaryName = globals.fs.path.basenameWithoutExtension(podFrameworkName);
          if (boolArg('universal')) {
499
            globals.fsUtils.copyDirectorySync(
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517
              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
            ];

518
            final RunResult pluginsLipoResult = await processUtils.run(
519 520 521 522 523 524 525 526
              lipoCommand,
              workingDirectory: outputDirectory.path,
              allowReentrantFlutter: false,
            );

            if (pluginsLipoResult.exitCode != 0) {
              throwToolExit(
                'Unable to create universal $binaryName.framework: ${buildPluginsResult.stderr}',
527 528
              );
            }
529
          }
530

531 532 533 534 535 536 537 538
          if (boolArg('xcframework')) {
            final List<String> xcframeworkCommand = <String>[
              'xcrun',
              'xcodebuild',
              '-create-xcframework',
              '-framework',
              podProduct.path,
              if (mode == BuildMode.debug)
539
                '-framework',
540 541 542 543 544 545 546 547 548
              if (mode == BuildMode.debug)
                simulatorBuildConfiguration
                  .childDirectory(binaryName)
                  .childDirectory(podFrameworkName)
                  .path,
              '-output',
              modeDirectory.childFile('$binaryName.xcframework').path
            ];

549
            final RunResult xcframeworkResult = await processUtils.run(
550 551 552 553 554 555 556 557
              xcframeworkCommand,
              workingDirectory: outputDirectory.path,
              allowReentrantFlutter: false,
            );

            if (xcframeworkResult.exitCode != 0) {
              throwToolExit(
                'Unable to create $binaryName.xcframework: ${xcframeworkResult.stderr}',
558 559
              );
            }
xster's avatar
xster committed
560 561 562
          }
        }
      }
563 564
    } finally {
      status.stop();
xster's avatar
xster committed
565 566
    }
  }
567

568
  Future<void> _produceXCFramework(BuildInfo buildInfo, Directory fatFramework) async {
569
    if (boolArg('xcframework')) {
570
      final String frameworkBinaryName = globals.fs.path.basenameWithoutExtension(
571 572
          fatFramework.basename);

573 574
      final Status status = globals.logger.startProgress(
        ' ├─Creating $frameworkBinaryName.xcframework...',
575
        timeout: timeoutConfiguration.slowOperation,
576
      );
577
      try {
578
        if (buildInfo.mode == BuildMode.debug) {
579
          await _produceDebugXCFramework(fatFramework, frameworkBinaryName);
580
        } else {
581
          await _produceNonDebugXCFramework(buildInfo, fatFramework, frameworkBinaryName);
582 583 584 585 586 587 588 589 590 591 592
        }
      } finally {
        status.stop();
      }
    }

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

593
  Future<void> _produceDebugXCFramework(Directory fatFramework, String frameworkBinaryName) async {
594 595
    final String frameworkFileName = fatFramework.basename;
    final File fatFlutterFrameworkBinary = fatFramework.childFile(
596 597
      frameworkBinaryName,
    );
598
    final Directory temporaryOutput = globals.fs.systemTempDirectory.createTempSync(
599 600
      'flutter_tool_build_ios_framework.',
    );
601 602 603
    try {
      // Copy universal framework to variant directory.
      final Directory iPhoneBuildOutput = temporaryOutput.childDirectory(
604 605
        'ios',
      )..createSync(recursive: true);
606
      final Directory simulatorBuildOutput = temporaryOutput.childDirectory(
607 608
        'simulator',
      )..createSync(recursive: true);
609
      final Directory armFlutterFrameworkDirectory = iPhoneBuildOutput
610
        .childDirectory(frameworkFileName);
611
      final File armFlutterFrameworkBinary = armFlutterFrameworkDirectory
612
        .childFile(frameworkBinaryName);
613
      globals.fsUtils.copyDirectorySync(fatFramework, armFlutterFrameworkDirectory);
614 615 616 617 618 619 620 621 622 623 624 625

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

626
      RunResult lipoResult = await processUtils.run(
627 628 629 630 631 632 633 634 635 636
        lipoCommand,
        allowReentrantFlutter: false,
      );

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

      // Create simulator framework.
      final Directory simulatorFlutterFrameworkDirectory = simulatorBuildOutput
637
        .childDirectory(frameworkFileName);
638
      final File simulatorFlutterFrameworkBinary = simulatorFlutterFrameworkDirectory
639
        .childFile(frameworkBinaryName);
640
      globals.fsUtils.copyDirectorySync(fatFramework, simulatorFlutterFrameworkDirectory);
641 642 643 644 645 646 647 648 649 650 651

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

652
      lipoResult = await processUtils.run(
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
        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
      ];

674
      final RunResult xcframeworkResult = await processUtils.run(
675 676 677 678 679 680
        xcframeworkCommand,
        allowReentrantFlutter: false,
      );

      if (xcframeworkResult.exitCode != 0) {
        throwToolExit(
681 682
          'Unable to create XCFramework: ${xcframeworkResult.stderr}',
        );
683 684 685 686 687 688
      }
    } finally {
      temporaryOutput.deleteSync(recursive: true);
    }
  }

689
  Future<void> _produceNonDebugXCFramework(
690
    BuildInfo buildInfo,
691 692
    Directory fatFramework,
    String frameworkBinaryName,
693
  ) async {
694 695 696 697 698 699 700 701 702 703 704 705
    // 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
    ];

706
    final RunResult xcframeworkResult = await processUtils.run(
707 708 709 710 711 712 713 714 715
      xcframeworkCommand,
      allowReentrantFlutter: false,
    );

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