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/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) {