From c9b132d0798ef22736c19a40277fddc9c94ef7a3 Mon Sep 17 00:00:00 2001 From: LouiseHsu Date: Tue, 2 May 2023 22:52:58 -0700 Subject: [PATCH] Allow .xcworkspace and .xcodeproj to be renamed from default name 'Runner' (#124533) Adds the ability to rename Runner.xcodeproj and Runner.xcworkspace - fixes https://github.com/flutter/flutter/issues/9767. To rename a project: 1. Open Runner.xcodeproj in Xcode 2. In the left panel, left click "Show File Inspector" Screenshot 2023-04-17 at 11 41 07 PM 3. In the right panel, the name of the project, "Runner", should be visible under "Identity and Type". Change the name and press enter. Screenshot 2023-04-17 at 11 40 43 PM 4. A wizard should pop up. Click Rename. Screenshot 2023-04-17 at 11 44 01 PM To rename the workspace: 1. Make sure Xcode is closed. 2. Rename the .xcworkspace to your new name. If you also renamed the project   3. Reopen the .xcworkspace in Xcode. If the selected project is the old name and in red, update it to match the new project name. Tests for schemeFor were changed as with Xcode 14, in some cases the scheme will be renamed along with the project. Thus we will get the best match scheme for either the project name, or the default name Runner. However if a flavor is present, the scheme should always match the flavor. --- .../flutter_tools/lib/src/ios/xcodeproj.dart | 1 + .../flutter_tools/lib/src/xcode_project.dart | 57 ++++++++++++------- .../hermetic/build_ios_test.dart | 38 ++++++++++++- .../hermetic/build_macos_test.dart | 23 ++++++++ .../general.shard/ios/xcodeproj_test.dart | 1 + 5 files changed, 97 insertions(+), 23 deletions(-) diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index afe88e85025..0edfa8dd1ef 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 41a49c22cec..94f57677ecc 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 3774a330504..01cb9ddfde4 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 92a34a55187..014ae63b6e0 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 f4bc973506e..90210c32556 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');