Adding vmservice to get iOS app settings (#123156)
fixes https://github.com/flutter/flutter/issues/120405
This commit is contained in:
parent
529b919f09
commit
b00f1c4599
@ -376,11 +376,11 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
|
||||
|
||||
final Map<String, String?> xcodeProjectSettingsMap = <String, String?>{};
|
||||
|
||||
xcodeProjectSettingsMap['Version Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleShortVersionStringKey);
|
||||
xcodeProjectSettingsMap['Build Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleVersionKey);
|
||||
xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleDisplayNameKey);
|
||||
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey);
|
||||
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
|
||||
xcodeProjectSettingsMap['Version Number'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleShortVersionStringKey);
|
||||
xcodeProjectSettingsMap['Build Number'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleVersionKey);
|
||||
xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleDisplayNameKey);
|
||||
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kMinimumOSVersionKey);
|
||||
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleIdentifierKey);
|
||||
|
||||
final List<ValidationMessage> validationMessages = xcodeProjectSettingsMap.entries.map((MapEntry<String, String?> entry) {
|
||||
final String title = entry.key;
|
||||
|
@ -494,7 +494,7 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
|
||||
|
||||
@override
|
||||
String get version {
|
||||
return _version ??= _plistParser.getStringValueFromFile(
|
||||
return _version ??= _plistParser.getValueFromFile<String>(
|
||||
plistFile,
|
||||
PlistParser.kCFBundleShortVersionStringKey,
|
||||
) ?? 'unknown';
|
||||
@ -508,7 +508,7 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
|
||||
}
|
||||
|
||||
final String? altLocation = _plistParser
|
||||
.getStringValueFromFile(plistFile, 'JetBrainsToolboxApp');
|
||||
.getValueFromFile<String>(plistFile, 'JetBrainsToolboxApp');
|
||||
|
||||
if (altLocation != null) {
|
||||
_pluginsPath = '$altLocation.plugins';
|
||||
|
@ -58,7 +58,7 @@ abstract class IOSApp extends ApplicationPackage {
|
||||
globals.printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
|
||||
return null;
|
||||
}
|
||||
final String? id = globals.plistParser.getStringValueFromFile(
|
||||
final String? id = globals.plistParser.getValueFromFile<String>(
|
||||
plistPath,
|
||||
PlistParser.kCFBundleIdentifierKey,
|
||||
);
|
||||
|
@ -451,7 +451,7 @@ class IOSDevice extends Device {
|
||||
_logger.printError('');
|
||||
return LaunchResult.failed();
|
||||
}
|
||||
packageId = buildResult.xcodeBuildExecution?.buildSettings['PRODUCT_BUNDLE_IDENTIFIER'];
|
||||
packageId = buildResult.xcodeBuildExecution?.buildSettings[IosProject.kProductBundleIdKey];
|
||||
}
|
||||
|
||||
packageId ??= package.id;
|
||||
|
@ -24,6 +24,7 @@ class PlistParser {
|
||||
final Logger _logger;
|
||||
final ProcessUtils _processUtils;
|
||||
|
||||
// info.pList keys
|
||||
static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
|
||||
static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
|
||||
static const String kCFBundleExecutableKey = 'CFBundleExecutable';
|
||||
@ -32,6 +33,9 @@ class PlistParser {
|
||||
static const String kMinimumOSVersionKey = 'MinimumOSVersion';
|
||||
static const String kNSPrincipalClassKey = 'NSPrincipalClass';
|
||||
|
||||
// entitlement file keys
|
||||
static const String kAssociatedDomainsKey = 'com.apple.developer.associated-domains';
|
||||
|
||||
static const String _plutilExecutable = '/usr/bin/plutil';
|
||||
|
||||
/// Returns the content, converted to XML, of the plist file located at
|
||||
@ -164,8 +168,8 @@ class PlistParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Parses the Plist file located at [plistFilePath] and returns the string
|
||||
/// value that's associated with the specified [key] within the property list.
|
||||
/// Parses the Plist file located at [plistFilePath] and returns the value
|
||||
/// that's associated with the specified [key] within the property list.
|
||||
///
|
||||
/// If [plistFilePath] points to a non-existent file or a file that's not a
|
||||
/// valid property list file, this will return null.
|
||||
@ -173,8 +177,8 @@ class PlistParser {
|
||||
/// If [key] is not found in the property list, this will return null.
|
||||
///
|
||||
/// The [plistFilePath] and [key] arguments must not be null.
|
||||
String? getStringValueFromFile(String plistFilePath, String key) {
|
||||
T? getValueFromFile<T>(String plistFilePath, String key) {
|
||||
final Map<String, dynamic> parsed = parseFile(plistFilePath);
|
||||
return parsed[key] as String?;
|
||||
return parsed[key] as T?;
|
||||
}
|
||||
}
|
||||
|
@ -468,7 +468,7 @@ class IOSSimulator extends Device {
|
||||
// parsing the xcodeproj or configuration files.
|
||||
// See https://github.com/flutter/flutter/issues/31037 for more information.
|
||||
final String plistPath = globals.fs.path.join(package.simulatorBundlePath, 'Info.plist');
|
||||
final String? bundleIdentifier = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
|
||||
final String? bundleIdentifier = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleIdentifierKey);
|
||||
if (bundleIdentifier == null) {
|
||||
globals.printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
|
||||
return LaunchResult.failed();
|
||||
|
@ -185,6 +185,7 @@ class XcodeProjectInterpreter {
|
||||
final Status status = _logger.startSpinner();
|
||||
final String? scheme = buildContext.scheme;
|
||||
final String? configuration = buildContext.configuration;
|
||||
final String? target = buildContext.target;
|
||||
final String? deviceId = buildContext.deviceId;
|
||||
final List<String> showBuildSettingsCommand = <String>[
|
||||
...xcrunCommand(),
|
||||
@ -195,6 +196,8 @@ class XcodeProjectInterpreter {
|
||||
...<String>['-scheme', scheme],
|
||||
if (configuration != null)
|
||||
...<String>['-configuration', configuration],
|
||||
if (target != null)
|
||||
...<String>['-target', target],
|
||||
if (buildContext.environmentType == EnvironmentType.simulator)
|
||||
...<String>['-sdk', 'iphonesimulator'],
|
||||
'-destination',
|
||||
@ -380,6 +383,7 @@ class XcodeProjectBuildContext {
|
||||
this.configuration,
|
||||
this.environmentType = EnvironmentType.physical,
|
||||
this.deviceId,
|
||||
this.target,
|
||||
this.isWatch = false,
|
||||
});
|
||||
|
||||
@ -387,10 +391,11 @@ class XcodeProjectBuildContext {
|
||||
final String? configuration;
|
||||
final EnvironmentType environmentType;
|
||||
final String? deviceId;
|
||||
final String? target;
|
||||
final bool isWatch;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(scheme, configuration, environmentType, deviceId);
|
||||
int get hashCode => Object.hash(scheme, configuration, environmentType, deviceId, target);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
@ -402,10 +407,26 @@ class XcodeProjectBuildContext {
|
||||
other.configuration == configuration &&
|
||||
other.deviceId == deviceId &&
|
||||
other.environmentType == environmentType &&
|
||||
other.isWatch == isWatch;
|
||||
other.isWatch == isWatch &&
|
||||
other.target == target;
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings that are relevant for setting up universal links
|
||||
@immutable
|
||||
class XcodeUniversalLinkSettings {
|
||||
const XcodeUniversalLinkSettings({
|
||||
this.bundleIdentifier,
|
||||
this.teamIdentifier,
|
||||
this.associatedDomains = const <String>[],
|
||||
});
|
||||
|
||||
final String? bundleIdentifier;
|
||||
final String? teamIdentifier;
|
||||
final List<String> associatedDomains;
|
||||
}
|
||||
|
||||
|
||||
/// Information about an Xcode project.
|
||||
///
|
||||
/// Represents the output of `xcodebuild -list`.
|
||||
|
@ -27,7 +27,7 @@ class FlutterApplicationMigration extends ProjectMigrator {
|
||||
void migrate() {
|
||||
if (_infoPlistFile.existsSync()) {
|
||||
final String? principalClass =
|
||||
globals.plistParser.getStringValueFromFile(_infoPlistFile.path, PlistParser.kNSPrincipalClassKey);
|
||||
globals.plistParser.getValueFromFile<String>(_infoPlistFile.path, PlistParser.kNSPrincipalClassKey);
|
||||
if (principalClass == null || principalClass == 'NSApplication') {
|
||||
// No NSPrincipalClass defined, or already converted. No migration
|
||||
// needed.
|
||||
|
@ -41,6 +41,7 @@ const String kFlutterMemoryInfoServiceName = 'flutterMemoryInfo';
|
||||
const String kFlutterGetSkSLServiceName = 'flutterGetSkSL';
|
||||
const String kFlutterGetIOSBuildOptionsServiceName = 'flutterGetIOSBuildOptions';
|
||||
const String kFlutterGetAndroidBuildVariantsServiceName = 'flutterGetAndroidBuildVariants';
|
||||
const String kFlutterGetIOSDeeplinkSettingsServiceName = 'flutterGetIOSDeeplinkSettings';
|
||||
|
||||
/// The error response code from an unrecoverable compilation failure.
|
||||
const int kIsolateReloadBarred = 1005;
|
||||
@ -337,6 +338,25 @@ Future<vm_service.VmService> setUpVmService({
|
||||
registrationRequests.add(
|
||||
vmService.registerService(kFlutterGetAndroidBuildVariantsServiceName, kFlutterToolAlias),
|
||||
);
|
||||
|
||||
vmService.registerServiceCallback(kFlutterGetIOSDeeplinkSettingsServiceName, (Map<String, Object?> params) async {
|
||||
final XcodeUniversalLinkSettings settings = await flutterProject.ios.universalLinkSettings(
|
||||
configuration: params['configuration']! as String,
|
||||
scheme: params['scheme']! as String,
|
||||
target: params['target']! as String,
|
||||
);
|
||||
return <String, Object>{
|
||||
'result': <String, Object>{
|
||||
kResultType: kResultTypeSuccess,
|
||||
'bundleIdentifier': settings.bundleIdentifier ?? '',
|
||||
'teamIdentifier': settings.teamIdentifier ?? '',
|
||||
'associatedDomains': settings.associatedDomains,
|
||||
},
|
||||
};
|
||||
});
|
||||
registrationRequests.add(
|
||||
vmService.registerService(kFlutterGetIOSDeeplinkSettingsServiceName, 'Flutter Tools'),
|
||||
);
|
||||
}
|
||||
|
||||
if (printStructuredErrorLogMethod != null) {
|
||||
|
@ -124,8 +124,16 @@ class IosProject extends XcodeBasedProject {
|
||||
@override
|
||||
String get pluginConfigKey => IOSPlugin.kConfigKey;
|
||||
|
||||
static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$''');
|
||||
static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)';
|
||||
// build setting keys
|
||||
static const String kProductBundleIdKey = 'PRODUCT_BUNDLE_IDENTIFIER';
|
||||
static const String kTeamIdKey = 'DEVELOPMENT_TEAM';
|
||||
static const String kEntitlementFilePathKey = 'CODE_SIGN_ENTITLEMENTS';
|
||||
static const String kHostAppBundleNameKey = 'FULL_PRODUCT_NAME';
|
||||
|
||||
static final RegExp _productBundleIdPattern = RegExp('^\\s*$kProductBundleIdKey\\s*=\\s*(["\']?)(.*?)\\1;\\s*\$');
|
||||
static const String _kProductBundleIdVariable = '\$($kProductBundleIdKey)';
|
||||
|
||||
static final RegExp _associatedDomainPattern = RegExp(r'^applinks:(.*)');
|
||||
|
||||
Directory get ephemeralModuleDirectory => parent.directory.childDirectory('.ios');
|
||||
Directory get _editableDirectory => parent.directory.childDirectory('ios');
|
||||
@ -203,24 +211,71 @@ class IosProject extends XcodeBasedProject {
|
||||
return parent.isModule || _editableDirectory.existsSync();
|
||||
}
|
||||
|
||||
Future<XcodeUniversalLinkSettings> universalLinkSettings({
|
||||
required String configuration,
|
||||
required String scheme,
|
||||
required String target,
|
||||
}) async {
|
||||
final XcodeProjectBuildContext context = XcodeProjectBuildContext(
|
||||
configuration: configuration,
|
||||
scheme: scheme,
|
||||
target: target,
|
||||
);
|
||||
|
||||
return XcodeUniversalLinkSettings(
|
||||
bundleIdentifier: await _productBundleIdentifierWithBuildContext(context),
|
||||
teamIdentifier: await _getTeamIdentifier(context),
|
||||
associatedDomains: await _getAssociatedDomains(context),
|
||||
);
|
||||
}
|
||||
|
||||
/// The product bundle identifier of the host app, or null if not set or if
|
||||
/// iOS tooling needed to read it is not installed.
|
||||
Future<String?> productBundleIdentifier(BuildInfo? buildInfo) async {
|
||||
if (!existsSync()) {
|
||||
return null;
|
||||
}
|
||||
return _productBundleIdentifier ??= await _parseProductBundleIdentifier(buildInfo);
|
||||
}
|
||||
String? _productBundleIdentifier;
|
||||
|
||||
Future<String?> _parseProductBundleIdentifier(BuildInfo? buildInfo) async {
|
||||
XcodeProjectBuildContext? buildContext;
|
||||
final XcodeProjectInfo? info = await projectInfo();
|
||||
if (info != null) {
|
||||
final String? scheme = info.schemeFor(buildInfo);
|
||||
if (scheme == null) {
|
||||
info.reportFlavorNotFoundAndExit();
|
||||
}
|
||||
final String? configuration = info.buildConfigurationFor(
|
||||
buildInfo,
|
||||
scheme,
|
||||
);
|
||||
buildContext = XcodeProjectBuildContext(
|
||||
configuration: configuration,
|
||||
scheme: scheme,
|
||||
);
|
||||
}
|
||||
return _productBundleIdentifierWithBuildContext(buildContext);
|
||||
}
|
||||
|
||||
Future<String?> _productBundleIdentifierWithBuildContext(XcodeProjectBuildContext? buildContext) async {
|
||||
if (!existsSync()) {
|
||||
return null;
|
||||
}
|
||||
if (_productBundleIdentifiers.containsKey(buildContext)) {
|
||||
return _productBundleIdentifiers[buildContext];
|
||||
}
|
||||
return _productBundleIdentifiers[buildContext] = await _parseProductBundleIdentifier(buildContext);
|
||||
}
|
||||
|
||||
final Map<XcodeProjectBuildContext?, String?> _productBundleIdentifiers = <XcodeProjectBuildContext?, String?>{};
|
||||
|
||||
|
||||
Future<String?> _parseProductBundleIdentifier(XcodeProjectBuildContext? buildContext) async {
|
||||
String? fromPlist;
|
||||
final File defaultInfoPlist = defaultHostInfoPlist;
|
||||
// Users can change the location of the Info.plist.
|
||||
// Try parsing the default, first.
|
||||
if (defaultInfoPlist.existsSync()) {
|
||||
try {
|
||||
fromPlist = globals.plistParser.getStringValueFromFile(
|
||||
fromPlist = globals.plistParser.getValueFromFile<String>(
|
||||
defaultHostInfoPlist.path,
|
||||
PlistParser.kCFBundleIdentifierKey,
|
||||
);
|
||||
@ -232,13 +287,18 @@ class IosProject extends XcodeBasedProject {
|
||||
return fromPlist;
|
||||
}
|
||||
}
|
||||
final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(buildInfo);
|
||||
if (buildContext == null) {
|
||||
// Getting build settings to evaluate info.Plist requires a context.
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, String>? allBuildSettings = await _buildSettingsForXcodeProjectBuildContext(buildContext);
|
||||
if (allBuildSettings != null) {
|
||||
if (fromPlist != null) {
|
||||
// Perform variable substitution using build settings.
|
||||
return substituteXcodeVariables(fromPlist, allBuildSettings);
|
||||
}
|
||||
return allBuildSettings['PRODUCT_BUNDLE_IDENTIFIER'];
|
||||
return allBuildSettings[kProductBundleIdKey];
|
||||
}
|
||||
|
||||
// On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from
|
||||
@ -248,13 +308,48 @@ class IosProject extends XcodeBasedProject {
|
||||
// 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)) {
|
||||
if (fromPbxproj != null && (fromPlist == null || fromPlist == _kProductBundleIdVariable)) {
|
||||
return fromPbxproj;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> _getTeamIdentifier(XcodeProjectBuildContext buildContext) async {
|
||||
final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(buildContext);
|
||||
if (buildSettings != null) {
|
||||
return buildSettings[kTeamIdKey];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<String>> _getAssociatedDomains(XcodeProjectBuildContext buildContext) async {
|
||||
final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(buildContext);
|
||||
if (buildSettings != null) {
|
||||
final String? entitlementPath = buildSettings[kEntitlementFilePathKey];
|
||||
if (entitlementPath != null) {
|
||||
final File entitlement = hostAppRoot.childFile(entitlementPath);
|
||||
if (entitlement.existsSync()) {
|
||||
final List<String>? domains = globals.plistParser.getValueFromFile<List<Object>>(
|
||||
entitlement.path,
|
||||
PlistParser.kAssociatedDomainsKey,
|
||||
)?.cast<String>();
|
||||
|
||||
if (domains != null) {
|
||||
final List<String> result = <String>[];
|
||||
for (final String domain in domains) {
|
||||
final RegExpMatch? match = _associatedDomainPattern.firstMatch(domain);
|
||||
if (match != null) {
|
||||
result.add(match.group(1)!);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return const <String>[];
|
||||
}
|
||||
|
||||
/// The bundle name of the host app, `My App.app`.
|
||||
Future<String?> hostAppBundleName(BuildInfo? buildInfo) async {
|
||||
if (!existsSync()) {
|
||||
@ -273,11 +368,11 @@ class IosProject extends XcodeBasedProject {
|
||||
if (globals.xcodeProjectInterpreter?.isInstalled ?? false) {
|
||||
final Map<String, String>? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo);
|
||||
if (xcodeBuildSettings != null) {
|
||||
productName = xcodeBuildSettings['FULL_PRODUCT_NAME'];
|
||||
productName = xcodeBuildSettings[kHostAppBundleNameKey];
|
||||
}
|
||||
}
|
||||
if (productName == null) {
|
||||
globals.printTrace('FULL_PRODUCT_NAME not present, defaulting to $hostAppProjectName');
|
||||
globals.printTrace('$kHostAppBundleNameKey not present, defaulting to $hostAppProjectName');
|
||||
}
|
||||
return productName ?? '${XcodeBasedProject._defaultHostAppName}.app';
|
||||
}
|
||||
@ -287,9 +382,11 @@ class IosProject extends XcodeBasedProject {
|
||||
/// Returns null, if iOS tooling is unavailable.
|
||||
Future<Map<String, String>?> buildSettingsForBuildInfo(
|
||||
BuildInfo? buildInfo, {
|
||||
String? scheme,
|
||||
String? configuration,
|
||||
String? target,
|
||||
EnvironmentType environmentType = EnvironmentType.physical,
|
||||
String? deviceId,
|
||||
String? scheme,
|
||||
bool isWatch = false,
|
||||
}) async {
|
||||
if (!existsSync()) {
|
||||
@ -300,24 +397,31 @@ class IosProject extends XcodeBasedProject {
|
||||
return null;
|
||||
}
|
||||
|
||||
scheme ??= info.schemeFor(buildInfo);
|
||||
if (scheme == null) {
|
||||
scheme = info.schemeFor(buildInfo);
|
||||
if (scheme == null) {
|
||||
info.reportFlavorNotFoundAndExit();
|
||||
}
|
||||
info.reportFlavorNotFoundAndExit();
|
||||
}
|
||||
|
||||
final String? configuration = (await projectInfo())?.buildConfigurationFor(
|
||||
configuration ??= (await projectInfo())?.buildConfigurationFor(
|
||||
buildInfo,
|
||||
scheme,
|
||||
);
|
||||
final XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
|
||||
environmentType: environmentType,
|
||||
scheme: scheme,
|
||||
configuration: configuration,
|
||||
deviceId: deviceId,
|
||||
isWatch: isWatch,
|
||||
return _buildSettingsForXcodeProjectBuildContext(
|
||||
XcodeProjectBuildContext(
|
||||
environmentType: environmentType,
|
||||
scheme: scheme,
|
||||
configuration: configuration,
|
||||
target: target,
|
||||
deviceId: deviceId,
|
||||
isWatch: isWatch,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, String>?> _buildSettingsForXcodeProjectBuildContext(XcodeProjectBuildContext buildContext) async {
|
||||
if (!existsSync()) {
|
||||
return null;
|
||||
}
|
||||
final Map<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext];
|
||||
if (currentBuildSettings == null) {
|
||||
final Map<String, String>? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext);
|
||||
@ -381,7 +485,7 @@ class IosProject extends XcodeBasedProject {
|
||||
// In older versions of Xcode, if the target was a watchOS companion app,
|
||||
// the Info.plist file of the target contained the key WKCompanionAppBundleIdentifier.
|
||||
if (infoFile.existsSync()) {
|
||||
final String? fromPlist = globals.plistParser.getStringValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier');
|
||||
final String? fromPlist = globals.plistParser.getValueFromFile<String>(infoFile.path, 'WKCompanionAppBundleIdentifier');
|
||||
if (bundleIdentifier == fromPlist) {
|
||||
return true;
|
||||
}
|
||||
|
@ -62,8 +62,8 @@ class FakePlistUtils extends Fake implements PlistParser {
|
||||
final Map<String, Map<String, Object>> fileContents = <String, Map<String, Object>>{};
|
||||
|
||||
@override
|
||||
String? getStringValueFromFile(String plistFilePath, String key) {
|
||||
return fileContents[plistFilePath]![key] as String?;
|
||||
T? getValueFromFile<T>(String plistFilePath, String key) {
|
||||
return fileContents[plistFilePath]![key] as T?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,7 +329,7 @@ platform :osx, '10.14'
|
||||
);
|
||||
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
|
||||
macOSProjectMigration.migrate();
|
||||
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull);
|
||||
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull);
|
||||
expect(testLogger.statusText, isEmpty);
|
||||
});
|
||||
|
||||
@ -341,7 +341,7 @@ platform :osx, '10.14'
|
||||
);
|
||||
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
|
||||
macOSProjectMigration.migrate();
|
||||
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
|
||||
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
|
||||
expect(testLogger.statusText, isEmpty);
|
||||
});
|
||||
|
||||
@ -353,7 +353,7 @@ platform :osx, '10.14'
|
||||
);
|
||||
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
|
||||
macOSProjectMigration.migrate();
|
||||
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
|
||||
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
|
||||
// Only print once.
|
||||
expect('Updating ${infoPlistFile.basename} to use NSApplication instead of FlutterApplication.'.allMatches(testLogger.statusText).length, 1);
|
||||
});
|
||||
@ -367,7 +367,7 @@ platform :osx, '10.14'
|
||||
);
|
||||
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
|
||||
macOSProjectMigration.migrate();
|
||||
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp);
|
||||
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp);
|
||||
expect(testLogger.traceText, isEmpty);
|
||||
});
|
||||
});
|
||||
|
@ -693,7 +693,7 @@ apply plugin: 'kotlin-android'
|
||||
});
|
||||
});
|
||||
|
||||
group('product bundle identifier', () {
|
||||
group('With mocked context', () {
|
||||
late MemoryFileSystem fs;
|
||||
late FakePlistParser testPlistUtils;
|
||||
late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
|
||||
@ -718,140 +718,260 @@ apply plugin: 'kotlin-android'
|
||||
});
|
||||
}
|
||||
|
||||
testWithMocks('null, if no build settings or plist entries', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
expect(await project.ios.productBundleIdentifier(null), isNull);
|
||||
});
|
||||
group('universal link', () {
|
||||
testWithMocks('build with flavor', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
project.ios.defaultHostInfoPlist.createSync(recursive: true);
|
||||
const String entitlementFilePath = 'myEntitlement.Entitlement';
|
||||
project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);
|
||||
|
||||
testWithMocks('from build settings, if no plist', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('from project file, if no plist or build settings', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject');
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
|
||||
target: 'Runner',
|
||||
scheme: 'Debug',
|
||||
configuration: 'config',
|
||||
);
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
IosProject.kTeamIdKey: 'ABC',
|
||||
IosProject.kEntitlementFilePathKey: entitlementFilePath,
|
||||
'SUFFIX': 'suffix',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
|
||||
testPlistUtils.setProperty(
|
||||
PlistParser.kAssociatedDomainsKey,
|
||||
<String>[
|
||||
'applinks:example.com',
|
||||
'applinks:example2.com',
|
||||
],
|
||||
);
|
||||
final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings(
|
||||
target: 'Runner',
|
||||
scheme: 'Debug',
|
||||
configuration: 'config',
|
||||
);
|
||||
expect(
|
||||
settings.associatedDomains,
|
||||
unorderedEquals(
|
||||
<String>[
|
||||
'example.com',
|
||||
'example2.com',
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(settings.teamIdentifier, 'ABC');
|
||||
expect(settings.bundleIdentifier, 'io.flutter.someProject.suffix');
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('from plist, if no variables', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.defaultHostInfoPlist.createSync(recursive: true);
|
||||
testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
testWithMocks('can handle entitlement file in nested directory structure.', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
project.ios.defaultHostInfoPlist.createSync(recursive: true);
|
||||
const String entitlementFilePath = 'nested/somewhere/myEntitlement.Entitlement';
|
||||
project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);
|
||||
|
||||
testWithMocks('from build settings and plist, if default variable', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('from build settings and plist, by substitution', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
project.ios.defaultHostInfoPlist.createSync(recursive: true);
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
'SUFFIX': 'suffix',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
|
||||
});
|
||||
|
||||
testWithMocks('Always pass parsing org on ios project with flavors', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
|
||||
target: 'Runner',
|
||||
scheme: 'Debug',
|
||||
configuration: 'config',
|
||||
);
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
IosProject.kTeamIdKey: 'ABC',
|
||||
IosProject.kEntitlementFilePathKey: entitlementFilePath,
|
||||
'SUFFIX': 'suffix',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
|
||||
testPlistUtils.setProperty(
|
||||
PlistParser.kAssociatedDomainsKey,
|
||||
<String>[
|
||||
'applinks:example.com',
|
||||
'applinks:example2.com',
|
||||
],
|
||||
);
|
||||
final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings(
|
||||
target: 'Runner',
|
||||
scheme: 'Debug',
|
||||
configuration: 'config',
|
||||
);
|
||||
expect(
|
||||
settings.associatedDomains,
|
||||
unorderedEquals(
|
||||
<String>[
|
||||
'example.com',
|
||||
'example2.com',
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(settings.teamIdentifier, 'ABC');
|
||||
expect(settings.bundleIdentifier, 'io.flutter.someProject.suffix');
|
||||
});
|
||||
project.ios.xcodeProject.createSync();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
|
||||
|
||||
expect(await project.organizationNames, <String>[]);
|
||||
});
|
||||
testWithMocks('return empty when no entitlement', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
project.ios.defaultHostInfoPlist.createSync(recursive: true);
|
||||
|
||||
testWithMocks('fails with no flavor and defined schemes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
|
||||
|
||||
await expectToolExitLater(
|
||||
project.ios.productBundleIdentifier(null),
|
||||
contains('You must specify a --flavor option to select one of the available schemes.')
|
||||
);
|
||||
});
|
||||
|
||||
testWithMocks('handles case insensitive flavor', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Free');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo =XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
|
||||
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('fails with flavor and default schemes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
|
||||
|
||||
await expectToolExitLater(
|
||||
project.ios.productBundleIdentifier(buildInfo),
|
||||
contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.')
|
||||
);
|
||||
});
|
||||
|
||||
testWithMocks('empty surrounded by quotes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('', qualifier: '"');
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
|
||||
target: 'Runner',
|
||||
scheme: 'Debug',
|
||||
configuration: 'config',
|
||||
);
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
IosProject.kTeamIdKey: 'ABC',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER)');
|
||||
final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings(
|
||||
target: 'Runner',
|
||||
scheme: 'Debug',
|
||||
configuration: 'config',
|
||||
);
|
||||
expect(settings.teamIdentifier, 'ABC');
|
||||
expect(settings.bundleIdentifier, 'io.flutter.someProject');
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), '');
|
||||
});
|
||||
|
||||
testWithMocks('surrounded by double quotes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
|
||||
group('product bundle identifier', () {
|
||||
testWithMocks('null, if no build settings or plist entries', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
expect(await project.ios.productBundleIdentifier(null), isNull);
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('surrounded by single quotes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
|
||||
testWithMocks('from build settings, if no plist', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] =
|
||||
<String, String>{
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('from project file, if no plist or build settings', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject');
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('from plist, if no variables', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.defaultHostInfoPlist.createSync(recursive: true);
|
||||
testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('from build settings and plist, if default variable', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('from build settings and plist, by substitution', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
project.ios.defaultHostInfoPlist.createSync(recursive: true);
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
'SUFFIX': 'suffix',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
|
||||
});
|
||||
|
||||
testWithMocks('Always pass parsing org on ios project with flavors', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
|
||||
});
|
||||
project.ios.xcodeProject.createSync();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
|
||||
|
||||
expect(await project.organizationNames, <String>[]);
|
||||
});
|
||||
|
||||
testWithMocks('fails with no flavor and defined schemes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
|
||||
|
||||
await expectToolExitLater(
|
||||
project.ios.productBundleIdentifier(null),
|
||||
contains('You must specify a --flavor option to select one of the available schemes.'),
|
||||
);
|
||||
});
|
||||
|
||||
testWithMocks('handles case insensitive flavor', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Free');
|
||||
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
};
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
|
||||
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
|
||||
|
||||
expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('fails with flavor and default schemes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
project.ios.xcodeProject.createSync();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
|
||||
|
||||
await expectToolExitLater(
|
||||
project.ios.productBundleIdentifier(buildInfo),
|
||||
contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.'),
|
||||
);
|
||||
});
|
||||
|
||||
testWithMocks('empty surrounded by quotes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('', qualifier: '"');
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), '');
|
||||
});
|
||||
|
||||
testWithMocks('surrounded by double quotes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
|
||||
testWithMocks('surrounded by single quotes', () async {
|
||||
final FlutterProject project = await someProject();
|
||||
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
|
||||
addIosProjectFile(project.directory, projectFileContent: () {
|
||||
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
|
||||
});
|
||||
});
|
||||
|
||||
@ -999,7 +1119,7 @@ apply plugin: 'kotlin-android'
|
||||
setUp(() {
|
||||
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
||||
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
};
|
||||
mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>['Runner', 'WatchTarget'], <String>[], <String>['Runner', 'WatchScheme'], logger);
|
||||
});
|
||||
@ -1093,7 +1213,7 @@ apply plugin: 'kotlin-android'
|
||||
deviceId: '123',
|
||||
);
|
||||
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
};
|
||||
project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
|
||||
testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
|
||||
@ -1127,7 +1247,7 @@ apply plugin: 'kotlin-android'
|
||||
deviceId: '123',
|
||||
);
|
||||
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
};
|
||||
|
||||
const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
|
||||
@ -1167,7 +1287,7 @@ apply plugin: 'kotlin-android'
|
||||
deviceId: '123'
|
||||
);
|
||||
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
};
|
||||
|
||||
const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
|
||||
@ -1176,7 +1296,7 @@ apply plugin: 'kotlin-android'
|
||||
isWatch: true,
|
||||
);
|
||||
mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
|
||||
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
|
||||
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
||||
'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': r'$(PRODUCT_BUNDLE_IDENTIFIER)',
|
||||
};
|
||||
|
||||
|
@ -82,6 +82,10 @@ const List<VmServiceExpectation> kAttachIsolateExpectations =
|
||||
'service': kFlutterGetAndroidBuildVariantsServiceName,
|
||||
'alias': kFlutterToolAlias,
|
||||
}),
|
||||
FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
|
||||
'service': kFlutterGetIOSDeeplinkSettingsServiceName,
|
||||
'alias': 'Flutter Tools',
|
||||
}),
|
||||
FakeVmServiceRequest(
|
||||
method: 'streamListen',
|
||||
args: <String, Object>{
|
||||
|
@ -74,52 +74,52 @@ void main() {
|
||||
file.deleteSync();
|
||||
});
|
||||
|
||||
testWithoutContext('PlistParser.getStringValueFromFile works with an XML file', () {
|
||||
testWithoutContext('PlistParser.getValueFromFile<String> works with an XML file', () {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistXml));
|
||||
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.getStringValueFromFile works with a binary file', () {
|
||||
testWithoutContext('PlistParser.getValueFromFile<String> works with a binary file', () {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistBinary));
|
||||
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.getStringValueFromFile works with a JSON file', () {
|
||||
testWithoutContext('PlistParser.getValueFromFile<String> works with a JSON file', () {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistJson));
|
||||
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.getStringValueFromFile returns null for a non-existent plist file', () {
|
||||
expect(parser.getStringValueFromFile('missing.plist', 'CFBundleIdentifier'), null);
|
||||
testWithoutContext('PlistParser.getValueFromFile<String> returns null for a non-existent plist file', () {
|
||||
expect(parser.getValueFromFile<String>('missing.plist', 'CFBundleIdentifier'), null);
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.getStringValueFromFile returns null for a non-existent key within a plist', () {
|
||||
testWithoutContext('PlistParser.getValueFromFile<String> returns null for a non-existent key within a plist', () {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistXml));
|
||||
|
||||
expect(parser.getStringValueFromFile(file.path, 'BadKey'), null);
|
||||
expect(parser.getStringValueFromFile(file.absolute.path, 'BadKey'), null);
|
||||
expect(parser.getValueFromFile<String>(file.path, 'BadKey'), null);
|
||||
expect(parser.getValueFromFile<String>(file.absolute.path, 'BadKey'), null);
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.getStringValueFromFile returns null for a malformed plist file', () {
|
||||
testWithoutContext('PlistParser.getValueFromFile<String> returns null for a malformed plist file', () {
|
||||
file.writeAsBytesSync(const <int>[1, 2, 3, 4, 5, 6]);
|
||||
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), null);
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), null);
|
||||
expect(logger.statusText, contains('Property List error: Unexpected character \x01 at line 1 / '
|
||||
'JSON error: JSON text did not start with array or object and option to allow fragments not '
|
||||
'set. around line 1, column 0.\n'));
|
||||
@ -127,11 +127,11 @@ void main() {
|
||||
' Command: /usr/bin/plutil -convert xml1 -o - ${file.absolute.path}\n');
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.getStringValueFromFile throws when /usr/bin/plutil is not found', () async {
|
||||
testWithoutContext('PlistParser.getValueFromFile<String> throws when /usr/bin/plutil is not found', () async {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistXml));
|
||||
|
||||
expect(
|
||||
() => parser.getStringValueFromFile(file.path, 'unused'),
|
||||
() => parser.getValueFromFile<String>(file.path, 'unused'),
|
||||
throwsA(isA<FileNotFoundException>()),
|
||||
);
|
||||
expect(logger.statusText, isEmpty);
|
||||
@ -141,21 +141,21 @@ void main() {
|
||||
testWithoutContext('PlistParser.replaceKey can replace a key', () async {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistXml));
|
||||
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier', value: 'dev.flutter.fake'), isTrue);
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), equals('dev.flutter.fake'));
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), equals('dev.flutter.fake'));
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.replaceKey can create a new key', () async {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistXml));
|
||||
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFNewKey'), isNull);
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFNewKey'), isNull);
|
||||
expect(parser.replaceKey(file.path, key: 'CFNewKey', value: 'dev.flutter.fake'), isTrue);
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFNewKey'), equals('dev.flutter.fake'));
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFNewKey'), equals('dev.flutter.fake'));
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.replaceKey can delete a key', () async {
|
||||
@ -164,7 +164,7 @@ void main() {
|
||||
expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier'), isTrue);
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), isNull);
|
||||
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), isNull);
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
||||
testWithoutContext('PlistParser.replaceKey throws when /usr/bin/plutil is not found', () async {
|
||||
@ -192,9 +192,9 @@ void main() {
|
||||
testWithoutContext('PlistParser.replaceKey works with a JSON file', () {
|
||||
file.writeAsBytesSync(base64.decode(base64PlistJson));
|
||||
|
||||
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
|
||||
expect(parser.replaceKey(file.path, key:'CFBundleIdentifier', value: 'dev.flutter.fake'), isTrue);
|
||||
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'dev.flutter.fake');
|
||||
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'dev.flutter.fake');
|
||||
expect(logger.statusText, isEmpty);
|
||||
expect(logger.errorText, isEmpty);
|
||||
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
|
||||
|
@ -296,8 +296,8 @@ class FakePlistParser implements PlistParser {
|
||||
}
|
||||
|
||||
@override
|
||||
String? getStringValueFromFile(String plistFilePath, String key) {
|
||||
return _underlyingValues[key] as String?;
|
||||
T? getValueFromFile<T>(String plistFilePath, String key) {
|
||||
return _underlyingValues[key] as T?;
|
||||
}
|
||||
|
||||
@override
|
||||
|
Loading…
x
Reference in New Issue
Block a user