Fix extraction of product bundle ID for iOS projects (#21252)
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index 58829a2..f71c0ae 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -34,7 +34,7 @@
File get packagesFile => null;
@override
- String toString() => displayName;
+ String toString() => displayName ?? id;
}
class AndroidApk extends ApplicationPackage {
diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart
index a1ce5e0..5196e1b 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios.dart
@@ -78,7 +78,7 @@
final String logTarget = forSimulator ? 'simulator' : 'device';
final String typeName = artifacts.getEngineType(TargetPlatform.ios, buildInfo.mode);
- printStatus('Building ${app.toString()} for $logTarget ($typeName)...');
+ printStatus('Building $app for $logTarget ($typeName)...');
final XcodeBuildResult result = await buildXcodeProject(
app: app,
buildInfo: buildInfo,
diff --git a/packages/flutter_tools/lib/src/ios/plist_utils.dart b/packages/flutter_tools/lib/src/ios/plist_utils.dart
index e394e7c..0c75ad2 100644
--- a/packages/flutter_tools/lib/src/ios/plist_utils.dart
+++ b/packages/flutter_tools/lib/src/ios/plist_utils.dart
@@ -16,7 +16,9 @@
// Don't use PlistBuddy since that is not guaranteed to be installed.
// 'defaults' requires the path to be absolute and without the 'plist'
// extension.
-
+ const String executable = '/usr/bin/defaults';
+ if (!fs.isFileSync(executable))
+ return null;
if (!fs.isFileSync(plistFilePath))
return null;
@@ -24,7 +26,7 @@
try {
final String value = runCheckedSync(<String>[
- '/usr/bin/defaults', 'read', normalizedPlistPath, key
+ executable, 'read', normalizedPlistPath, key
]);
return value.isEmpty ? null : value;
} catch (error) {
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index c2aeaca..9ae0d11 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -20,7 +20,7 @@
import '../project.dart';
final RegExp _settingExpr = new RegExp(r'(\w+)\s*=\s*(.*)$');
-final RegExp _varExpr = new RegExp(r'\$\((.*)\)');
+final RegExp _varExpr = new RegExp(r'\$\(([^)]*)\)');
String flutterFrameworkDir(BuildMode mode) {
return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, mode)));
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index b1fc04d..5067269 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -13,6 +13,8 @@
import 'bundle.dart' as bundle;
import 'cache.dart';
import 'flutter_manifest.dart';
+import 'ios/ios_workflow.dart';
+import 'ios/plist_utils.dart' as plist;
import 'ios/xcodeproj.dart' as xcode;
import 'plugins.dart';
import 'template.dart';
@@ -147,6 +149,7 @@
/// Flutter applications and the `.ios/` sub-folder of Flutter modules.
class IosProject {
static final RegExp _productBundleIdPattern = new RegExp(r'^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(.*);\s*$');
+ static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)';
static const String _hostAppBundleName = 'Runner';
IosProject._(this.parent);
@@ -174,22 +177,47 @@
/// The 'Manifest.lock'.
File get podManifestLock => directory.childDirectory('Pods').childFile('Manifest.lock');
+ /// The 'Info.plist' file of the host app.
+ File get hostInfoPlist => directory.childDirectory(_hostAppBundleName).childFile('Info.plist');
+
/// '.xcodeproj' folder of the host app.
Directory get xcodeProject => directory.childDirectory('$_hostAppBundleName.xcodeproj');
/// The '.pbxproj' file of the host app.
File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
- /// The product bundle identifier of the host app.
+ /// The product bundle identifier of the host app, or null if not set or if
+ /// iOS tooling needed to read it is not installed.
String get productBundleIdentifier {
- return _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(1);
+ final String fromPlist = iosWorkflow.getPlistValueFromFile(
+ hostInfoPlist.path,
+ plist.kCFBundleIdentifierKey,
+ );
+ if (fromPlist != null && !fromPlist.contains('\$')) {
+ // Info.plist has no build variables in product bundle ID.
+ return fromPlist;
+ }
+ final String fromPbxproj = _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(1);
+ 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, buildSettings);
+ }
+ return null;
}
/// True, if the host app project is using Swift.
bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION');
/// The build settings for the host app of this project, as a detached map.
+ ///
+ /// Returns null, if iOS tooling is unavailable.
Map<String, String> get buildSettings {
+ if (!xcode.xcodeProjectInterpreter.isInstalled)
+ return null;
return xcode.xcodeProjectInterpreter.getBuildSettings(xcodeProject.path, _hostAppBundleName);
}
diff --git a/packages/flutter_tools/test/project_test.dart b/packages/flutter_tools/test/project_test.dart
index dc19f23..5ab8100 100644
--- a/packages/flutter_tools/test/project_test.dart
+++ b/packages/flutter_tools/test/project_test.dart
@@ -9,10 +9,13 @@
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
+import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
+import 'package:mockito/mockito.dart';
import 'src/common.dart';
import 'src/context.dart';
@@ -221,6 +224,55 @@
});
});
+ group('product bundle identifier', () {
+ MemoryFileSystem fs;
+ MockIOSWorkflow mockIOSWorkflow;
+ MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
+ setUp(() {
+ fs = new MemoryFileSystem();
+ mockIOSWorkflow = new MockIOSWorkflow();
+ mockXcodeProjectInterpreter = new MockXcodeProjectInterpreter();
+ });
+
+ void testWithMocks(String description, Future<Null> testMethod()) {
+ testUsingContext(description, testMethod, overrides: <Type, Generator>{
+ FileSystem: () => fs,
+ IOSWorkflow: () => mockIOSWorkflow,
+ XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
+ });
+ }
+
+ testWithMocks('null, if no pbxproj or plist entries', () async {
+ final FlutterProject project = await someProject();
+ expect(project.ios.productBundleIdentifier, isNull);
+ });
+ testWithMocks('from pbxproj file, if no plist', () async {
+ final FlutterProject project = await someProject();
+ addIosWithBundleId(project.directory, 'io.flutter.someProject');
+ expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+ });
+ testWithMocks('from plist, if no variables', () async {
+ final FlutterProject project = await someProject();
+ when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('io.flutter.someProject');
+ expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+ });
+ testWithMocks('from pbxproj and plist, if default variable', () async {
+ final FlutterProject project = await someProject();
+ addIosWithBundleId(project.directory, 'io.flutter.someProject');
+ when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER)');
+ expect(project.ios.productBundleIdentifier, 'io.flutter.someProject');
+ });
+ testWithMocks('from pbxproj and plist, by substitution', () async {
+ final FlutterProject project = await someProject();
+ when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
+ 'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
+ 'SUFFIX': 'suffix',
+ });
+ when(mockIOSWorkflow.getPlistValueFromFile(any, any)).thenReturn('\$(PRODUCT_BUNDLE_IDENTIFIER).\$(SUFFIX)');
+ expect(project.ios.productBundleIdentifier, 'io.flutter.someProject.suffix');
+ });
+ });
+
group('organization names set', () {
testInMemory('is empty, if project not created', () async {
final FlutterProject project = await someProject();
@@ -457,3 +509,10 @@
.childDirectory('plugins')
.childFile('GeneratedPluginRegistrant.java');
}
+
+class MockIOSWorkflow extends Mock implements IOSWorkflow {}
+
+class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {
+ @override
+ bool get isInstalled => true;
+}