Change from using `defaults` to `plutil` for Plist parsing (#38662)
We were using the `defaults` command-line utility to parse
Plist files, but it was never supported by Apple, and it
appears that in an upcoming OS release, it will be less likely
to work:
> WARNING: The defaults command will be changed in an upcoming
> major release to only operate on preferences domains. General
> plist manipulation utilities will be folded into a different
> command-line program.
Fixes https://github.com/flutter/flutter/issues/37701
diff --git a/packages/flutter_tools/lib/src/android/android_studio.dart b/packages/flutter_tools/lib/src/android/android_studio.dart
index f1330dd..2fae44a 100644
--- a/packages/flutter_tools/lib/src/android/android_studio.dart
+++ b/packages/flutter_tools/lib/src/android/android_studio.dart
@@ -10,8 +10,7 @@
import '../base/process_manager.dart';
import '../base/version.dart';
import '../globals.dart';
-import '../ios/ios_workflow.dart';
-import '../ios/plist_utils.dart' as plist;
+import '../ios/plist_parser.dart';
AndroidStudio get androidStudio => context.get<AndroidStudio>();
@@ -43,34 +42,30 @@
factory AndroidStudio.fromMacOSBundle(String bundlePath) {
String studioPath = fs.path.join(bundlePath, 'Contents');
String plistFile = fs.path.join(studioPath, 'Info.plist');
- String plistValue = iosWorkflow.getPlistValueFromFile(
- plistFile,
- null,
- );
- final RegExp _pathsSelectorMatcher = RegExp(r'"idea.paths.selector" = "[^;]+"');
- final RegExp _jetBrainsToolboxAppMatcher = RegExp(r'JetBrainsToolboxApp = "[^;]+"');
+ Map<String, dynamic> plistValues = PlistParser.instance.parseFile(plistFile);
// As AndroidStudio managed by JetBrainsToolbox could have a wrapper pointing to the real Android Studio.
// Check if we've found a JetBrainsToolbox wrapper and deal with it properly.
- final String jetBrainsToolboxAppBundlePath = extractStudioPlistValueWithMatcher(plistValue, _jetBrainsToolboxAppMatcher);
+ final String jetBrainsToolboxAppBundlePath = plistValues['JetBrainsToolboxApp'];
if (jetBrainsToolboxAppBundlePath != null) {
studioPath = fs.path.join(jetBrainsToolboxAppBundlePath, 'Contents');
plistFile = fs.path.join(studioPath, 'Info.plist');
- plistValue = iosWorkflow.getPlistValueFromFile(
- plistFile,
- null,
- );
+ plistValues = PlistParser.instance.parseFile(plistFile);
}
- final String versionString = iosWorkflow.getPlistValueFromFile(
- plistFile,
- plist.kCFBundleShortVersionStringKey,
- );
+ final String versionString = plistValues[PlistParser.kCFBundleShortVersionStringKey];
Version version;
if (versionString != null)
version = Version.parse(versionString);
- final String pathsSelectorValue = extractStudioPlistValueWithMatcher(plistValue, _pathsSelectorMatcher);
+ String pathsSelectorValue;
+ final Map<String, dynamic> jvmOptions = plistValues['JVMOptions'];
+ if (jvmOptions != null) {
+ final Map<String, dynamic> jvmProperties = jvmOptions['Properties'];
+ if (jvmProperties != null) {
+ pathsSelectorValue = jvmProperties['idea.paths.selector'];
+ }
+ }
final String presetPluginsPath = pathsSelectorValue == null
? null
: fs.path.join(homeDirPath, 'Library', 'Application Support', '$pathsSelectorValue');
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index a032cfd..9da49e9 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -19,8 +19,7 @@
import 'build_info.dart';
import 'fuchsia/application_package.dart';
import 'globals.dart';
-import 'ios/ios_workflow.dart';
-import 'ios/plist_utils.dart' as plist;
+import 'ios/plist_parser.dart';
import 'linux/application_package.dart';
import 'macos/application_package.dart';
import 'project.dart';
@@ -309,9 +308,9 @@
printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
return null;
}
- final String id = iosWorkflow.getPlistValueFromFile(
+ final String id = PlistParser.instance.getValueFromFile(
plistPath,
- plist.kCFBundleIdentifierKey,
+ PlistParser.kCFBundleIdentifierKey,
);
if (id == null) {
printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart
index 94de99c..1048e32 100644
--- a/packages/flutter_tools/lib/src/base/file_system.dart
+++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -157,3 +157,13 @@
return referenceFile.existsSync()
&& referenceFile.lastModifiedSync().isAfter(entity.statSync().modified);
}
+
+/// Exception indicating that a file that was expected to exist was not found.
+class FileNotFoundException implements IOException {
+ const FileNotFoundException(this.path);
+
+ final String path;
+
+ @override
+ String toString() => 'File not found: $path';
+}
diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart
index 6718891..bbb03f2 100644
--- a/packages/flutter_tools/lib/src/commands/build_aot.dart
+++ b/packages/flutter_tools/lib/src/commands/build_aot.dart
@@ -15,7 +15,7 @@
import '../build_info.dart';
import '../dart/package_map.dart';
import '../globals.dart';
-import '../ios/ios_workflow.dart';
+import '../ios/plist_parser.dart';
import '../macos/xcode.dart';
import '../resident_runner.dart';
import '../runner/flutter_command.dart';
@@ -222,7 +222,7 @@
}
final RunResult clangResult = await xcode.clang(<String>['--version']);
final String clangVersion = clangResult.stdout.split('\n').first;
- final String engineClangVersion = iosWorkflow.getPlistValueFromFile(
+ final String engineClangVersion = PlistParser.instance.getValueFromFile(
fs.path.join(flutterFrameworkPath, 'Info.plist'),
'ClangVersion',
);
diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index fb12357..083a74a 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -24,7 +24,7 @@
import 'globals.dart';
import 'intellij/intellij.dart';
import 'ios/ios_workflow.dart';
-import 'ios/plist_utils.dart';
+import 'ios/plist_parser.dart';
import 'linux/linux_doctor.dart';
import 'linux/linux_workflow.dart';
import 'macos/cocoapods_validator.dart';
@@ -731,9 +731,9 @@
String get version {
if (_version == null) {
final String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist');
- _version = iosWorkflow.getPlistValueFromFile(
+ _version = PlistParser.instance.getValueFromFile(
plistFile,
- kCFBundleShortVersionStringKey,
+ PlistParser.kCFBundleShortVersionStringKey,
) ?? 'unknown';
}
return _version;
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
index f21dffd..ac9630b 100644
--- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -6,7 +6,6 @@
import '../base/platform.dart';
import '../doctor.dart';
import '../macos/xcode.dart';
-import 'plist_utils.dart' as plist;
IOSWorkflow get iosWorkflow => context.get<IOSWorkflow>();
@@ -27,8 +26,4 @@
@override
bool get canListEmulators => false;
-
- String getPlistValueFromFile(String path, String key) {
- return plist.getValueFromFile(path, key);
- }
}
diff --git a/packages/flutter_tools/lib/src/ios/plist_parser.dart b/packages/flutter_tools/lib/src/ios/plist_parser.dart
new file mode 100644
index 0000000..8e8beeb
--- /dev/null
+++ b/packages/flutter_tools/lib/src/ios/plist_parser.dart
@@ -0,0 +1,63 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/process.dart';
+import '../convert.dart';
+import '../globals.dart';
+
+class PlistParser {
+ const PlistParser();
+
+ static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
+ static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
+ static const String kCFBundleExecutable = 'CFBundleExecutable';
+
+ static PlistParser get instance => context.get<PlistParser>() ?? const PlistParser();
+
+ /// Parses the plist file located at [plistFilePath] and returns the
+ /// associated map of key/value property list pairs.
+ ///
+ /// If [plistFilePath] points to a non-existent file or a file that's not a
+ /// valid property list file, this will return an empty map.
+ ///
+ /// The [plistFilePath] argument must not be null.
+ Map<String, dynamic> parseFile(String plistFilePath) {
+ assert(plistFilePath != null);
+ const String executable = '/usr/bin/plutil';
+ if (!fs.isFileSync(executable))
+ throw const FileNotFoundException(executable);
+ if (!fs.isFileSync(plistFilePath))
+ return const <String, dynamic>{};
+
+ final String normalizedPlistPath = fs.path.absolute(plistFilePath);
+
+ try {
+ final List<String> args = <String>[
+ executable, '-convert', 'json', '-o', '-', normalizedPlistPath,
+ ];
+ final String jsonContent = runCheckedSync(args);
+ return json.decode(jsonContent);
+ } catch (error) {
+ printTrace('$error');
+ return const <String, dynamic>{};
+ }
+ }
+
+ /// 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.
+ ///
+ /// If [key] is not found in the property list, this will return null.
+ ///
+ /// The [plistFilePath] and [key] arguments must not be null.
+ String getValueFromFile(String plistFilePath, String key) {
+ assert(key != null);
+ final Map<String, dynamic> parsed = parseFile(plistFilePath);
+ return parsed[key];
+ }
+}
diff --git a/packages/flutter_tools/lib/src/ios/plist_utils.dart b/packages/flutter_tools/lib/src/ios/plist_utils.dart
deleted file mode 100644
index 0a67279..0000000
--- a/packages/flutter_tools/lib/src/ios/plist_utils.dart
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import '../base/file_system.dart';
-import '../base/process.dart';
-
-const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
-const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
-const String kCFBundleExecutable = 'CFBundleExecutable';
-
-// Prefer using [iosWorkflow.getPlistValueFromFile] to enable mocking.
-String getValueFromFile(String plistFilePath, String key) {
- // TODO(chinmaygarde): For now, we only need to read from plist files on a mac
- // host. If this changes, we will need our own Dart plist reader.
-
- // Don't use PlistBuddy since that is not guaranteed to be installed.
- // 'defaults' requires the path to be absolute and without the 'plist'
- // extension.
- const String executable = '/usr/bin/defaults';
- if (!fs.isFileSync(executable))
- return null;
- if (!fs.isFileSync(plistFilePath))
- return null;
-
- final String normalizedPlistPath = fs.path.withoutExtension(fs.path.absolute(plistFilePath));
-
- try {
- final List<String> args = <String>[
- executable, 'read', normalizedPlistPath,
- ];
- if (key != null && key.isNotEmpty) {
- args.add(key);
- }
- final String value = runCheckedSync(args);
- return value.isEmpty ? null : value;
- } catch (error) {
- return null;
- }
-}
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index 043f651..ad46f6d 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -24,7 +24,7 @@
import '../protocol_discovery.dart';
import 'ios_workflow.dart';
import 'mac.dart';
-import 'plist_utils.dart';
+import 'plist_parser.dart';
const String _xcrunPath = '/usr/bin/xcrun';
const String iosSimulatorId = 'apple_ios_simulator';
@@ -379,7 +379,7 @@
// parsing the xcodeproj or configuration files.
// See https://github.com/flutter/flutter/issues/31037 for more information.
final String plistPath = fs.path.join(package.simulatorBundlePath, 'Info.plist');
- final String bundleIdentifier = iosWorkflow.getPlistValueFromFile(plistPath, kCFBundleIdentifierKey);
+ final String bundleIdentifier = PlistParser.instance.getValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
await SimControl.instance.launch(id, bundleIdentifier, args);
} catch (error) {
diff --git a/packages/flutter_tools/lib/src/macos/application_package.dart b/packages/flutter_tools/lib/src/macos/application_package.dart
index 278122a..e29f356 100644
--- a/packages/flutter_tools/lib/src/macos/application_package.dart
+++ b/packages/flutter_tools/lib/src/macos/application_package.dart
@@ -8,7 +8,7 @@
import '../base/file_system.dart';
import '../build_info.dart';
import '../globals.dart';
-import '../ios/plist_utils.dart' as plist;
+import '../ios/plist_parser.dart';
import '../project.dart';
/// Tests whether a [FileSystemEntity] is an macOS bundle directory
@@ -65,8 +65,9 @@
printError('Invalid prebuilt macOS app. Does not contain Info.plist.');
return null;
}
- final String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey);
- final String executableName = plist.getValueFromFile(plistPath, plist.kCFBundleExecutable);
+ final Map<String, dynamic> propertyValues = PlistParser.instance.parseFile(plistPath);
+ final String id = propertyValues[PlistParser.kCFBundleIdentifierKey];
+ final String executableName = propertyValues[PlistParser.kCFBundleExecutable];
if (id == null) {
printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier');
return null;
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index ef83cfe..1e99c89 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -17,8 +17,7 @@
import 'features.dart';
import 'flutter_manifest.dart';
import 'globals.dart';
-import 'ios/ios_workflow.dart';
-import 'ios/plist_utils.dart' as plist;
+import 'ios/plist_parser.dart';
import 'ios/xcodeproj.dart' as xcode;
import 'plugins.dart';
import 'template.dart';
@@ -361,10 +360,15 @@
/// The product bundle identifier of the host app, or null if not set or if
/// iOS tooling needed to read it is not installed.
String get productBundleIdentifier {
- final String fromPlist = iosWorkflow.getPlistValueFromFile(
- hostInfoPlist.path,
- plist.kCFBundleIdentifierKey,
- );
+ String fromPlist;
+ try {
+ fromPlist = PlistParser.instance.getValueFromFile(
+ hostInfoPlist.path,
+ PlistParser.kCFBundleIdentifierKey,
+ );
+ } on FileNotFoundException {
+ // iOS tooling not found; likely not running OSX; let [fromPlist] be null
+ }
if (fromPlist != null && !fromPlist.contains('\$')) {
// Info.plist has no build variables in product bundle ID.
return fromPlist;