Unverified Commit b00f1c45 authored by chunhtai's avatar chunhtai Committed by GitHub

Adding vmservice to get iOS app settings (#123156)

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