Fix extraction of product bundle ID for iOS projects (#21252)
This commit is contained in:
parent
f999f1447a
commit
6cc8008283
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user