build_ios_framework.dart 28.9 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';
xster's avatar
xster committed
9 10 11 12 13

import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
14
import '../base/platform.dart';
xster's avatar
xster committed
15 16 17
import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
18
import '../build_system/build_system.dart';
19
import '../build_system/targets/common.dart';
20
import '../build_system/targets/icon_tree_shaker.dart';
xster's avatar
xster committed
21 22
import '../build_system/targets/ios.dart';
import '../bundle.dart';
23
import '../cache.dart';
24
import '../convert.dart';
25
import '../globals.dart' as globals;
xster's avatar
xster committed
26 27 28 29 30
import '../macos/cocoapod_utils.dart';
import '../macos/xcode.dart';
import '../plugins.dart';
import '../project.dart';
import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
31
import '../version.dart';
xster's avatar
xster committed
32 33 34 35 36 37 38
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 {
39 40 41
  BuildIOSFrameworkCommand({
    FlutterVersion flutterVersion, // Instantiating FlutterVersion kicks off networking, so delay until it's needed, but allow test injection.
    @required BundleBuilder bundleBuilder,
42
    @required BuildSystem buildSystem,
43 44
    Cache cache,
    Platform platform
45
  }) : _flutterVersion = flutterVersion,
46
       _buildSystem = buildSystem,
47
       _bundleBuilder = bundleBuilder,
48 49
       _injectedCache = cache,
       _injectedPlatform = platform {
50
    addTreeShakeIconsFlag();
xster's avatar
xster committed
51 52 53
    usesTargetOption();
    usesFlavorOption();
    usesPubOption();
54
    usesDartDefineOption();
55 56
    addSplitDebugInfoOption();
    addDartObfuscationOption();
57
    usesExtraFrontendOptions();
xster's avatar
xster committed
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 84 85
    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).',
      )
86 87 88
      ..addFlag('cocoapods',
        help: 'Produce a Flutter.podspec instead of an engine Flutter.framework (recomended if host app uses CocoaPods).',
      )
xster's avatar
xster committed
89 90 91 92
      ..addOption('output',
        abbr: 'o',
        valueHelp: 'path/to/directory/',
        help: 'Location to write the frameworks.',
93 94 95 96 97
      )
      ..addFlag('force',
        abbr: 'f',
        help: 'Force Flutter.podspec creation on the master channel. For testing only.',
        hide: true
xster's avatar
xster committed
98 99 100
      );
  }

101
  final BundleBuilder _bundleBuilder;
102 103
  final BuildSystem _buildSystem;
  BuildSystem get buildSystem => _buildSystem ?? globals.buildSystem;
104 105 106 107 108 109

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

  Platform get _platform => _injectedPlatform ?? globals.platform;
  final Platform _injectedPlatform;
110 111

  FlutterVersion _flutterVersion;
xster's avatar
xster committed
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127

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

128 129
  List<BuildInfo> get buildInfos {
    final List<BuildInfo> buildModes = <BuildInfo>[];
xster's avatar
xster committed
130

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

345
    await _produceXCFramework(buildInfo, fatFlutterFrameworkCopy);
xster's avatar
xster committed
346 347
  }

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

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

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

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

389 390
  Future<void> _produceStubAppFrameworkIfNeeded(BuildInfo buildInfo, Directory iPhoneBuildOutput, Directory simulatorBuildOutput, Directory destinationAppFrameworkDirectory) async {
    if (buildInfo.mode != BuildMode.debug) {
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
      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
    ];

414
    final RunResult lipoResult = await processUtils.run(
415 416 417
      lipoCommand,
      allowReentrantFlutter: false,
    );
418 419 420 421

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

424
  Future<void> _produceAotAppFrameworkIfNeeded(
425
    BuildInfo buildInfo,
426
    Directory destinationDirectory,
427
  ) async {
428
    if (buildInfo.mode == BuildMode.debug) {
xster's avatar
xster committed
429 430
      return;
    }
431
    final Status status = globals.logger.startProgress(
432 433 434
      ' ├─Building Dart AOT for App.framework...',
      timeout: timeoutConfiguration.slowOperation,
    );
435
    try {
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
      final Target target = buildInfo.isRelease
        ? const ReleaseIosApplicationBundle()
        : const ProfileIosApplicationBundle();
      final Environment environment = Environment(
        projectDir: globals.fs.currentDirectory,
        outputDir: destinationDirectory,
        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,
463 464 465
        engineVersion: globals.artifacts.isLocalEngine
          ? null
          : globals.flutterVersion.engineRevision,
466
      );
467 468 469 470 471 472 473
      final BuildResult result = await buildSystem.build(target, environment);
      if (!result.success) {
        for (final ExceptionMeasurement measurement in result.exceptions.values) {
          globals.printError(measurement.exception.toString());
        }
        throwToolExit('The aot build failed.');
      }
474 475 476
    } finally {
      status.stop();
    }
xster's avatar
xster committed
477 478 479
  }

  Future<void> _producePlugins(
480
    BuildMode mode,
xster's avatar
xster committed
481 482 483 484 485 486
    String xcodeBuildConfiguration,
    Directory iPhoneBuildOutput,
    Directory simulatorBuildOutput,
    Directory modeDirectory,
    Directory outputDirectory,
  ) async {
487 488
    final Status status = globals.logger.startProgress(
      ' ├─Building plugins...', timeout: timeoutConfiguration.slowOperation);
489
    try {
490 491 492 493 494 495 496 497 498
      // 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.

499 500 501 502 503 504 505 506
      List<String> pluginsBuildCommand = <String>[
        'xcrun',
        'xcodebuild',
        '-alltargets',
        '-sdk',
        'iphoneos',
        '-configuration',
        xcodeBuildConfiguration,
507
        '-destination generic/platform=iOS',
508
        'SYMROOT=${iPhoneBuildOutput.path}',
509
        'BITCODE_GENERATION_MODE=$bitcodeGenerationMode',
510 511
        'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
        'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
512
      ];
xster's avatar
xster committed
513

514
      RunResult buildPluginsResult = await processUtils.run(
515 516 517 518
        pluginsBuildCommand,
        workingDirectory: _project.ios.hostAppRoot.childDirectory('Pods').path,
        allowReentrantFlutter: false,
      );
xster's avatar
xster committed
519

520 521 522
      if (buildPluginsResult.exitCode != 0) {
        throwToolExit('Unable to build plugin frameworks: ${buildPluginsResult.stderr}');
      }
xster's avatar
xster committed
523

524 525 526 527 528 529 530 531 532
      if (mode == BuildMode.debug) {
        pluginsBuildCommand = <String>[
          'xcrun',
          'xcodebuild',
          '-alltargets',
          '-sdk',
          'iphonesimulator',
          '-configuration',
          xcodeBuildConfiguration,
533
          '-destination generic/platform=iOS',
534 535
          'SYMROOT=${simulatorBuildOutput.path}',
          'ARCHS=x86_64',
536 537
          'ONLY_ACTIVE_ARCH=NO', // No device targeted, so build all valid architectures.
          'BUILD_LIBRARY_FOR_DISTRIBUTION=YES',
538
        ];
xster's avatar
xster committed
539

540
        buildPluginsResult = await processUtils.run(
541 542
          pluginsBuildCommand,
          workingDirectory: _project.ios.hostAppRoot
543 544
            .childDirectory('Pods')
            .path,
545 546
          allowReentrantFlutter: false,
        );
xster's avatar
xster committed
547

548
        if (buildPluginsResult.exitCode != 0) {
549 550 551
          throwToolExit(
            'Unable to build plugin frameworks for simulator: ${buildPluginsResult.stderr}',
          );
552
        }
553 554
      }

555 556 557 558 559 560
      final Directory iPhoneBuildConfiguration = iPhoneBuildOutput.childDirectory(
        '$xcodeBuildConfiguration-iphoneos',
      );
      final Directory simulatorBuildConfiguration = simulatorBuildOutput.childDirectory(
        '$xcodeBuildConfiguration-iphonesimulator',
      );
561

562 563 564 565
      final Iterable<Directory> products = iPhoneBuildConfiguration
        .listSync(followLinks: false)
        .whereType<Directory>();
      for (final Directory builtProduct in products) {
566
        for (final FileSystemEntity podProduct in builtProduct.listSync(followLinks: false)) {
567
          final String podFrameworkName = podProduct.basename;
568 569 570 571 572
          if (globals.fs.path.extension(podFrameworkName) != '.framework') {
            continue;
          }
          final String binaryName = globals.fs.path.basenameWithoutExtension(podFrameworkName);
          if (boolArg('universal')) {
573
            globals.fsUtils.copyDirectorySync(
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
              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
            ];

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

            if (pluginsLipoResult.exitCode != 0) {
              throwToolExit(
                'Unable to create universal $binaryName.framework: ${buildPluginsResult.stderr}',
601 602
              );
            }
603
          }
604

605 606 607 608 609 610 611 612
          if (boolArg('xcframework')) {
            final List<String> xcframeworkCommand = <String>[
              'xcrun',
              'xcodebuild',
              '-create-xcframework',
              '-framework',
              podProduct.path,
              if (mode == BuildMode.debug)
613
                '-framework',
614 615 616 617 618 619 620 621 622
              if (mode == BuildMode.debug)
                simulatorBuildConfiguration
                  .childDirectory(binaryName)
                  .childDirectory(podFrameworkName)
                  .path,
              '-output',
              modeDirectory.childFile('$binaryName.xcframework').path
            ];

623
            final RunResult xcframeworkResult = await processUtils.run(
624 625 626 627 628 629 630 631
              xcframeworkCommand,
              workingDirectory: outputDirectory.path,
              allowReentrantFlutter: false,
            );

            if (xcframeworkResult.exitCode != 0) {
              throwToolExit(
                'Unable to create $binaryName.xcframework: ${xcframeworkResult.stderr}',
632 633
              );
            }
xster's avatar
xster committed
634 635 636
          }
        }
      }
637 638
    } finally {
      status.stop();
xster's avatar
xster committed
639 640
    }
  }
641

642
  Future<void> _produceXCFramework(BuildInfo buildInfo, Directory fatFramework) async {
643
    if (boolArg('xcframework')) {
644
      final String frameworkBinaryName = globals.fs.path.basenameWithoutExtension(
645 646
          fatFramework.basename);

647 648
      final Status status = globals.logger.startProgress(
        ' ├─Creating $frameworkBinaryName.xcframework...',
649
        timeout: timeoutConfiguration.slowOperation,
650
      );
651
      try {
652
        if (buildInfo.mode == BuildMode.debug) {
653
          await _produceDebugXCFramework(fatFramework, frameworkBinaryName);
654
        } else {
655
          await _produceNonDebugXCFramework(buildInfo, fatFramework, frameworkBinaryName);
656 657 658 659 660 661 662 663 664 665 666
        }
      } finally {
        status.stop();
      }
    }

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

667
  Future<void> _produceDebugXCFramework(Directory fatFramework, String frameworkBinaryName) async {
668 669
    final String frameworkFileName = fatFramework.basename;
    final File fatFlutterFrameworkBinary = fatFramework.childFile(
670 671
      frameworkBinaryName,
    );
672
    final Directory temporaryOutput = globals.fs.systemTempDirectory.createTempSync(
673 674
      'flutter_tool_build_ios_framework.',
    );
675 676 677
    try {
      // Copy universal framework to variant directory.
      final Directory iPhoneBuildOutput = temporaryOutput.childDirectory(
678 679
        'ios',
      )..createSync(recursive: true);
680
      final Directory simulatorBuildOutput = temporaryOutput.childDirectory(
681 682
        'simulator',
      )..createSync(recursive: true);
683
      final Directory armFlutterFrameworkDirectory = iPhoneBuildOutput
684
        .childDirectory(frameworkFileName);
685
      final File armFlutterFrameworkBinary = armFlutterFrameworkDirectory
686
        .childFile(frameworkBinaryName);
687
      globals.fsUtils.copyDirectorySync(fatFramework, armFlutterFrameworkDirectory);
688 689 690 691 692 693 694 695 696 697 698 699

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

700
      RunResult lipoResult = await processUtils.run(
701 702 703 704 705 706 707 708 709 710
        lipoCommand,
        allowReentrantFlutter: false,
      );

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

      // Create simulator framework.
      final Directory simulatorFlutterFrameworkDirectory = simulatorBuildOutput
711
        .childDirectory(frameworkFileName);
712
      final File simulatorFlutterFrameworkBinary = simulatorFlutterFrameworkDirectory
713
        .childFile(frameworkBinaryName);
714
      globals.fsUtils.copyDirectorySync(fatFramework, simulatorFlutterFrameworkDirectory);
715 716 717 718 719 720 721 722 723 724 725

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

726
      lipoResult = await processUtils.run(
727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747
        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
      ];

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

      if (xcframeworkResult.exitCode != 0) {
        throwToolExit(
755 756
          'Unable to create XCFramework: ${xcframeworkResult.stderr}',
        );
757 758 759 760 761 762
      }
    } finally {
      temporaryOutput.deleteSync(recursive: true);
    }
  }

763
  Future<void> _produceNonDebugXCFramework(
764
    BuildInfo buildInfo,
765 766
    Directory fatFramework,
    String frameworkBinaryName,
767
  ) async {
768 769 770 771 772 773 774 775 776 777 778 779
    // 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
    ];

780
    final RunResult xcframeworkResult = await processUtils.run(
781 782 783 784 785 786 787 788 789
      xcframeworkCommand,
      allowReentrantFlutter: false,
    );

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