diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 57c629a4b13..e783f793b9e 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -23,6 +23,7 @@ import '../darwin/darwin.dart'; import '../doctor_validator.dart'; import '../globals.dart' as globals; import '../ios/application_package.dart'; +import '../ios/code_signing.dart'; import '../ios/mac.dart'; import '../ios/plist_parser.dart'; import '../runner/flutter_command.dart'; @@ -532,7 +533,28 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { final String exportMethodDisplayName = isAppStoreUpload ? 'App Store' : exportMethod; status = globals.logger.startProgress('Building $exportMethodDisplayName IPA...'); if (exportOptions == null) { - generatedExportPlist = _createExportPlist(exportMethod); + final Map? buildSettings = await app.project.buildSettingsForBuildInfo( + buildInfo, + ); + // Create XcodeCodeSigningSettings for dependency injection into createExportPlist + final codeSigningSettings = XcodeCodeSigningSettings( + config: globals.config, + logger: logger, + platform: globals.platform, + processUtils: globals.processUtils, + fileSystem: globals.fs, + fileSystemUtils: globals.fsUtils, + terminal: globals.terminal, + plistParser: globals.plistParser, + ); + generatedExportPlist = await createExportPlist( + exportMethod: exportMethod, + app: app, + buildInfo: buildInfo, + buildSettings: buildSettings, + fileSystem: globals.fs, + codeSigningSettings: codeSigningSettings, + ); exportOptions = generatedExportPlist.path; } @@ -618,8 +640,88 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { return FlutterCommandResult.success(); } - File _createExportPlist(String exportMethod) { - // Create the plist to be passed into xcodebuild -exportOptionsPlist. + /// Generates an ExportOptions.plist for use with `xcodebuild -exportArchive`. + /// + /// For manual signing configurations, generates an enhanced plist with complete signing + /// configuration. Otherwise, generates a simple plist with just the export method. + /// + /// **Enhancement conditions:** + /// - `CODE_SIGN_STYLE=Manual` is detected (manual provisioning profile signing) + /// - Build mode is Release or Profile (production builds only, skips Debug to avoid overhead) + /// - A provisioning profile can be located and parsed + /// + /// **Known limitations:** + /// - Currently only supports single-target apps (main app bundle ID only) + /// - TODO: Handle multi-target apps with extensions/widgets (requires multiple bundle IDs + /// mapped to their respective provisioning profiles in the provisioningProfiles dict) + /// + /// **Fallback behavior:** + /// - For Automatic signing: Let Xcode handle the export options (simple plist) + /// - For Debug builds: Skip enhancement (not needed for App Store export) + /// - If profile lookup fails: Use simple plist and log trace message + /// - If required build settings missing: Use simple plist + /// + /// Returns an enhanced plist with `teamID`, `signingStyle`, and `provisioningProfiles` + /// mapping when conditions are met, otherwise returns a simple plist with just `method`. + Future createExportPlist({ + required String exportMethod, + required BuildableIOSApp app, + required BuildInfo buildInfo, + required Map? buildSettings, + required FileSystem fileSystem, + XcodeCodeSigningSettings? codeSigningSettings, + FileSystemUtils? fileSystemUtils, + }) async { + final String? codeSignStyle = buildSettings?['CODE_SIGN_STYLE']; + final isManualSigning = codeSignStyle == 'Manual'; + + // Only enhance for manual signing in Release/Profile builds + // (skip for Debug to avoid overhead during development workflow) + if (isManualSigning && + (buildInfo.mode == BuildMode.release || buildInfo.mode == BuildMode.profile)) { + final String? profileSpecifier = buildSettings?['PROVISIONING_PROFILE_SPECIFIER']; + final String? teamId = buildSettings?['DEVELOPMENT_TEAM']; + final String bundleId = buildSettings?['PRODUCT_BUNDLE_IDENTIFIER'] ?? app.id; + + if (profileSpecifier != null && teamId != null) { + // Try to find and parse the provisioning profile + final String? profileUuid = await _findProvisioningProfileUuid( + profileSpecifier, + codeSigningSettings: codeSigningSettings, + fileSystem: fileSystem, + fileSystemUtils: fileSystemUtils ?? globals.fsUtils, + ); + + if (profileUuid != null) { + // Generate enhanced ExportOptions.plist for manual signing + logger.printTrace( + 'Detected manual code signing. Generated ExportOptions.plist with ' + 'teamID, signingStyle=manual, and provisioningProfiles for $bundleId.', + ); + return _createManualSigningExportPlist( + exportMethod: exportMethod, + teamId: teamId, + bundleId: bundleId, + profileUuid: profileUuid, + fileSystem: fileSystem, + ); + } + // Note: If profile lookup fails, we fall back to simple plist. + // Trace-level logging is already emitted by _findProvisioningProfileUuid. + logger.printTrace( + 'Manual signing detected but no matching provisioning profile UUID was found ' + 'for $bundleId. Falling back to default ExportOptions.plist. ' + 'If exportArchive fails with provisioning profile errors, consider supplying ' + '--export-options-plist manually.', + ); + } + } + + // Fall back to simple plist (current behavior) + return _createSimpleExportPlist(exportMethod: exportMethod, fileSystem: fileSystem); + } + + File _createSimpleExportPlist({required String exportMethod, required FileSystem fileSystem}) { final plistContents = StringBuffer(''' @@ -633,7 +735,7 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { '''); - final File tempPlist = globals.fs.systemTempDirectory + final File tempPlist = fileSystem.systemTempDirectory .createTempSync('flutter_build_ios.') .childFile('ExportOptions.plist'); tempPlist.writeAsStringSync(plistContents.toString()); @@ -641,6 +743,132 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { return tempPlist; } + File _createManualSigningExportPlist({ + required String exportMethod, + required String teamId, + required String bundleId, + required String profileUuid, + required FileSystem fileSystem, + }) { + // Note: uploadBitcode=false and stripSwiftSymbols=true are conservative defaults + // that work for most App Store distribution scenarios. These values are not currently + // configurable, but could be made configurable in a future enhancement if needed. + final plistContents = StringBuffer(''' + + + + + method + $exportMethod + teamID + $teamId + signingStyle + manual + provisioningProfiles + + $bundleId + $profileUuid + + uploadBitcode + + stripSwiftSymbols + + + +'''); + + final File tempPlist = fileSystem.systemTempDirectory + .createTempSync('flutter_build_ios.') + .childFile('ExportOptions.plist'); + tempPlist.writeAsStringSync(plistContents.toString()); + + return tempPlist; + } + + /// Searches the provisioning profiles directory for a profile matching [profileSpecifier]. + /// + /// The [profileSpecifier] can be either: + /// - A provisioning profile UUID (e.g., "12345678-1234-1234-1234-123456789012") + /// - A provisioning profile name (e.g., "MyApp Distribution") + /// + /// **Search strategy:** + /// - Iterates through all `.mobileprovision` files in the standard Xcode profiles directory + /// - Decodes each profile once to extract both UUID and name (performance optimization) + /// - Matches against both UUID and name to handle either specification format + /// - Returns the UUID of the first matching profile + /// + /// **Edge cases handled:** + /// - Home directory not available (CI/CD environments) - logs trace and returns null + /// - Provisioning profiles directory doesn't exist - logs trace and returns null + /// - Profile file parsing fails - logs trace and continues searching other profiles + /// - Multiple profiles with same name - returns first match (profile parse order dependent) + /// - Profile not found - logs trace with helpful context and returns null + /// + /// Returns the UUID of the matching profile, or null if not found or parsing fails. + Future _findProvisioningProfileUuid( + String profileSpecifier, { + XcodeCodeSigningSettings? codeSigningSettings, + required FileSystem fileSystem, + required FileSystemUtils fileSystemUtils, + }) async { + final Directory? profileDirectory = getProvisioningProfileDirectory( + fileSystemUtils: fileSystemUtils, + fileSystem: fileSystem, + ); + if (profileDirectory == null) { + logger.printTrace( + 'Manual signing: provisioning profile "$profileSpecifier" not found. ' + 'Home directory not available. Falling back to basic ExportOptions.plist.', + ); + return null; + } + + if (!profileDirectory.existsSync()) { + logger.printTrace( + 'Manual signing: provisioning profile "$profileSpecifier" not found. ' + 'Provisioning Profiles directory does not exist at: ${profileDirectory.path}. ' + 'Falling back to basic ExportOptions.plist.', + ); + return null; + } + + // Use provided or create new XcodeCodeSigningSettings instance + final XcodeCodeSigningSettings settings = + codeSigningSettings ?? + XcodeCodeSigningSettings( + config: globals.config, + logger: logger, + platform: globals.platform, + processUtils: globals.processUtils, + fileSystem: globals.fs, + fileSystemUtils: globals.fsUtils, + terminal: globals.terminal, + plistParser: globals.plistParser, + ); + + // Search for profiles matching the specifier (could be name or UUID) + await for (final FileSystemEntity entity in profileDirectory.list()) { + if (entity is! File || fileSystem.path.extension(entity.path) != '.mobileprovision') { + continue; + } + + // Decode profile once and extract both UUID and name + final ProvisioningProfile? profile = await settings.parseProvisioningProfile(entity); + if (profile != null) { + // Check if this profile matches the specifier (by UUID or name) + if (profile.uuid == profileSpecifier || profile.name == profileSpecifier) { + return profile.uuid; + } + } + } + + logger.printTrace( + 'Manual signing: provisioning profile "$profileSpecifier" not found in ${profileDirectory.path}. ' + 'Falling back to basic ExportOptions.plist.', + ); + return null; + } + // As of Xcode 15.4, the old export methods 'app-store', 'ad-hoc', and 'development' // are now deprecated. The new equivalents are 'app-store-connect', 'release-testing', // and 'debugging'. diff --git a/packages/flutter_tools/lib/src/ios/code_signing.dart b/packages/flutter_tools/lib/src/ios/code_signing.dart index 56c77cfa598..7dfbc935666 100644 --- a/packages/flutter_tools/lib/src/ios/code_signing.dart +++ b/packages/flutter_tools/lib/src/ios/code_signing.dart @@ -225,6 +225,32 @@ Future getCodeSigningIdentityDevelopmentTeam({ return buildSettings?[_developmentTeamBuildSettingName]; } +/// Returns the standard Provisioning Profiles directory, or null if home directory is unavailable. +/// +/// Provisioning profiles are stored in `~/Library/Developer/Xcode/UserData/Provisioning Profiles/` +/// by Xcode. This function constructs the path to this directory. +/// +/// Returns null if the home directory cannot be determined (e.g., in some CI/CD environments). +Directory? getProvisioningProfileDirectory({ + required FileSystemUtils fileSystemUtils, + required FileSystem fileSystem, +}) { + final String? homeDir = fileSystemUtils.homeDirPath; + if (homeDir == null) { + return null; + } + return fileSystem.directory( + fileSystem.path.join( + homeDir, + 'Library', + 'Developer', + 'Xcode', + 'UserData', + 'Provisioning Profiles', + ), + ); +} + class XcodeCodeSigningSettings { XcodeCodeSigningSettings({ required Config config, @@ -330,7 +356,7 @@ class XcodeCodeSigningSettings { if (automaticCodeSignStyleOnly) { return null; } - final _ProvisioningProfile? validatedProfile = await _validateSavedProfile( + final ProvisioningProfile? validatedProfile = await _validateSavedProfile( savedProfile, validCodeSigningIdentities, ); @@ -385,7 +411,7 @@ class XcodeCodeSigningSettings { _config.setValue(kConfigCodeSignCertificate, identity); } - void _saveProvisioningProfile(_ProvisioningProfile profile) { + void _saveProvisioningProfile(ProvisioningProfile profile) { _logger.printStatus('Provisioning Profile "${profile.name}" saved.'); _config.setValue(kConfigCodeSignProvisioningProfile, profile.filePath); } @@ -439,7 +465,7 @@ class XcodeCodeSigningSettings { /// valid identity/certificate for the profile. /// /// Returns null if profile cannot be found, parsed, or validated. - Future<_ProvisioningProfile?> _validateSavedProfile( + Future _validateSavedProfile( String savedProfilePath, List validCodeSigningIdentities, ) async { @@ -448,7 +474,7 @@ class XcodeCodeSigningSettings { _logger.printError('Unable to find saved provisioning profile $savedProfilePath'); return null; } - final _ProvisioningProfile? parsedProfile = await _parseProvisioningProfile(savedProfile); + final ProvisioningProfile? parsedProfile = await parseProvisioningProfile(savedProfile); if (parsedProfile == null) { return null; } @@ -465,8 +491,11 @@ class XcodeCodeSigningSettings { } /// Decode and convert a .mobileprovision file to a .plist file and then - /// parse the .plist into [_ProvisioningProfile]. - Future<_ProvisioningProfile?> _parseProvisioningProfile(File provisioningProfileFile) async { + /// parse the .plist into [ProvisioningProfile]. + /// + /// This is a public method that can be used by other parts of flutter_tools + /// to parse provisioning profiles without pulling in certificate validation logic. + Future parseProvisioningProfile(File provisioningProfileFile) async { final Directory profilesDirectory = _fileSystem.systemTempDirectory.childDirectory( 'provisioning_profiles', ); @@ -494,7 +523,7 @@ class XcodeCodeSigningSettings { } try { final Map contents = _plistParser.parseFile(decodedProfile.path); - return _ProvisioningProfile.fromPlist( + return ProvisioningProfile.fromPlist( provisioningProfileFile.path, contents, fileSystem: _fileSystem, @@ -667,7 +696,7 @@ class XcodeCodeSigningSettings { return; } } else if (style == _CodeSigningStyle.manual) { - final List<_ProvisioningProfile> validProvisioningProfiles = await _getProvisioningProfiles(); + final List validProvisioningProfiles = await _getProvisioningProfiles(); if (validProvisioningProfiles.isEmpty) { _logger.printError( 'No provisioning profiles were found. To learn how to create or download ' @@ -678,7 +707,7 @@ class XcodeCodeSigningSettings { _logger.printWarning(_codeSignSelectionCanceled); return; } - final _ProvisioningProfile? profile = await _selectProvisioningProfile( + final ProvisioningProfile? profile = await _selectProvisioningProfile( validProvisioningProfiles, ); if (profile == null) { @@ -776,34 +805,24 @@ class XcodeCodeSigningSettings { /// Get list of provisioning profiles from `~/Library/Developer/Xcode/UserData/Provisioning\ Profiles`. /// /// Only return non-Xcode-managed profiles with matching valid identities. - Future> _getProvisioningProfiles() async { - final String? homeDir = _fileSystemUtils.homeDirPath; - if (homeDir == null) { - return <_ProvisioningProfile>[]; - } - final Directory profileDirectory = _fileSystem.directory( - _fileSystem.path.join( - homeDir, - 'Library', - 'Developer', - 'Xcode', - 'UserData', - 'Provisioning Profiles', - ), + Future> _getProvisioningProfiles() async { + final Directory? profileDirectory = getProvisioningProfileDirectory( + fileSystemUtils: _fileSystemUtils, + fileSystem: _fileSystem, ); - if (!profileDirectory.existsSync()) { - return <_ProvisioningProfile>[]; + if (profileDirectory == null || !profileDirectory.existsSync()) { + return []; } final List validCodeSigningIdentities = await _getSigningIdentities(); - final profiles = <_ProvisioningProfile>[]; + final profiles = []; for (final FileSystemEntity entity in profileDirectory.listSync()) { if (entity is! File || _fileSystem.path.extension(entity.path) != '.mobileprovision') { continue; } - final _ProvisioningProfile? profile = await _parseProvisioningProfile(entity); + final ProvisioningProfile? profile = await parseProvisioningProfile(entity); // Xcode managed profiles can't be used for manual code-signing. final bool? isXcodeManaged = profile?.isXcodeManaged; @@ -823,8 +842,8 @@ class XcodeCodeSigningSettings { } /// Prompt the user to select from list of [validatedProfiles]. - Future<_ProvisioningProfile?> _selectProvisioningProfile( - List<_ProvisioningProfile> validatedProfiles, + Future _selectProvisioningProfile( + List validatedProfiles, ) async { if (validatedProfiles.isEmpty) { return null; @@ -875,17 +894,18 @@ enum _CodeSigningStyle { final String label; } -class _ProvisioningProfile { - _ProvisioningProfile({ +class ProvisioningProfile { + ProvisioningProfile({ required this.filePath, required this.name, + required this.uuid, required this.teamIdentifier, required this.expirationDate, required this.developerCertificates, this.isXcodeManaged, }); - factory _ProvisioningProfile.fromPlist( + factory ProvisioningProfile.fromPlist( String filePath, Map data, { required FileSystem fileSystem, @@ -936,9 +956,10 @@ class _ProvisioningProfile { } final DateTime expirationDate = DateTime.parse(expirationDateString); - return _ProvisioningProfile( + return ProvisioningProfile( filePath: filePath, name: name, + uuid: uuid, developerCertificates: certificateFiles, isXcodeManaged: data['IsXcodeManaged'] is bool? ? data['IsXcodeManaged'] as bool? : null, expirationDate: expirationDate, @@ -948,6 +969,7 @@ class _ProvisioningProfile { final String filePath; final String name; + final String uuid; final String teamIdentifier; final DateTime expirationDate; final List developerCertificates; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_export_plist_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_export_plist_test.dart new file mode 100644 index 00000000000..b72cb8fdb98 --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ipa_export_plist_test.dart @@ -0,0 +1,223 @@ +// 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. + +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/build_info.dart'; +import 'package:flutter_tools/src/commands/build_ios.dart'; +import 'package:flutter_tools/src/ios/application_package.dart'; +import 'package:flutter_tools/src/ios/code_signing.dart'; +import 'package:flutter_tools/src/project.dart'; + +import '../../src/common.dart'; + +void main() { + group('ExportOptions.plist generation for manual signing', () { + late FakeXcodeCodeSigningSettings fakeCodeSigningSettings; + + setUp(() { + fakeCodeSigningSettings = FakeXcodeCodeSigningSettings(); + }); + + test('generates simple plist for automatic signing', () async { + final fileSystem = MemoryFileSystem.test(); + final command = BuildIOSArchiveCommand(logger: BufferLogger.test(), verboseHelp: false); + + final File plistFile = await command.createExportPlist( + exportMethod: 'app-store', + app: FakeBuildableIOSApp(FakeIosProject()), + buildInfo: BuildInfo.release, + buildSettings: const {'CODE_SIGN_STYLE': 'Automatic'}, + fileSystem: fileSystem, + codeSigningSettings: fakeCodeSigningSettings, + ); + + final String plistContent = plistFile.readAsStringSync(); + expect(plistContent, contains('method')); + expect(plistContent, contains('app-store')); + expect(plistContent, isNot(contains('teamID'))); + }); + + test('falls back to simple plist when profile UUID cannot be found', () async { + final fileSystem = MemoryFileSystem.test(); + final command = BuildIOSArchiveCommand(logger: BufferLogger.test(), verboseHelp: false); + + // Even with manual signing, if we can't find a profile UUID, we fall back to simple plist + final File plistFile = await command.createExportPlist( + exportMethod: 'app-store', + app: FakeBuildableIOSApp(FakeIosProject()), + buildInfo: BuildInfo.release, + buildSettings: const { + 'CODE_SIGN_STYLE': 'Manual', + 'DEVELOPMENT_TEAM': 'ABC123DEF4', + 'PRODUCT_BUNDLE_IDENTIFIER': 'com.example.myapp', + 'PROVISIONING_PROFILE_SPECIFIER': 'MyProfile', + }, + fileSystem: fileSystem, + codeSigningSettings: fakeCodeSigningSettings, // Returns null - no profile found + ); + + final String plistContent = plistFile.readAsStringSync(); + // Should fall back to simple plist when profile UUID can't be determined + expect(plistContent, contains('method')); + expect(plistContent, contains('app-store')); + expect(plistContent, isNot(contains('teamID'))); + }); + + test('does not enhance plist for debug builds with manual signing', () async { + final fileSystem = MemoryFileSystem.test(); + final command = BuildIOSArchiveCommand(logger: BufferLogger.test(), verboseHelp: false); + + final File plistFile = await command.createExportPlist( + exportMethod: 'app-store', + app: FakeBuildableIOSApp(FakeIosProject()), + buildInfo: BuildInfo.debug, + buildSettings: const {'CODE_SIGN_STYLE': 'Manual', 'DEVELOPMENT_TEAM': 'ABC123DEF4'}, + fileSystem: fileSystem, + codeSigningSettings: fakeCodeSigningSettings, + ); + + final String plistContent = plistFile.readAsStringSync(); + expect(plistContent, isNot(contains('teamID'))); + }); + + test('handles null buildSettings gracefully', () async { + final fileSystem = MemoryFileSystem.test(); + final command = BuildIOSArchiveCommand(logger: BufferLogger.test(), verboseHelp: false); + + final File plistFile = await command.createExportPlist( + exportMethod: 'app-store', + app: FakeBuildableIOSApp(FakeIosProject()), + buildInfo: BuildInfo.release, + buildSettings: null, + fileSystem: fileSystem, + codeSigningSettings: fakeCodeSigningSettings, + ); + + final String plistContent = plistFile.readAsStringSync(); + expect(plistContent, contains('method')); + expect(plistContent, isNot(contains('teamID'))); + }); + + test('generates enhanced plist for manual signing when profile is found', () async { + final fileSystem = MemoryFileSystem.test(); + final command = BuildIOSArchiveCommand(logger: BufferLogger.test(), verboseHelp: false); + + // Set up home directory path for the MemoryFileSystem + final String homeDir = fileSystem.currentDirectory.path; + final fakeFileSystemUtils = FakeFileSystemUtils(homeDirPath: homeDir); + + // Create the provisioning profiles directory structure in the memory filesystem. + // _findProvisioningProfileUuid iterates over files in this directory. + final Directory provisioningProfilesDir = fileSystem.directory( + fileSystem.path.join( + homeDir, + 'Library', + 'Developer', + 'Xcode', + 'UserData', + 'Provisioning Profiles', + ), + ); + provisioningProfilesDir.createSync(recursive: true); + + // Create a dummy provisioning profile file for the fake to "parse" + final File dummyProfileFile = provisioningProfilesDir.childFile( + 'MyDistProfile.mobileprovision', + ); + dummyProfileFile.writeAsStringSync('dummy content'); + + // Configure fake to return a valid provisioning profile when parseProvisioningProfile is called + final fakeWithProfile = FakeXcodeCodeSigningSettings( + profileToReturn: ProvisioningProfile( + filePath: dummyProfileFile.path, + name: 'MyDistProfile', + uuid: '12345678-1234-1234-1234-123456789012', + teamIdentifier: 'ABC123DEF4', + expirationDate: DateTime.now().add(const Duration(days: 365)), + developerCertificates: [], + ), + ); + + final File plistFile = await command.createExportPlist( + exportMethod: 'app-store', + app: FakeBuildableIOSApp(FakeIosProject()), + buildInfo: BuildInfo.release, + buildSettings: const { + 'CODE_SIGN_STYLE': 'Manual', + 'DEVELOPMENT_TEAM': 'ABC123DEF4', + 'PRODUCT_BUNDLE_IDENTIFIER': 'com.example.myapp', + 'PROVISIONING_PROFILE_SPECIFIER': 'MyDistProfile', + }, + fileSystem: fileSystem, + codeSigningSettings: fakeWithProfile, + fileSystemUtils: fakeFileSystemUtils, + ); + + final String plistContent = plistFile.readAsStringSync(); + // Should contain enhanced manual signing configuration + expect(plistContent, contains('method')); + expect(plistContent, contains('app-store')); + expect(plistContent, contains('teamID')); + expect(plistContent, contains('ABC123DEF4')); + expect(plistContent, contains('signingStyle')); + expect(plistContent, contains('manual')); + expect(plistContent, contains('provisioningProfiles')); + expect(plistContent, contains('com.example.myapp')); + expect(plistContent, contains('12345678-1234-1234-1234-123456789012')); + }); + }); +} + +// Fake implementation for testing dependency injection - simplified without Fake base class +class FakeXcodeCodeSigningSettings implements XcodeCodeSigningSettings { + FakeXcodeCodeSigningSettings({this.profileToReturn}); + + final ProvisioningProfile? profileToReturn; + + @override + Future parseProvisioningProfile(File provisioningProfileFile) async { + return profileToReturn; + } + + @override + Future selectSettings() async { + // No-op for test cases - no interactive selection needed + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeIosProject implements IosProject { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeBuildableIOSApp implements BuildableIOSApp { + FakeBuildableIOSApp(this.project); + + @override + final IosProject project; + + @override + String get id => 'com.example.app'; + + @override + String get name => 'app'; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class FakeFileSystemUtils implements FileSystemUtils { + FakeFileSystemUtils({this.homeDirPath}); + + @override + final String? homeDirPath; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} 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 efd9f8c4846..4c70f07e3cf 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 @@ -32,12 +32,12 @@ import '../../src/test_flutter_command_runner.dart'; import '../../src/throwing_pub.dart'; class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInterpreter { - FakeXcodeProjectInterpreterWithBuildSettings({ - this.overrides = const {}, - Version? version, - }) : version = version ?? Version(14, 0, 0); + FakeXcodeProjectInterpreterWithBuildSettings({Map? overrides, Version? version}) + : _overrides = overrides ?? const {}, + version = version ?? Version(14, 0, 0); - final Map overrides; + final Map _overrides; + Map get overrides => _overrides; @override Future> getBuildSettings( String projectPath, { @@ -45,9 +45,9 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter Duration timeout = const Duration(minutes: 1), }) async { return { - ...overrides, 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject', 'DEVELOPMENT_TEAM': 'abc', + ...overrides, }; } @@ -111,6 +111,13 @@ void main() { fileSystem .file(fileSystem.path.join('ios', 'Runner.xcodeproj', 'project.pbxproj')) .createSync(); + // Create Flutter.xcodeproj directory for IosProject.existsSync() check + fileSystem + .directory(fileSystem.path.join('ios', 'Flutter', 'Flutter.xcodeproj')) + .createSync(recursive: true); + fileSystem + .file(fileSystem.path.join('ios', 'Flutter', 'Flutter.xcodeproj', 'project.pbxproj')) + .createSync(); final packageConfigPath = '${Cache.flutterRoot!}/packages/flutter_tools/.dart_tool/package_config.json'; fileSystem.file(packageConfigPath)