From 6ceb418b5886f06e075e9ce546d3ce07daf4ebc2 Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Tue, 1 Feb 2022 16:13:21 -0800 Subject: [PATCH] Export an IPA for distribution via "flutter build ipa" without --export-options-plist (#97243) --- .../lib/src/commands/build_ios.dart | 120 ++++++- .../hermetic/build_ipa_test.dart | 330 +++++++++++++----- 2 files changed, 349 insertions(+), 101 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 869ce589b52..71299a892f1 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:meta/meta.dart'; import '../base/analyze_size.dart'; import '../base/common.dart'; @@ -66,12 +67,23 @@ class BuildIOSCommand extends _BuildIOSSubCommand { class BuildIOSArchiveCommand extends _BuildIOSSubCommand { BuildIOSArchiveCommand({required bool verboseHelp}) : super(verboseHelp: verboseHelp) { + argParser.addOption( + 'export-method', + defaultsTo: 'app-store', + allowed: ['app-store', 'ad-hoc', 'development'], + help: 'Specify how the IPA will be distributed.', + allowedHelp: { + 'app-store': 'Upload to the App Store.', + 'ad-hoc': 'Distribute to designated devices that do not need to be registered with the Apple developer account. ' + 'Requires a distribution certificate.', + 'development': 'Distribute only to development devices registered with the Apple developer account.', + }, + ); argParser.addOption( 'export-options-plist', valueHelp: 'ExportOptions.plist', - // TODO(jmagman): Update help text with link to Flutter docs. help: - 'Optionally export an IPA with these options. See "xcodebuild -h" for available exportOptionsPlist keys.', + 'Export an IPA with these options. See "xcodebuild -h" for available exportOptionsPlist keys.', ); } @@ -82,7 +94,7 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { final List aliases = ['xcarchive']; @override - final String description = 'Build an iOS archive bundle (Mac OS X host only).'; + final String description = 'Build an iOS archive bundle and IPA for distribution (Mac OS X host only).'; @override final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.archive; @@ -105,9 +117,16 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { .childDirectory('Applications'); @override - Future runCommand() async { + Future validateCommand() async { final String? exportOptions = exportOptionsPlist; if (exportOptions != null) { + if (argResults?.wasParsed('export-method') == true) { + throwToolExit( + '"--export-options-plist" is not compatible with "--export-method". Either use "--export-options-plist" and ' + 'a plist describing how the IPA should be exported by Xcode, or use "--export-method" to create a new plist.\n' + 'See "xcodebuild -h" for available exportOptionsPlist keys.' + ); + } final FileSystemEntityType type = globals.fs.typeSync(exportOptions); if (type == FileSystemEntityType.notFound) { throwToolExit( @@ -117,14 +136,15 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { '"$exportOptions" is not a file. See "xcodebuild -h" for available keys.'); } } + return super.validateCommand(); + } + + @override + Future runCommand() async { final FlutterCommandResult xcarchiveResult = await super.runCommand(); final BuildInfo buildInfo = await getBuildInfo(); displayNullSafetyMode(buildInfo); - if (exportOptions == null) { - return xcarchiveResult; - } - // xcarchive failed or not at expected location. if (xcarchiveResult.exitStatus != ExitStatus.success) { globals.printStatus('Skipping IPA'); @@ -135,9 +155,20 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { final BuildableIOSApp app = await buildableIOSApp; Status? status; RunResult? result; - final String outputPath = globals.fs.path.absolute(app.ipaOutputPath); + final String relativeOutputPath = app.ipaOutputPath; + final String absoluteOutputPath = globals.fs.path.absolute(relativeOutputPath); + final String absoluteArchivePath = globals.fs.path.absolute(app.archiveBundleOutputPath); + final String exportMethod = stringArg('export-method')!; + final bool isAppStoreUpload = exportMethod == 'app-store'; + File? generatedExportPlist; try { - status = globals.logger.startProgress('Building IPA...'); + final String exportMethodDisplayName = isAppStoreUpload ? 'App Store' : exportMethod; + status = globals.logger.startProgress('Building $exportMethodDisplayName IPA...'); + String? exportOptions = exportOptionsPlist; + if (exportOptions == null) { + generatedExportPlist = _createExportPlist(); + exportOptions = generatedExportPlist.path; + } result = await globals.processUtils.run( [ @@ -149,14 +180,15 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { '-allowProvisioningUpdates', ], '-archivePath', - globals.fs.path.absolute(app.archiveBundleOutputPath), + absoluteArchivePath, '-exportPath', - outputPath, + absoluteOutputPath, '-exportOptionsPlist', globals.fs.path.absolute(exportOptions), ], ); } finally { + generatedExportPlist?.deleteSync(); status?.stop(); } @@ -171,13 +203,68 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { LineSplitter.split(result.stderr) .where((String line) => line.contains('error: ')) .forEach(errorMessage.writeln); - throwToolExit('Encountered error while building IPA:\n$errorMessage'); + + globals.printError('Encountered error while creating the IPA:'); + globals.printError(errorMessage.toString()); + globals.printError('Try distributing the app in Xcode: "open $absoluteArchivePath"'); + throwToolExit(null); } - globals.printStatus('Built IPA to $outputPath.'); + globals.printStatus('Built IPA to $absoluteOutputPath.'); + + if (isAppStoreUpload) { + globals.printStatus('To upload to the App Store either:'); + globals.printStatus( + '1. Drag and drop the "$relativeOutputPath/*.ipa" bundle into the Apple Transport macOS app https://apps.apple.com/us/app/transporter/id1450874784', + indent: 4, + ); + globals.printStatus( + '2. Run "xcrun altool --upload-app --type ios -f $relativeOutputPath/*.ipa --apiKey your_api_key --apiIssuer your_issuer_id".', + indent: 4, + ); + globals.printStatus( + 'See "man altool" for details about how to authenticate with the App Store Connect API key.', + indent: 7, + ); + } return FlutterCommandResult.success(); } + + File _createExportPlist() { + // Create the plist to be passed into xcodebuild -exportOptionsPlist. + final StringBuffer plistContents = StringBuffer(''' + + + + + method +'''); + + plistContents.write(''' + ${stringArg('export-method')} + '''); + if (xcodeBuildResult?.xcodeBuildExecution?.buildSettings['ENABLE_BITCODE'] != 'YES') { + // Bitcode is off by default in Flutter iOS apps. + plistContents.write(''' + uploadBitcode + + + +'''); + } else { + plistContents.write(''' + + +'''); + } + + final File tempPlist = globals.fs.systemTempDirectory + .createTempSync('flutter_build_ios.').childFile('ExportOptions.plist'); + tempPlist.writeAsStringSync(plistContents.toString()); + + return tempPlist; + } } abstract class _BuildIOSSubCommand extends BuildSubCommand { @@ -208,6 +295,10 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand { }; XcodeBuildAction get xcodeBuildAction; + + /// The result of the Xcode build command. Null until it finishes. + @protected + XcodeBuildResult? xcodeBuildResult; EnvironmentType get environmentType; bool get configOnly; bool get shouldCodesign; @@ -271,6 +362,7 @@ abstract class _BuildIOSSubCommand extends BuildSubCommand { buildAction: xcodeBuildAction, deviceID: globals.deviceManager?.specifiedDeviceId, ); + xcodeBuildResult = result; if (!result.success) { await diagnoseXcodeBuildFailure(result, globals.flutterUsage, globals.logger); diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart index 27734fb90e9..9f6bc89719f 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_test.dart @@ -7,7 +7,6 @@ import 'package:args/command_runner.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; -import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/build.dart'; @@ -18,9 +17,14 @@ import 'package:flutter_tools/src/reporting/reporting.dart'; import '../../general.shard/ios/xcresult_test_data.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fake_process_manager.dart'; import '../../src/test_flutter_command_runner.dart'; class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInterpreter { + FakeXcodeProjectInterpreterWithBuildSettings({ this.overrides = const {} }); + + final Map overrides; + @override Future> getBuildSettings( String projectPath, { @@ -28,6 +32,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter Duration timeout = const Duration(minutes: 1), }) async { return { + ...overrides, 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject', 'DEVELOPMENT_TEAM': 'abc', }; @@ -50,7 +55,7 @@ final Platform notMacosPlatform = FakePlatform( void main() { FileSystem fileSystem; TestUsage usage; - BufferLogger logger; + FakeProcessManager fakeProcessManager; setUpAll(() { Cache.disableLocking(); @@ -59,7 +64,7 @@ void main() { setUp(() { fileSystem = MemoryFileSystem.test(); usage = TestUsage(); - logger = BufferLogger.test(); + fakeProcessManager = FakeProcessManager.empty(); }); // Sets up the minimal mock project files necessary to look like a Flutter project. @@ -127,21 +132,33 @@ void main() { ); } - const FakeCommand exportArchiveCommand = FakeCommand( - command: [ - 'xcrun', - 'xcodebuild', - '-exportArchive', - '-allowProvisioningDeviceRegistration', - '-allowProvisioningUpdates', - '-archivePath', - '/build/ios/archive/Runner.xcarchive', - '-exportPath', - '/build/ios/ipa', - '-exportOptionsPlist', - '/ExportOptions.plist' - ], - ); + FakeCommand _exportArchiveCommand({ + String exportOptionsPlist = '/ExportOptions.plist', + File cachePlist, + }) { + return FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-exportArchive', + '-allowProvisioningDeviceRegistration', + '-allowProvisioningUpdates', + '-archivePath', + '/build/ios/archive/Runner.xcarchive', + '-exportPath', + '/build/ios/ipa', + '-exportOptionsPlist', + exportOptionsPlist, + ], + onRun: () { + // exportOptionsPlist will be cleaned up within the command. + // Save it somewhere else so test expectations can be run on it. + if (cachePlist != null) { + cachePlist.writeAsStringSync(fileSystem.file(_exportOptionsPlist).readAsStringSync()); + } + } + ); + } testUsingContext('ipa build fails when there is no ios project', () async { final BuildCommand command = BuildCommand(); @@ -235,37 +252,167 @@ void main() { FakeXcodeProjectInterpreterWithBuildSettings(), }); - testUsingContext('ipa build invokes xcode build', () async { + testUsingContext('ipa build fails when --export-options-plist and --export-method are used together', () async { final BuildCommand command = BuildCommand(); _createMinimalMockProjectFiles(); + await expectToolExitLater( + createTestCommandRunner(command).run([ + 'build', + 'ipa', + '--export-options-plist', + 'ExportOptions.plist', + '--export-method', + 'app-store', + '--no-pub', + ]), + contains('"--export-options-plist" is not compatible with "--export-method"'), + ); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => + FakeXcodeProjectInterpreterWithBuildSettings(), + }); + + testUsingContext('ipa build invokes xcodebuild and archives for app store', () async { + final File cachedExportOptionsPlist = fileSystem.file('/CachedExportOptions.plist'); + final BuildCommand command = BuildCommand(); + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(), + _exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist, cachePlist: cachedExportOptionsPlist), + ]); + _createMinimalMockProjectFiles(); + await createTestCommandRunner(command).run( const ['build', 'ipa', '--no-pub'] ); + + const String expectedIpaPlistContents = ''' + + + + + method + app-store + uploadBitcode + + + +'''; + + final String actualIpaPlistContents = fileSystem.file(cachedExportOptionsPlist).readAsStringSync(); + expect(actualIpaPlistContents, expectedIpaPlistContents); + expect(testLogger.statusText, contains('build/ios/archive/Runner.xcarchive')); + expect(testLogger.statusText, contains('Building App Store IPA')); + expect(testLogger.statusText, contains('Built IPA to /build/ios/ipa')); + expect(testLogger.statusText, contains('To upload to the App Store')); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(), - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); + testUsingContext('ipa build invokes xcodebuild and archives for ad-hoc distribution', () async { + final File cachedExportOptionsPlist = fileSystem.file('/CachedExportOptions.plist'); + final BuildCommand command = BuildCommand(); + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(), + _exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist, cachePlist: cachedExportOptionsPlist), + ]); + _createMinimalMockProjectFiles(); + + await createTestCommandRunner(command).run( + const ['build', 'ipa', '--no-pub', '--export-method', 'ad-hoc'] + ); + + const String expectedIpaPlistContents = ''' + + + + + method + ad-hoc + uploadBitcode + + + +'''; + + final String actualIpaPlistContents = fileSystem.file(cachedExportOptionsPlist).readAsStringSync(); + expect(actualIpaPlistContents, expectedIpaPlistContents); + + expect(testLogger.statusText, contains('build/ios/archive/Runner.xcarchive')); + expect(testLogger.statusText, contains('Building ad-hoc IPA')); + expect(testLogger.statusText, contains('Built IPA to /build/ios/ipa')); + // Don't instruct how to upload to the App Store. + expect(testLogger.statusText, isNot(contains('To upload'))); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => fakeProcessManager, + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + }); + + testUsingContext('ipa build invokes xcodebuild and archives with bitcode on', () async { + final File cachedExportOptionsPlist = fileSystem.file('/CachedExportOptions.plist'); + final BuildCommand command = BuildCommand(); + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(), + _exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist, cachePlist: cachedExportOptionsPlist), + ]); + _createMinimalMockProjectFiles(); + + await createTestCommandRunner(command).run( + const ['build', 'ipa', '--no-pub',] + ); + + const String expectedIpaPlistContents = ''' + + + + + method + app-store + + +'''; + + final String actualIpaPlistContents = fileSystem.file(cachedExportOptionsPlist).readAsStringSync(); + expect(actualIpaPlistContents, expectedIpaPlistContents); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => fakeProcessManager, + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings( + overrides: {'ENABLE_BITCODE': 'YES'}, + ), + }); + testUsingContext('ipa build invokes xcode build with verbosity', () async { final BuildCommand command = BuildCommand(); + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(verbose: true), + _exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist), + ]); _createMinimalMockProjectFiles(); await createTestCommandRunner(command).run( const ['build', 'ipa', '--no-pub', '-v'] ); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(verbose: true), - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); @@ -295,19 +442,7 @@ void main() { fileSystem.file('build/ios/archive/Runner.xcarchive/Products/Applications/Runner.app/Frameworks/App.framework/App') ..createSync(recursive: true) ..writeAsBytesSync(List.generate(10000, (int index) => 0)); - - await createTestCommandRunner(command).run( - const ['build', 'ipa', '--no-pub', '--analyze-size'] - ); - - expect(testLogger.statusText, contains('A summary of your iOS bundle analysis can be found at')); - expect(testLogger.statusText, contains('flutter pub global activate devtools; flutter pub global run devtools --appSizeBase=')); - expect(usage.events, contains( - const TestUsageEvent('code-size-analysis', 'ios'), - )); - }, overrides: { - FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ + fakeProcessManager.addCommands([ xattrCommand, _setUpFakeXcodeBuildHandler(onRun: () { fileSystem.file('build/flutter_size_01/snapshot.arm64.json') @@ -325,19 +460,39 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync('{}'); }), - ]), + _exportArchiveCommand(exportOptionsPlist: _exportOptionsPlist), + ]); + + await createTestCommandRunner(command).run( + const ['build', 'ipa', '--no-pub', '--analyze-size'] + ); + + expect(testLogger.statusText, contains('A summary of your iOS bundle analysis can be found at')); + expect(testLogger.statusText, contains('flutter pub global activate devtools; flutter pub global run devtools --appSizeBase=')); + expect(usage.events, contains( + const TestUsageEvent('code-size-analysis', 'ios'), + )); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, FileSystemUtils: () => FileSystemUtils(fileSystem: fileSystem, platform: macosPlatform), Usage: () => usage, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); - testUsingContext('ipa build invokes xcode build export archive', () async { + testUsingContext('ipa build invokes xcode build export archive when passed plist', () async { final String outputPath = fileSystem.path.absolute(fileSystem.path.join('build', 'ios', 'ipa')); final File exportOptions = fileSystem.file('ExportOptions.plist') ..createSync(); final BuildCommand command = BuildCommand(); + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(), + _exportArchiveCommand(), + ]); _createMinimalMockProjectFiles(); await createTestCommandRunner(command).run( @@ -350,23 +505,25 @@ void main() { ], ); - expect(logger.statusText, contains('Built IPA to $outputPath.')); + expect(testLogger.statusText, contains('Built IPA to $outputPath.')); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(), - exportArchiveCommand, - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, - Logger: () => logger, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); testUsingContext('Trace error if xcresult is empty.', () async { final BuildCommand command = BuildCommand(); - + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { + fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + }), + _setUpXCResultCommand(), + ]); _createMinimalMockProjectFiles(); await expectLater( @@ -375,22 +532,23 @@ void main() { ); expect(testLogger.traceText, contains('xcresult parser: Unrecognized top level json format.')); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); - }), - _setUpXCResultCommand(), - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); testUsingContext('Display xcresult issues on console if parsed.', () async { final BuildCommand command = BuildCommand(); - + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { + fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + }), + _setUpXCResultCommand(stdout: kSampleResultJsonWithIssues), + ]); _createMinimalMockProjectFiles(); await expectLater( @@ -400,22 +558,23 @@ void main() { expect(testLogger.errorText, contains("Use of undeclared identifier 'asdas'")); expect(testLogger.errorText, contains('/Users/m/Projects/test_create/ios/Runner/AppDelegate.m:7:56')); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); - }), - _setUpXCResultCommand(stdout: kSampleResultJsonWithIssues), - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); testUsingContext('Do not display xcresult issues that needs to be discarded.', () async { final BuildCommand command = BuildCommand(); - + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { + fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + }), + _setUpXCResultCommand(stdout: kSampleResultJsonWithIssuesToBeDiscarded), + ]); _createMinimalMockProjectFiles(); await expectLater( @@ -427,22 +586,20 @@ void main() { expect(testLogger.errorText, contains('/Users/m/Projects/test_create/ios/Runner/AppDelegate.m:7:56')); expect(testLogger.errorText, isNot(contains('Command PhaseScriptExecution failed with a nonzero exit code'))); expect(testLogger.warningText, isNot(contains("The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99."))); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); - }), - _setUpXCResultCommand(stdout: kSampleResultJsonWithIssuesToBeDiscarded), - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); testUsingContext('Trace if xcresult bundle does not exist.', () async { final BuildCommand command = BuildCommand(); - + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(exitCode: 1), + ]); _createMinimalMockProjectFiles(); await expectLater( @@ -451,13 +608,10 @@ void main() { ); expect(testLogger.traceText, contains('The xcresult bundle are not generated. Displaying xcresult is disabled.')); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(exitCode: 1), - _setUpXCResultCommand(stdout: kSampleResultJsonWithIssues), - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); @@ -465,7 +619,13 @@ void main() { testUsingContext('Extra error message for provision profile issue in xcresulb bundle.', () async { final BuildCommand command = BuildCommand(); - + fakeProcessManager.addCommands([ + xattrCommand, + _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { + fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); + }), + _setUpXCResultCommand(stdout: kSampleResultJsonWithProvisionIssue), + ]); _createMinimalMockProjectFiles(); await expectLater( @@ -478,18 +638,14 @@ void main() { expect(testLogger.errorText, contains('Verify that the Bundle Identifier in your project is your signing id in Xcode')); expect(testLogger.errorText, contains('open ios/Runner.xcworkspace')); expect(testLogger.errorText, contains("Also try selecting 'Product > Build' to fix the problem:")); + expect(fakeProcessManager, hasNoRemainingExpectations); }, overrides: { FileSystem: () => fileSystem, - ProcessManager: () => FakeProcessManager.list([ - xattrCommand, - _setUpFakeXcodeBuildHandler(exitCode: 1, onRun: () { - fileSystem.systemTempDirectory.childDirectory(_xcBundleFilePath).createSync(); - }), - _setUpXCResultCommand(stdout: kSampleResultJsonWithProvisionIssue), - ]), + ProcessManager: () => fakeProcessManager, Platform: () => macosPlatform, XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); } const String _xcBundleFilePath = '/.tmp_rand0/flutter_ios_build_temp_dirrand0/temporary_xcresult_bundle'; +const String _exportOptionsPlist = '/.tmp_rand0/flutter_build_ios.rand0/ExportOptions.plist';