diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index 773c7356f42..6d43076175e 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -195,6 +195,11 @@ class XcodeProjectInterpreter { final String? configuration = buildContext.configuration; final String? target = buildContext.target; final String? deviceId = buildContext.deviceId; + final String buildDir = switch (buildContext.sdk) { + XcodeSdk.MacOSX => getMacOSBuildDirectory(), + XcodeSdk.IPhoneOS || XcodeSdk.IPhoneSimulator => getIosBuildDirectory(), + XcodeSdk.WatchOS || XcodeSdk.WatchSimulator => getIosBuildDirectory(), + }; final List showBuildSettingsCommand = [ ...xcrunCommand(), 'xcodebuild', @@ -206,21 +211,20 @@ class XcodeProjectInterpreter { ...['-configuration', configuration], if (target != null) ...['-target', target], - if (buildContext.environmentType == EnvironmentType.simulator) + if (buildContext.sdk == XcodeSdk.IPhoneSimulator || buildContext.sdk == XcodeSdk.WatchSimulator) ...['-sdk', 'iphonesimulator'], '-destination', - if (buildContext.isWatch && buildContext.environmentType == EnvironmentType.physical) - 'generic/platform=watchOS' - else if (buildContext.isWatch) - 'generic/platform=watchOS Simulator' - else if (deviceId != null) + if (deviceId != null) 'id=$deviceId' - else if (buildContext.environmentType == EnvironmentType.physical) - 'generic/platform=iOS' - else - 'generic/platform=iOS Simulator', + else switch (buildContext.sdk) { + XcodeSdk.IPhoneOS => 'generic/platform=iOS', + XcodeSdk.IPhoneSimulator => 'generic/platform=iOS Simulator', + XcodeSdk.MacOSX => 'generic/platform=macOS', + XcodeSdk.WatchOS => 'generic/platform=watchOS', + XcodeSdk.WatchSimulator => 'generic/platform=watchOS Simulator', + }, '-showBuildSettings', - 'BUILD_DIR=${_fileSystem.path.absolute(getIosBuildDirectory())}', + 'BUILD_DIR=${_fileSystem.path.absolute(buildDir)}', ...environmentVariablesAsXcodeBuildSettings(_platform), ]; try { @@ -238,14 +242,19 @@ class XcodeProjectInterpreter { return parseXcodeBuildSettings(out); } on Exception catch (error) { if (error is ProcessException && error.toString().contains('timed out')) { + final String eventType = switch (buildContext.sdk) { + XcodeSdk.MacOSX => 'macos', + XcodeSdk.IPhoneOS || XcodeSdk.IPhoneSimulator => 'ios', + XcodeSdk.WatchOS || XcodeSdk.WatchSimulator => 'watchos', + }; BuildEvent('xcode-show-build-settings-timeout', - type: 'ios', + type: eventType, command: showBuildSettingsCommand.join(' '), flutterUsage: _usage, ).send(); _analytics.send(Event.flutterBuildInfo( label: 'xcode-show-build-settings-timeout', - buildType: 'ios', + buildType: eventType, command: showBuildSettingsCommand.join(' '), )); } @@ -394,26 +403,40 @@ String substituteXcodeVariables(String str, Map xcodeBuildSettin return str.replaceAllMapped(_varExpr, (Match m) => xcodeBuildSettings[m[1]!] ?? m[0]!); } +/// Xcode SDKs. Corresponds to undocumented Xcode SUPPORTED_PLATFORMS values. +/// Use `xcodebuild -showsdks` to get a list of SDKs installed on your machine. +enum XcodeSdk { + IPhoneOS, + IPhoneSimulator, + MacOSX, + WatchOS, + WatchSimulator, +} + @immutable class XcodeProjectBuildContext { const XcodeProjectBuildContext({ this.scheme, this.configuration, - this.environmentType = EnvironmentType.physical, + this.sdk = XcodeSdk.IPhoneOS, this.deviceId, this.target, - this.isWatch = false, }); final String? scheme; final String? configuration; - final EnvironmentType environmentType; + final XcodeSdk sdk; final String? deviceId; final String? target; - final bool isWatch; @override - int get hashCode => Object.hash(scheme, configuration, environmentType, deviceId, target); + int get hashCode => Object.hash( + scheme, + configuration, + sdk, + deviceId, + target, + ); @override bool operator ==(Object other) { @@ -421,12 +444,11 @@ class XcodeProjectBuildContext { return true; } return other is XcodeProjectBuildContext && - other.scheme == scheme && - other.configuration == configuration && - other.deviceId == deviceId && - other.environmentType == environmentType && - other.isWatch == isWatch && - other.target == target; + other.scheme == scheme && + other.configuration == configuration && + other.deviceId == deviceId && + other.sdk == sdk && + other.target == target; } } diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index ea4204c0bf3..4a13eda6fbd 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -28,6 +28,7 @@ import 'migrations/macos_deployment_target_migration.dart'; import 'migrations/nsapplicationmain_deprecation_migration.dart'; import 'migrations/remove_macos_framework_link_and_embedding_migration.dart'; import 'migrations/secure_restorable_state_migration.dart'; +import 'swift_package_manager.dart'; /// When run in -quiet mode, Xcode should only print from the underlying tasks to stdout. /// Passing this regexp to trace moves the stdout output to stderr. @@ -108,29 +109,6 @@ Future buildMacOS({ if (!flutterBuildDir.existsSync()) { flutterBuildDir.createSync(recursive: true); } - // Write configuration to an xconfig file in a standard location. - await updateGeneratedXcodeProperties( - project: flutterProject, - buildInfo: buildInfo, - targetOverride: targetOverride, - useMacOSConfig: true, - ); - - // TODO(vashworth): Call `SwiftPackageManager.updateMinimumDeployment` - // using MACOSX_DEPLOYMENT_TARGET once https://github.com/flutter/flutter/issues/146204 - // is fixed. - - await processPodsIfNeeded(flutterProject.macos, getMacOSBuildDirectory(), buildInfo.mode); - // If the xcfilelists do not exist, create empty version. - if (!flutterProject.macos.inputFileList.existsSync()) { - flutterProject.macos.inputFileList.createSync(recursive: true); - } - if (!flutterProject.macos.outputFileList.existsSync()) { - flutterProject.macos.outputFileList.createSync(recursive: true); - } - if (configOnly) { - return; - } final Directory xcodeProject = flutterProject.macos.xcodeProject; @@ -150,6 +128,44 @@ Future buildMacOS({ if (configuration == null) { throwToolExit('Unable to find expected configuration in Xcode project.'); } + + final Map buildSettings = await flutterProject.macos.buildSettingsForBuildInfo( + buildInfo, + scheme: scheme, + configuration: configuration, + ) ?? {}; + + // Write configuration to an xconfig file in a standard location. + await updateGeneratedXcodeProperties( + project: flutterProject, + buildInfo: buildInfo, + targetOverride: targetOverride, + useMacOSConfig: true, + ); + + if (flutterProject.usesSwiftPackageManager) { + final String? macOSDeploymentTarget = buildSettings['MACOSX_DEPLOYMENT_TARGET']; + if (macOSDeploymentTarget != null) { + SwiftPackageManager.updateMinimumDeployment( + platform: SupportedPlatform.macos, + project: flutterProject.macos, + deploymentTarget: macOSDeploymentTarget, + ); + } + } + + await processPodsIfNeeded(flutterProject.macos, getMacOSBuildDirectory(), buildInfo.mode); + // If the xcfilelists do not exist, create empty version. + if (!flutterProject.macos.inputFileList.existsSync()) { + flutterProject.macos.inputFileList.createSync(recursive: true); + } + if (!flutterProject.macos.outputFileList.existsSync()) { + flutterProject.macos.outputFileList.createSync(recursive: true); + } + if (configOnly) { + return; + } + // Run the Xcode build. final Stopwatch sw = Stopwatch()..start(); final Status status = globals.logger.startProgress( diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 375db04a3dc..85c135cf97d 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -155,6 +155,89 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path); } XcodeProjectInfo? _projectInfo; + + /// The build settings for the host app of this project, as a detached map. + /// + /// Returns null, if Xcode tooling is unavailable. + Future?> buildSettingsForBuildInfo( + BuildInfo? buildInfo, { + String? scheme, + String? configuration, + String? target, + EnvironmentType environmentType = EnvironmentType.physical, + String? deviceId, + bool isWatch = false, + }) async { + if (!existsSync()) { + return null; + } + final XcodeProjectInfo? info = await projectInfo(); + if (info == null) { + return null; + } + + scheme ??= info.schemeFor(buildInfo); + if (scheme == null) { + info.reportFlavorNotFoundAndExit(); + } + + configuration ??= (await projectInfo())?.buildConfigurationFor( + buildInfo, + scheme, + ); + + final XcodeSdk sdk = switch ((environmentType, this)) { + (EnvironmentType.physical, _) when isWatch => XcodeSdk.WatchOS, + (EnvironmentType.simulator, _) when isWatch => XcodeSdk.WatchSimulator, + (EnvironmentType.physical, IosProject _) => XcodeSdk.IPhoneOS, + (EnvironmentType.simulator, IosProject _) => XcodeSdk.WatchSimulator, + (EnvironmentType.physical, MacOSProject _) => XcodeSdk.MacOSX, + (_, _) => throw ArgumentError('Unsupported SDK') + }; + + return _buildSettingsForXcodeProjectBuildContext( + XcodeProjectBuildContext( + scheme: scheme, + configuration: configuration, + sdk: sdk, + target: target, + deviceId: deviceId, + ), + ); + } + + Future?> _buildSettingsForXcodeProjectBuildContext(XcodeProjectBuildContext buildContext) async { + if (!existsSync()) { + return null; + } + final Map? currentBuildSettings = _buildSettingsByBuildContext[buildContext]; + if (currentBuildSettings == null) { + final Map? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext); + if (calculatedBuildSettings != null) { + _buildSettingsByBuildContext[buildContext] = calculatedBuildSettings; + } + } + return _buildSettingsByBuildContext[buildContext]; + } + + final Map> _buildSettingsByBuildContext = >{}; + + Future?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async { + final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; + if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { + return null; + } + + final Map buildSettings = await xcodeProjectInterpreter.getBuildSettings( + xcodeProject.path, + buildContext: buildContext, + ); + if (buildSettings.isNotEmpty) { + // No timeouts, flakes, or errors. + return buildSettings; + } + return null; + } } /// Represents the iOS sub-project of a Flutter project. @@ -424,80 +507,6 @@ class IosProject extends XcodeBasedProject { return productName ?? XcodeBasedProject._defaultHostAppName; } - /// The build settings for the host app of this project, as a detached map. - /// - /// Returns null, if iOS tooling is unavailable. - Future?> buildSettingsForBuildInfo( - BuildInfo? buildInfo, { - String? scheme, - String? configuration, - String? target, - EnvironmentType environmentType = EnvironmentType.physical, - String? deviceId, - bool isWatch = false, - }) async { - if (!existsSync()) { - return null; - } - final XcodeProjectInfo? info = await projectInfo(); - if (info == null) { - return null; - } - - scheme ??= info.schemeFor(buildInfo); - if (scheme == null) { - info.reportFlavorNotFoundAndExit(); - } - - configuration ??= (await projectInfo())?.buildConfigurationFor( - buildInfo, - scheme, - ); - return _buildSettingsForXcodeProjectBuildContext( - XcodeProjectBuildContext( - environmentType: environmentType, - scheme: scheme, - configuration: configuration, - target: target, - deviceId: deviceId, - isWatch: isWatch, - ), - ); - } - - Future?> _buildSettingsForXcodeProjectBuildContext(XcodeProjectBuildContext buildContext) async { - if (!existsSync()) { - return null; - } - final Map? currentBuildSettings = _buildSettingsByBuildContext[buildContext]; - if (currentBuildSettings == null) { - final Map? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext); - if (calculatedBuildSettings != null) { - _buildSettingsByBuildContext[buildContext] = calculatedBuildSettings; - } - } - return _buildSettingsByBuildContext[buildContext]; - } - - final Map> _buildSettingsByBuildContext = >{}; - - Future?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async { - final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; - if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { - return null; - } - - final Map buildSettings = await xcodeProjectInterpreter.getBuildSettings( - xcodeProject.path, - buildContext: buildContext, - ); - if (buildSettings.isNotEmpty) { - // No timeouts, flakes, or errors. - return buildSettings; - } - return null; - } - Future ensureReadyForPlatformSpecificTooling() async { await _regenerateFromTemplateIfNeeded(); if (!_flutterLibRoot.existsSync()) { diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index 7563ab77d49..9a5fce06713 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -325,7 +325,9 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( '', - buildContext: const XcodeProjectBuildContext(environmentType: EnvironmentType.simulator), + buildContext: const XcodeProjectBuildContext( + sdk: XcodeSdk.IPhoneSimulator, + ), ), const {}, ); @@ -398,7 +400,7 @@ void main() { ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('build settings uses watch destination if isWatch is true', () async { + testUsingContext('build settings uses watch destination', () async { platform.environment = const {}; fakeProcessManager.addCommands([ @@ -422,7 +424,9 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( '', - buildContext: const XcodeProjectBuildContext(isWatch: true), + buildContext: const XcodeProjectBuildContext( + sdk: XcodeSdk.WatchOS, + ), ), const {}, ); @@ -432,7 +436,7 @@ void main() { ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('build settings uses watch simulator destination if isWatch is true and environment type is simulator', () async { + testUsingContext('build settings uses watch simulator destination', () async { platform.environment = const {}; fakeProcessManager.addCommands([ @@ -458,7 +462,45 @@ void main() { expect( await xcodeProjectInterpreter.getBuildSettings( '', - buildContext: const XcodeProjectBuildContext(environmentType: EnvironmentType.simulator, isWatch: true), + buildContext: const XcodeProjectBuildContext( + sdk: XcodeSdk.WatchSimulator, + ), + ), + const {}, + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('build settings uses macosx destination', () async { + platform.environment = const {}; + + fakeProcessManager.addCommands([ + kWhichSysctlCommand, + kx64CheckCommand, + FakeCommand( + command: [ + 'xcrun', + 'xcodebuild', + '-project', + '/', + '-destination', + 'generic/platform=macOS', + '-showBuildSettings', + 'BUILD_DIR=${fileSystem.path.absolute('build', 'macos')}', + ], + exitCode: 1, + ), + ]); + + expect( + await xcodeProjectInterpreter.getBuildSettings( + '', + buildContext: const XcodeProjectBuildContext( + sdk: XcodeSdk.MacOSX, + ), ), const {}, ); diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index f624982cf3a..84978ae29f7 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -1458,7 +1458,7 @@ plugins { const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext( scheme: 'WatchScheme', deviceId: '123', - isWatch: true, + sdk: XcodeSdk.WatchOS, ); mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = { 'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': 'io.flutter.someProject', @@ -1498,7 +1498,7 @@ plugins { const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext( scheme: 'WatchScheme', deviceId: '123', - isWatch: true, + sdk: XcodeSdk.WatchOS, ); mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = { IosProject.kProductBundleIdKey: 'io.flutter.someProject', diff --git a/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart b/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart index 3dbc4897334..2460fdece56 100644 --- a/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart +++ b/packages/flutter_tools/test/integration.shard/swift_package_manager_test.dart @@ -475,4 +475,136 @@ void main() { ); } }, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos. + + test("Generated Swift package uses iOS's project minimum deployment", () async { + final Directory workingDirectory = fileSystem.systemTempDirectory + .createTempSync('swift_package_manager_minimum_deployment_ios.'); + final String workingDirectoryPath = workingDirectory.path; + try { + await SwiftPackageManagerUtils.enableSwiftPackageManager(flutterBin, workingDirectoryPath); + final String appDirectoryPath = await SwiftPackageManagerUtils.createApp( + flutterBin, + workingDirectoryPath, + iosLanguage: 'swift', + platform: 'ios', + usesSwiftPackageManager: true, + options: ['--platforms=ios'], + ); + + // Modify the project to raise the deployment version. + final File projectFile = fileSystem + .directory(appDirectoryPath) + .childDirectory('ios') + .childDirectory('Runner.xcodeproj') + .childFile('project.pbxproj'); + + final String oldProject = projectFile.readAsStringSync(); + final String newProject = oldProject.replaceAll( + RegExp(r'IPHONEOS_DEPLOYMENT_TARGET = \d+\.\d+;'), + 'IPHONEOS_DEPLOYMENT_TARGET = 15.1;', + ); + + projectFile.writeAsStringSync(newProject); + + // Build the app. This generates Flutter's Swift package. + await SwiftPackageManagerUtils.buildApp( + flutterBin, + appDirectoryPath, + options: ['ios', '--debug', '-v'], + ); + + // Verify the generated Swift package uses the project's minimum deployment. + final File generatedManifestFile = fileSystem + .directory(appDirectoryPath) + .childDirectory('ios') + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .childFile('Package.swift'); + + expect(generatedManifestFile.existsSync(), isTrue); + + final String generatedManifest = generatedManifestFile.readAsStringSync(); + const String expected = ''' + platforms: [ + .iOS("15.1") + ], +'''; + + expect(generatedManifest.contains(expected), isTrue); + } finally { + await SwiftPackageManagerUtils.disableSwiftPackageManager(flutterBin, workingDirectoryPath); + ErrorHandlingFileSystem.deleteIfExists( + workingDirectory, + recursive: true, + ); + } + }, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos. + + test("Generated Swift package uses macOS's project minimum deployment", () async { + final Directory workingDirectory = fileSystem.systemTempDirectory + .createTempSync('swift_package_manager_minimum_deployment_macos.'); + final String workingDirectoryPath = workingDirectory.path; + try { + await SwiftPackageManagerUtils.enableSwiftPackageManager(flutterBin, workingDirectoryPath); + final String appDirectoryPath = await SwiftPackageManagerUtils.createApp( + flutterBin, + workingDirectoryPath, + iosLanguage: 'swift', + platform: 'macos', + usesSwiftPackageManager: true, + options: ['--platforms=macos'], + ); + + // Modify the project to raise the deployment version. + final File projectFile = fileSystem + .directory(appDirectoryPath) + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .childFile('project.pbxproj'); + + final String oldProject = projectFile.readAsStringSync(); + final String newProject = oldProject.replaceAll( + RegExp(r'MACOSX_DEPLOYMENT_TARGET = \d+\.\d+;'), + 'MACOSX_DEPLOYMENT_TARGET = 15.1;', + ); + + projectFile.writeAsStringSync(newProject); + + // Build the app. This generates Flutter's Swift package. + await SwiftPackageManagerUtils.buildApp( + flutterBin, + appDirectoryPath, + options: ['macos', '--debug', '-v'], + ); + + // Verify the generated Swift package uses the project's minimum deployment. + final File generatedManifestFile = fileSystem + .directory(appDirectoryPath) + .childDirectory('macos') + .childDirectory('Flutter') + .childDirectory('ephemeral') + .childDirectory('Packages') + .childDirectory('FlutterGeneratedPluginSwiftPackage') + .childFile('Package.swift'); + + expect(generatedManifestFile.existsSync(), isTrue); + + final String generatedManifest = generatedManifestFile.readAsStringSync(); + const String expected = ''' + platforms: [ + .macOS("15.1") + ], +'''; + + expect(generatedManifest.contains(expected), isTrue); + } finally { + await SwiftPackageManagerUtils.disableSwiftPackageManager(flutterBin, workingDirectoryPath); + ErrorHandlingFileSystem.deleteIfExists( + workingDirectory, + recursive: true, + ); + } + }, skip: !platform.isMacOS); // [intended] Swift Package Manager only works on macos. }