Commit a6c3ffe1 authored by Jenn Magder's avatar Jenn Magder Committed by Flutter GitHub Bot

Allow Xcode project Info.plist to be moved (#47993)

parent c3406914
......@@ -354,8 +354,8 @@ class IosProject implements XcodeBasedProject {
@override
File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
/// The 'Info.plist' file of the host app.
File get hostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
/// The default 'Info.plist' file of the host app. The developer can change this location in Xcode.
File get defaultHostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
@override
Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');
......@@ -384,27 +384,43 @@ class IosProject implements XcodeBasedProject {
/// iOS tooling needed to read it is not installed.
Future<String> get productBundleIdentifier async {
String fromPlist;
try {
fromPlist = PlistParser.instance.getValueFromFile(
hostInfoPlist.path,
PlistParser.kCFBundleIdentifierKey,
);
} on FileNotFoundException {
// iOS tooling not found; likely not running OSX; let [fromPlist] be null
final File defaultInfoPlist = defaultHostInfoPlist;
// Users can change the location of the Info.plist.
// Try parsing the default, first.
if (defaultInfoPlist.existsSync()) {
try {
fromPlist = PlistParser.instance.getValueFromFile(
defaultHostInfoPlist.path,
PlistParser.kCFBundleIdentifierKey,
);
} on FileNotFoundException {
// iOS tooling not found; likely not running OSX; let [fromPlist] be null
}
if (fromPlist != null && !fromPlist.contains('\$')) {
// Info.plist has no build variables in product bundle ID.
return fromPlist;
}
}
if (fromPlist != null && !fromPlist.contains('\$')) {
// Info.plist has no build variables in product bundle ID.
return fromPlist;
final Map<String, String> allBuildSettings = await buildSettings;
if (allBuildSettings != null) {
if (fromPlist != null) {
// Perform variable substitution using build settings.
return xcode.substituteXcodeVariables(fromPlist, allBuildSettings);
}
return allBuildSettings['PRODUCT_BUNDLE_IDENTIFIER'];
}
// On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from
// the project file. This can return the wrong bundle identifier if additional
// bundles have been added to the project and are found first, like frameworks
// or companion watchOS projects. However, on non-macOS platforms this is
// only used for display purposes and to regenerate organization names, so
// best-effort is probably fine.
final String fromPbxproj = _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2);
if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) {
// Common case. Avoids parsing build settings.
return fromPbxproj;
}
if (fromPlist != null && xcode.xcodeProjectInterpreter.isInstalled) {
// General case: perform variable substitution using build settings.
return xcode.substituteXcodeVariables(fromPlist, await buildSettings);
}
return null;
}
......@@ -415,11 +431,17 @@ class IosProject implements XcodeBasedProject {
if (!xcode.xcodeProjectInterpreter.isInstalled) {
return null;
}
_buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings(
Map<String, String> buildSettings = _buildSettings;
buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings(
xcodeProject.path,
_hostAppBundleName,
);
return _buildSettings;
if (buildSettings != null && buildSettings.isNotEmpty) {
// No timeouts, flakes, or errors.
_buildSettings = buildSettings;
return buildSettings;
}
return null;
}
Map<String, String> _buildSettings;
......
......@@ -348,32 +348,54 @@ apply plugin: 'kotlin-android'
});
}
testWithMocks('null, if no pbxproj or plist entries', () async {
testWithMocks('null, if no build settings or plist entries', () async {
final FlutterProject project = await someProject();
expect(await project.ios.productBundleIdentifier, isNull);
});
testWithMocks('from pbxproj file, if no plist', () async {
testWithMocks('from build settings, if no plist', () async {
final FlutterProject project = await someProject();
when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
(_) {
return Future<Map<String,String>>.value(<String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
});
}
);
expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
});
testWithMocks('from project file, if no plist or build settings', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject');
});
expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
});
testWithMocks('from plist, if no variables', () async {
final FlutterProject project = await someProject();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('io.flutter.someProject');
expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
});
testWithMocks('from pbxproj and plist, if default variable', () async {
testWithMocks('from build settings and plist, if default variable', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject');
});
when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
(_) {
return Future<Map<String,String>>.value(<String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
});
}
);
when(mockPlistUtils.getValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)');
expect(await project.ios.productBundleIdentifier, 'io.flutter.someProject');
});
testWithMocks('from pbxproj and plist, by substitution', () async {
testWithMocks('from build settings and plist, by substitution', () async {
final FlutterProject project = await someProject();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenAnswer(
(_) {
return Future<Map<String,String>>.value(<String, String>{
......
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