diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index afe88e8502..0edfa8dd1e 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -477,6 +477,7 @@ class XcodeProjectInfo { } return false; } + /// Returns unique scheme matching [buildInfo], or null, if there is no unique /// best match. String? schemeFor(BuildInfo? buildInfo) { diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 41a49c22ce..94f57677ec 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -21,7 +21,36 @@ import 'template.dart'; /// /// This defines interfaces common to iOS and macOS projects. abstract class XcodeBasedProject extends FlutterProjectPlatform { - static const String _hostAppProjectName = 'Runner'; + static const String _defaultHostAppName = 'Runner'; + + /// The Xcode workspace (.xcworkspace directory) of the host app. + Directory? get xcodeWorkspace { + if (!hostAppRoot.existsSync()) { + return null; + } + return _xcodeDirectoryWithExtension('.xcworkspace'); + } + + /// The project name (.xcodeproj basename) of the host app. + late final String hostAppProjectName = () { + if (!hostAppRoot.existsSync()) { + return _defaultHostAppName; + } + final Directory? xcodeProjectDirectory = _xcodeDirectoryWithExtension('.xcodeproj'); + return xcodeProjectDirectory != null + ? xcodeProjectDirectory.fileSystem.path.basenameWithoutExtension(xcodeProjectDirectory.path) + : _defaultHostAppName; + }(); + + Directory? _xcodeDirectoryWithExtension(String extension) { + final List contents = hostAppRoot.listSync(); + for (final FileSystemEntity entity in contents) { + if (globals.fs.path.extension(entity.path) == extension && !globals.fs.path.basename(entity.path).startsWith('.')) { + return hostAppRoot.childDirectory(entity.basename); + } + } + return null; + } /// The parent of this project. FlutterProject get parent; @@ -29,10 +58,10 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { Directory get hostAppRoot; /// The default 'Info.plist' file of the host app. The developer can change this location in Xcode. - File get defaultHostInfoPlist => hostAppRoot.childDirectory(_hostAppProjectName).childFile('Info.plist'); + File get defaultHostInfoPlist => hostAppRoot.childDirectory(_defaultHostAppName).childFile('Info.plist'); /// The Xcode project (.xcodeproj directory) of the host app. - Directory get xcodeProject => hostAppRoot.childDirectory('$_hostAppProjectName.xcodeproj'); + Directory get xcodeProject => hostAppRoot.childDirectory('$hostAppProjectName.xcodeproj'); /// The 'project.pbxproj' file of [xcodeProject]. File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); @@ -46,22 +75,6 @@ abstract class XcodeBasedProject extends FlutterProjectPlatform { .childDirectory('project.xcworkspace') .childFile('contents.xcworkspacedata'); - /// The Xcode workspace (.xcworkspace directory) of the host app. - Directory? get xcodeWorkspace { - if (!hostAppRoot.existsSync()) { - return null; - } - final List contents = hostAppRoot.listSync(); - for (final FileSystemEntity entity in contents) { - // On certain volume types, there is sometimes a stray `._Runner.xcworkspace` file. - // Find the first non-hidden xcworkspace and return the directory. - if (globals.fs.path.extension(entity.path) == '.xcworkspace' && !globals.fs.path.basename(entity.path).startsWith('.')) { - return hostAppRoot.childDirectory(entity.basename); - } - } - return null; - } - /// Xcode workspace shared data directory for the host app. Directory? get xcodeWorkspaceSharedData => xcodeWorkspace?.childDirectory('xcshareddata'); @@ -264,9 +277,9 @@ class IosProject extends XcodeBasedProject { } } if (productName == null) { - globals.printTrace('FULL_PRODUCT_NAME not present, defaulting to ${XcodeBasedProject._hostAppProjectName}'); + globals.printTrace('FULL_PRODUCT_NAME not present, defaulting to $hostAppProjectName'); } - return productName ?? '${XcodeBasedProject._hostAppProjectName}.app'; + return productName ?? '${XcodeBasedProject._defaultHostAppName}.app'; } /// The build settings for the host app of this project, as a detached map. @@ -498,7 +511,7 @@ class IosProject extends XcodeBasedProject { ? _flutterLibRoot .childDirectory('Flutter') .childDirectory('FlutterPluginRegistrant') - : hostAppRoot.childDirectory(XcodeBasedProject._hostAppProjectName); + : hostAppRoot.childDirectory(XcodeBasedProject._defaultHostAppName); } File get pluginRegistrantHeader { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart index 3774a33050..01cb9ddfde 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart @@ -129,6 +129,7 @@ void main() { FakeCommand setUpFakeXcodeBuildHandler({ bool verbose = false, bool simulator = false, + bool customNaming = false, String? deviceId, int exitCode = 0, String? stdout, @@ -147,7 +148,11 @@ void main() { 'VERBOSE_SCRIPT_LOGGING=YES' else '-quiet', - '-workspace', 'Runner.xcworkspace', + '-workspace', + if (customNaming) + 'RenamedWorkspace.xcworkspace' + else + 'Runner.xcworkspace', '-scheme', 'Runner', 'BUILD_DIR=/build/ios', '-sdk', @@ -272,6 +277,37 @@ void main() { XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), }); + testUsingContext('ios build invokes xcode build with renamed xcodeproj and xcworkspace', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + + fileSystem.directory(fileSystem.path.join('ios', 'RenamedProj.xcodeproj')).createSync(recursive: true); + fileSystem.directory(fileSystem.path.join('ios', 'RenamedWorkspace.xcworkspace')).createSync(recursive: true); + fileSystem.file(fileSystem.path.join('ios', 'RenamedProj.xcodeproj', 'project.pbxproj')).createSync(); + createCoreMockProjectFiles(); + + await createTestCommandRunner(command).run( + const ['build', 'ios', '--no-pub'] + ); + expect(testLogger.statusText, contains('build/ios/iphoneos/Runner.app')); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.list([ + xattrCommand, + setUpFakeXcodeBuildHandler(customNaming: true, onRun: () { + fileSystem.directory('build/ios/Release-iphoneos/Runner.app').createSync(recursive: true); + }), + setUpRsyncCommand(), + ]), + Platform: () => macosPlatform, + XcodeProjectInterpreter: () => FakeXcodeProjectInterpreterWithBuildSettings(), + }); + testUsingContext('ios build invokes xcode build with device ID', () async { final BuildCommand command = BuildCommand( androidSdk: FakeAndroidSdk(), diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart index 92a34a5518..014ae63b6e 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_macos_test.dart @@ -159,6 +159,29 @@ STDERR STUFF FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), }); + testUsingContext('macOS build successfully with renamed .xcodeproj/.xcworkspace files', () async { + final BuildCommand command = BuildCommand( + androidSdk: FakeAndroidSdk(), + buildSystem: TestBuildSystem.all(BuildResult(success: true)), + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + osUtils: FakeOperatingSystemUtils(), + ); + + fileSystem.directory(fileSystem.path.join('macos', 'RenamedProj.xcodeproj')).createSync(recursive: true); + fileSystem.directory(fileSystem.path.join('macos', 'RenamedWorkspace.xcworkspace')).createSync(recursive: true); + createCoreMockProjectFiles(); + + await createTestCommandRunner(command).run( + const ['build', 'macos', '--no-pub'] + ); + }, overrides: { + Platform: () => macosPlatform, + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true), + }); + testUsingContext('macOS build fails on non-macOS platform', () async { final BuildCommand command = BuildCommand( androidSdk: FakeAndroidSdk(), 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 f4bc973506..90210c3255 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -632,6 +632,7 @@ Information about project "Runner": expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO', treeShakeIcons: false)), 'HELLO'); expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello', treeShakeIcons: false)), 'Hello'); }); + testWithoutContext('expected build configuration for flavored build is Mode-Flavor', () { expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello', treeShakeIcons: false), 'Hello'), 'Debug-Hello'); expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO', treeShakeIcons: false), 'Hello'), 'Profile-Hello');