ios_test.dart 23.1 KB
Newer Older
1 2 3 4
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7
import 'package:file/memory.dart';
8 9 10
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
11
import 'package:flutter_tools/src/base/logger.dart';
12
import 'package:flutter_tools/src/base/platform.dart';
13 14 15
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/build_system/targets/ios.dart';
16
import 'package:flutter_tools/src/convert.dart';
17 18

import '../../../src/common.dart';
19 20 21
import '../../../src/context.dart';

final Platform macPlatform = FakePlatform(operatingSystem: 'macos', environment: <String, String>{});
22 23 24 25

const List<String> _kSharedConfig = <String>[
  '-dynamiclib',
  '-fembed-bitcode-marker',
26
  '-miphoneos-version-min=9.0',
27 28 29 30 31 32 33 34 35 36 37
  '-Xlinker',
  '-rpath',
  '-Xlinker',
  '@executable_path/Frameworks',
  '-Xlinker',
  '-rpath',
  '-Xlinker',
  '@loader_path/Frameworks',
  '-install_name',
  '@rpath/App.framework/App',
  '-isysroot',
38
  'path/to/iPhoneOS.sdk',
39 40 41 42
];

void main() {
  Environment environment;
43 44 45
  FileSystem fileSystem;
  FakeProcessManager processManager;
  Artifacts artifacts;
46
  BufferLogger logger;
47 48

  setUp(() {
49
    fileSystem = MemoryFileSystem.test();
50
    processManager = FakeProcessManager.empty();
51 52 53 54 55 56 57 58 59 60 61 62 63 64
    logger = BufferLogger.test();
    artifacts = Artifacts.test();
    environment = Environment.test(
      fileSystem.currentDirectory,
      defines: <String, String>{
        kTargetPlatform: 'ios',
      },
      inputs: <String, String>{},
      processManager: processManager,
      artifacts: artifacts,
      logger: logger,
      fileSystem: fileSystem,
      engineVersion: '2',
    );
65 66
  });

67
  testWithoutContext('iOS AOT targets has analyticsName', () {
68 69 70 71
    expect(const AotAssemblyRelease().analyticsName, 'ios_aot');
    expect(const AotAssemblyProfile().analyticsName, 'ios_aot');
  });

72
  testUsingContext('DebugUniversalFramework creates simulator binary', () async {
73 74 75 76 77 78 79 80 81 82 83 84 85 86
    environment.defines[kIosArchs] = 'x86_64';
    environment.defines[kSdkRoot] = 'path/to/iPhoneSimulator.sdk';
    processManager.addCommand(
      FakeCommand(command: <String>[
        'xcrun',
        'clang',
        '-x',
        'c',
        '-arch',
        'x86_64',
        fileSystem.path.absolute(fileSystem.path.join(
            '.tmp_rand0', 'flutter_tools_stub_source.rand0', 'debug_app.cc')),
        '-dynamiclib',
        '-fembed-bitcode-marker',
87
        '-miphonesimulator-version-min=9.0',
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
        '-Xlinker',
        '-rpath',
        '-Xlinker',
        '@executable_path/Frameworks',
        '-Xlinker',
        '-rpath',
        '-Xlinker',
        '@loader_path/Frameworks',
        '-install_name',
        '@rpath/App.framework/App',
        '-isysroot',
        'path/to/iPhoneSimulator.sdk',
        '-o',
        environment.buildDir
            .childDirectory('App.framework')
            .childFile('App')
            .path,
      ]),
    );

    await const DebugUniversalFramework().build(environment);
    expect(processManager.hasRemainingExpectations, isFalse);
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
    Platform: () => macPlatform,
  });

116
  testUsingContext('DebugUniversalFramework creates expected binary with arm64 only arch', () async {
117
    environment.defines[kIosArchs] = 'arm64';
118
    environment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk';
119
    processManager.addCommand(
120 121 122 123 124
      FakeCommand(command: <String>[
        'xcrun',
        'clang',
        '-x',
        'c',
125
        // iphone only gets 64 bit arch based on kIosArchs
126 127
        '-arch',
        'arm64',
128 129
        fileSystem.path.absolute(fileSystem.path.join(
            '.tmp_rand0', 'flutter_tools_stub_source.rand0', 'debug_app.cc')),
130 131
        ..._kSharedConfig,
        '-o',
132 133 134 135
        environment.buildDir
            .childDirectory('App.framework')
            .childFile('App')
            .path,
136
      ]),
137
    );
138

139
    await const DebugUniversalFramework().build(environment);
140
    expect(processManager.hasRemainingExpectations, isFalse);
141
  }, overrides: <Type, Generator>{
142
    FileSystem: () => fileSystem,
143
    ProcessManager: () => processManager,
144 145
    Platform: () => macPlatform,
  });
146

147
  testUsingContext('DebugIosApplicationBundle', () async {
148
    environment.defines[kBundleSkSLPath] = 'bundle.sksl';
149
    environment.defines[kBuildMode] = 'debug';
150
    environment.defines[kCodesignIdentity] = 'ABC123';
151
    // Precompiled dart data
152 153 154 155 156

    fileSystem.file(artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: BuildMode.debug))
      .createSync();
    fileSystem.file(artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: BuildMode.debug))
      .createSync();
157
    // Project info
158 159
    fileSystem.file('pubspec.yaml').writeAsStringSync('name: hello');
    fileSystem.file('.packages').writeAsStringSync('\n');
160
    // Plist file
161
    fileSystem.file(fileSystem.path.join('ios', 'Flutter', 'AppFrameworkInfo.plist'))
162
      .createSync(recursive: true);
163 164 165
    // App kernel
    environment.buildDir.childFile('app.dill').createSync(recursive: true);
    // Stub framework
166
    environment.buildDir
167
        .childDirectory('App.framework')
168 169
      .childFile('App')
      .createSync(recursive: true);
170
    // sksl bundle
171
    fileSystem.file('bundle.sksl').writeAsStringSync(json.encode(
172 173 174 175 176 177 178 179
      <String, Object>{
        'engineRevision': '2',
        'platform': 'ios',
        'data': <String, Object>{
          'A': 'B',
        }
      }
    ));
180

181 182
    final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
    final File frameworkDirectoryBinary = frameworkDirectory.childFile('App');
183
    processManager.addCommand(
184 185 186 187 188 189 190 191
      FakeCommand(command: <String>[
        'codesign',
        '--force',
        '--sign',
        'ABC123',
        '--timestamp=none',
        frameworkDirectoryBinary.path,
      ]),
192
    );
193

194
    await const DebugIosApplicationBundle().build(environment);
195
    expect(processManager.hasRemainingExpectations, isFalse);
196

197
    expect(frameworkDirectoryBinary, exists);
198 199 200 201 202 203 204
    expect(frameworkDirectory.childFile('Info.plist'), exists);

    final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
    expect(assetDirectory.childFile('kernel_blob.bin'), exists);
    expect(assetDirectory.childFile('AssetManifest.json'), exists);
    expect(assetDirectory.childFile('vm_snapshot_data'), exists);
    expect(assetDirectory.childFile('isolate_snapshot_data'), exists);
205 206
    expect(assetDirectory.childFile('io.flutter.shaders.json'), exists);
    expect(assetDirectory.childFile('io.flutter.shaders.json').readAsStringSync(), '{"data":{"A":"B"}}');
207
  });
208

209
  testUsingContext('ReleaseIosApplicationBundle', () async {
210
    environment.defines[kBuildMode] = 'release';
211
    environment.defines[kCodesignIdentity] = 'ABC123';
212 213

    // Project info
214 215
    fileSystem.file('pubspec.yaml').writeAsStringSync('name: hello');
    fileSystem.file('.packages').writeAsStringSync('\n');
216
    // Plist file
217
    fileSystem.file(fileSystem.path.join('ios', 'Flutter', 'AppFrameworkInfo.plist'))
218
      .createSync(recursive: true);
219 220 221 222 223 224 225 226 227

    // Real framework
    environment.buildDir
      .childDirectory('App.framework')
      .childFile('App')
      .createSync(recursive: true);


    final Directory frameworkDirectory = environment.outputDir.childDirectory('App.framework');
228
    final File frameworkDirectoryBinary = frameworkDirectory.childFile('App');
229
    processManager.addCommand(
230 231 232 233 234 235 236
      FakeCommand(command: <String>[
        'codesign',
        '--force',
        '--sign',
        'ABC123',
        frameworkDirectoryBinary.path,
      ]),
237
    );
238 239 240 241 242

    await const ReleaseIosApplicationBundle().build(environment);
    expect(processManager.hasRemainingExpectations, isFalse);

    expect(frameworkDirectoryBinary, exists);
243 244 245 246 247 248 249
    expect(frameworkDirectory.childFile('Info.plist'), exists);

    final Directory assetDirectory = frameworkDirectory.childDirectory('flutter_assets');
    expect(assetDirectory.childFile('kernel_blob.bin'), isNot(exists));
    expect(assetDirectory.childFile('AssetManifest.json'), exists);
    expect(assetDirectory.childFile('vm_snapshot_data'), isNot(exists));
    expect(assetDirectory.childFile('isolate_snapshot_data'), isNot(exists));
250 251 252 253 254
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
    Platform: () => macPlatform,
  });
255

256
  testUsingContext('AotAssemblyRelease throws exception if asked to build for simulator', () async {
257 258 259 260 261
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Environment environment = Environment.test(
      fileSystem.currentDirectory,
      defines: <String, String>{
        kTargetPlatform: 'ios',
262 263 264
        kSdkRoot: 'path/to/iPhoneSimulator.sdk',
        kBuildMode: 'release',
        kIosArchs: 'x86_64',
265 266
      },
      processManager: processManager,
267 268
      artifacts: artifacts,
      logger: logger,
269 270 271
      fileSystem: fileSystem,
    );

272
    expect(const AotAssemblyRelease().build(environment), throwsA(isException
273 274 275 276 277 278
      .having(
        (Exception exception) => exception.toString(),
        'description',
        contains('release/profile builds are only supported for physical devices.'),
      )
    ));
279
    expect(processManager.hasRemainingExpectations, isFalse);
280 281 282 283 284
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
    Platform: () => macPlatform,
  });
285

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  testUsingContext('AotAssemblyRelease throws exception if sdk root is missing', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Environment environment = Environment.test(
      fileSystem.currentDirectory,
      defines: <String, String>{
        kTargetPlatform: 'ios',
      },
      processManager: processManager,
      artifacts: artifacts,
      logger: logger,
      fileSystem: fileSystem,
    );
    environment.defines[kBuildMode] = 'release';
    environment.defines[kIosArchs] = 'x86_64';

301 302
    expect(const AotAssemblyRelease().build(environment), throwsA(isException.having(
      (Exception exception) => exception.toString(),
303 304
      'description',
      contains('required define SdkRoot but it was not provided'),
305
    )));
306
    expect(processManager.hasRemainingExpectations, isFalse);
307 308 309 310 311 312
  }, overrides: <Type, Generator>{
    FileSystem: () => fileSystem,
    ProcessManager: () => processManager,
    Platform: () => macPlatform,
  });

313
  group('copies Flutter.framework', () {
314
    Directory outputDir;
315
    File binary;
316
    FakeCommand copyPhysicalFrameworkCommand;
317 318 319
    FakeCommand lipoCommandNonFatResult;
    FakeCommand lipoVerifyArm64Command;
    FakeCommand bitcodeStripCommand;
320 321

    setUp(() {
322
      final FileSystem fileSystem = MemoryFileSystem.test();
323
      outputDir = fileSystem.directory('output');
324
      binary = outputDir.childDirectory('Flutter.framework').childFile('Flutter');
325 326 327 328 329 330 331 332 333
      copyPhysicalFrameworkCommand = FakeCommand(command: <String>[
        'rsync',
        '-av',
        '--delete',
        '--filter',
        '- .DS_Store/',
        'Artifact.flutterFramework.TargetPlatform.ios.debug.EnvironmentType.physical',
        outputDir.path,
      ]);
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355

      lipoCommandNonFatResult = FakeCommand(command: <String>[
        'lipo',
        '-info',
        binary.path,
      ], stdout: 'Non-fat file:');

      lipoVerifyArm64Command = FakeCommand(command: <String>[
        'lipo',
        binary.path,
        '-verify_arch',
        'arm64',
      ]);

      bitcodeStripCommand = FakeCommand(command: <String>[
        'xcrun',
        'bitcode_strip',
        binary.path,
        '-m',
        '-o',
        binary.path,
      ]);
356 357 358
    });

    testWithoutContext('iphonesimulator', () async {
359 360 361 362 363 364 365 366
      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
367
          kIosArchs: 'x86_64',
368
          kSdkRoot: 'path/to/iPhoneSimulator.sdk',
369
          kBitcodeFlag: 'true',
370 371
        },
      );
372

373
      processManager.addCommands(<FakeCommand>[
374 375 376 377 378 379 380 381
        FakeCommand(command: <String>[
          'rsync',
          '-av',
          '--delete',
          '--filter',
          '- .DS_Store/',
          'Artifact.flutterFramework.TargetPlatform.ios.debug.EnvironmentType.simulator',
          outputDir.path,
382 383 384
          ],
          onRun: () => binary.createSync(recursive: true),
        ),
385
        lipoCommandNonFatResult,
386
        FakeCommand(command: <String>[
387 388 389 390
          'lipo',
          binary.path,
          '-verify_arch',
          'x86_64',
391
        ]),
392
      ]);
393
      await const DebugUnpackIOS().build(environment);
394 395 396

      expect(logger.traceText, contains('Skipping lipo for non-fat file output/Flutter.framework/Flutter'));
      expect(processManager.hasRemainingExpectations, isFalse);
397
    });
398

399
    testWithoutContext('fails when frameworks missing', () async {
400 401 402 403 404 405 406 407 408
      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64',
409
          kSdkRoot: 'path/to/iPhoneOS.sdk',
410
          kBitcodeFlag: '',
411 412
        },
      );
413
      processManager.addCommand(copyPhysicalFrameworkCommand);
414
      await expectLater(
415
        const DebugUnpackIOS().build(environment),
416
        throwsA(isException.having(
417 418
          (Exception exception) => exception.toString(),
          'description',
419
          contains('Flutter.framework/Flutter does not exist, cannot thin'),
420 421 422
        )));
    });

423
    testWithoutContext('fails when requested archs missing from framework', () async {
424
      binary.createSync(recursive: true);
425 426 427 428 429 430 431 432 433 434

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64 armv7',
435
          kSdkRoot: 'path/to/iPhoneOS.sdk',
436
          kBitcodeFlag: '',
437 438 439
        },
      );

440 441
      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
442 443 444
        FakeCommand(command: <String>[
          'lipo',
          '-info',
445
          binary.path,
446 447 448
        ], stdout: 'Architectures in the fat file:'),
        FakeCommand(command: <String>[
          'lipo',
449
          binary.path,
450 451 452 453
          '-verify_arch',
          'arm64',
          'armv7',
        ], exitCode: 1),
454
      ]);
455

456
      await expectLater(
457 458 459 460 461 462 463
        const DebugUnpackIOS().build(environment),
        throwsA(isException.having(
          (Exception exception) => exception.toString(),
          'description',
          contains('does not contain arm64 armv7. Running lipo -info:\nArchitectures in the fat file:'),
        )),
      );
464 465
    });

466
    testWithoutContext('fails when lipo extract fails', () async {
467
      binary.createSync(recursive: true);
468 469 470 471 472 473 474 475 476 477

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64 armv7',
478
          kSdkRoot: 'path/to/iPhoneOS.sdk',
479
          kBitcodeFlag: '',
480 481 482
        },
      );

483 484
      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
485 486 487
        FakeCommand(command: <String>[
          'lipo',
          '-info',
488
          binary.path,
489 490 491
        ], stdout: 'Architectures in the fat file:'),
        FakeCommand(command: <String>[
          'lipo',
492
          binary.path,
493 494 495 496 497 498 499
          '-verify_arch',
          'arm64',
          'armv7',
        ]),
        FakeCommand(command: <String>[
          'lipo',
          '-output',
500
          binary.path,
501 502 503 504
          '-extract',
          'arm64',
          '-extract',
          'armv7',
505
          binary.path,
506 507
        ], exitCode: 1,
        stderr: 'lipo error'),
508
      ]);
509

510
      await expectLater(
511
        const DebugUnpackIOS().build(environment),
512 513
        throwsA(isException.having(
          (Exception exception) => exception.toString(),
514
          'description',
515
          contains('Failed to extract arm64 armv7 for output/Flutter.framework/Flutter.\nlipo error\nRunning lipo -info:\nArchitectures in the fat file:'),
516 517
        )),
      );
518 519
    });

520
    testWithoutContext('skips thin framework', () async {
521
      binary.createSync(recursive: true);
522 523 524 525 526 527 528 529 530 531

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64',
532
          kSdkRoot: 'path/to/iPhoneOS.sdk',
533
          kBitcodeFlag: 'true',
534 535 536
        },
      );

537 538 539 540 541
      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
        lipoCommandNonFatResult,
        lipoVerifyArm64Command,
      ]);
542
      await const DebugUnpackIOS().build(environment);
543

544
      expect(logger.traceText, contains('Skipping lipo for non-fat file output/Flutter.framework/Flutter'));
545 546 547 548

      expect(processManager.hasRemainingExpectations, isFalse);
    });

549
    testWithoutContext('thins fat framework', () async {
550
      binary.createSync(recursive: true);
551 552 553 554 555 556 557 558 559 560

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64 armv7',
561
          kSdkRoot: 'path/to/iPhoneOS.sdk',
562
          kBitcodeFlag: 'true',
563 564 565
        },
      );

566 567
      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
568 569 570
        FakeCommand(command: <String>[
          'lipo',
          '-info',
571
          binary.path,
572 573 574
        ], stdout: 'Architectures in the fat file:'),
        FakeCommand(command: <String>[
          'lipo',
575
          binary.path,
576 577 578 579 580 581 582
          '-verify_arch',
          'arm64',
          'armv7',
        ]),
        FakeCommand(command: <String>[
          'lipo',
          '-output',
583
          binary.path,
584 585 586 587
          '-extract',
          'arm64',
          '-extract',
          'armv7',
588
          binary.path,
589
        ]),
590
      ]);
591

592
      await const DebugUnpackIOS().build(environment);
593 594
      expect(processManager.hasRemainingExpectations, isFalse);
    });
595 596

    testWithoutContext('fails when bitcode strip fails', () async {
597
      binary.createSync(recursive: true);
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64',
          kSdkRoot: 'path/to/iPhoneOS.sdk',
          kBitcodeFlag: '',
        },
      );

      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
615 616
        lipoCommandNonFatResult,
        lipoVerifyArm64Command,
617 618 619 620 621 622 623 624 625 626 627
        FakeCommand(command: <String>[
          'xcrun',
          'bitcode_strip',
          binary.path,
          '-m',
          '-o',
          binary.path,
        ], exitCode: 1, stderr: 'bitcode_strip error'),
      ]);

      await expectLater(
628 629 630 631 632 633 634
        const DebugUnpackIOS().build(environment),
        throwsA(isException.having(
          (Exception exception) => exception.toString(),
          'description',
          contains('Failed to strip bitcode for output/Flutter.framework/Flutter.\nbitcode_strip error'),
        )),
      );
635 636 637 638 639

      expect(processManager.hasRemainingExpectations, isFalse);
    });

    testWithoutContext('strips framework', () async {
640
      binary.createSync(recursive: true);
641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64',
          kSdkRoot: 'path/to/iPhoneOS.sdk',
          kBitcodeFlag: '',
        },
      );

      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
        lipoCommandNonFatResult,
        lipoVerifyArm64Command,
        bitcodeStripCommand,
      ]);
      await const DebugUnpackIOS().build(environment);

      expect(processManager.hasRemainingExpectations, isFalse);
    });

    testWithoutContext('fails when codesign fails', () async {
      binary.createSync(recursive: true);

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64',
          kSdkRoot: 'path/to/iPhoneOS.sdk',
          kBitcodeFlag: '',
          kCodesignIdentity: 'ABC123',
        },
      );

      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
        lipoCommandNonFatResult,
        lipoVerifyArm64Command,
        bitcodeStripCommand,
690
        FakeCommand(command: <String>[
691 692 693 694 695
          'codesign',
          '--force',
          '--sign',
          'ABC123',
          '--timestamp=none',
696
          binary.path,
697 698 699 700
        ], exitCode: 1, stderr: 'codesign error'),
      ]);

      await expectLater(
701 702 703 704 705 706 707
        const DebugUnpackIOS().build(environment),
        throwsA(isException.having(
          (Exception exception) => exception.toString(),
          'description',
          contains('Failed to codesign output/Flutter.framework/Flutter with identity ABC123.\ncodesign error'),
        )),
      );
708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734

      expect(processManager.hasRemainingExpectations, isFalse);
    });

    testWithoutContext('codesigns framework', () async {
      binary.createSync(recursive: true);

      final Environment environment = Environment.test(
        fileSystem.currentDirectory,
        processManager: processManager,
        artifacts: artifacts,
        logger: logger,
        fileSystem: fileSystem,
        outputDir: outputDir,
        defines: <String, String>{
          kIosArchs: 'arm64',
          kSdkRoot: 'path/to/iPhoneOS.sdk',
          kBitcodeFlag: '',
          kCodesignIdentity: 'ABC123',
        },
      );

      processManager.addCommands(<FakeCommand>[
        copyPhysicalFrameworkCommand,
        lipoCommandNonFatResult,
        lipoVerifyArm64Command,
        bitcodeStripCommand,
735
        FakeCommand(command: <String>[
736 737 738 739 740
          'codesign',
          '--force',
          '--sign',
          'ABC123',
          '--timestamp=none',
741 742 743 744 745 746 747
          binary.path,
        ]),
      ]);
      await const DebugUnpackIOS().build(environment);

      expect(processManager.hasRemainingExpectations, isFalse);
    });
748
  });
749
}