Unverified Commit 6cc80082 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

Fix extraction of product bundle ID for iOS projects (#21252)

parent f999f144
......@@ -34,7 +34,7 @@ abstract class ApplicationPackage {
File get packagesFile => null;
@override
String toString() => displayName;
String toString() => displayName ?? id;
}
class AndroidApk extends ApplicationPackage {
......
......@@ -78,7 +78,7 @@ class BuildIOSCommand extends BuildSubCommand {
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,
......
......@@ -16,7 +16,9 @@ String getValueFromFile(String plistFilePath, String key) {
// 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 @@ String getValueFromFile(String plistFilePath, String key) {
try {
final String value = runCheckedSync(<String>[
'/usr/bin/defaults', 'read', normalizedPlistPath, key
executable, 'read', normalizedPlistPath, key
]);
return value.isEmpty ? null : value;
} catch (error) {
......
......@@ -20,7 +20,7 @@ import '../globals.dart';
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)));
......
......@@ -13,6 +13,8 @@ import 'build_info.dart';
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 @@ class FlutterProject {
/// 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 @@ class IosProject {
/// 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);
}
......
......@@ -9,10 +9,13 @@ import 'package:flutter_tools/src/base/context.dart';
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 @@ void main() {
});
});
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 @@ File androidPluginRegistrant(Directory parent) {
.childDirectory('plugins')
.childFile('GeneratedPluginRegistrant.java');
}
class MockIOSWorkflow extends Mock implements IOSWorkflow {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {
@override
bool get isInstalled => true;
}
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