Unverified Commit c9b132d0 authored by LouiseHsu's avatar LouiseHsu Committed by GitHub

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" 
<img width="441" alt="Screenshot 2023-04-17 at 11 41 07 PM" src="https://user-images.githubusercontent.com/36148254/232692957-8743742d-c3ef-42e5-833f-dff31aeb2b6a.png">
3. In the right panel, the name of the project, "Runner", should be visible under "Identity and Type". Change the name and press enter.
<img width="299" alt="Screenshot 2023-04-17 at 11 40 43 PM" src="https://user-images.githubusercontent.com/36148254/232693315-b6a71165-f5e3-4a0f-8954-2f3eee5b67cf.png">
4. A wizard should pop up. Click Rename.
<img width="573" alt="Screenshot 2023-04-17 at 11 44 01 PM" src="https://user-images.githubusercontent.com/36148254/232693381-bb9cf026-2a75-4844-b42d-ae0036ae9fdd.png">
To rename the workspace:

1. Make sure Xcode is closed.
2. Rename the .xcworkspace to your new name.

If you also renamed the project

&nbsp; 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.
parent aef7929f
......@@ -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) {
......
......@@ -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<FileSystemEntity> 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<FileSystemEntity> 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 {
......
......@@ -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 <String>['build', 'ios', '--no-pub']
);
expect(testLogger.statusText, contains('build/ios/iphoneos/Runner.app'));
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.list(<FakeCommand>[
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(),
......
......@@ -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 <String>['build', 'macos', '--no-pub']
);
}, overrides: <Type, Generator>{
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(),
......
......@@ -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');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment