diff --git a/dev/benchmarks/complex_layout/macos/Runner/Info.plist b/dev/benchmarks/complex_layout/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/dev/benchmarks/complex_layout/macos/Runner/Info.plist
+++ b/dev/benchmarks/complex_layout/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist b/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist
+++ b/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/dev/integration_tests/channels/macos/Runner/Info.plist b/dev/integration_tests/channels/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/dev/integration_tests/channels/macos/Runner/Info.plist
+++ b/dev/integration_tests/channels/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/dev/integration_tests/flavors/macos/Runner/Info.plist b/dev/integration_tests/flavors/macos/Runner/Info.plist
index e6e728b5521..eba8f244a53 100644
--- a/dev/integration_tests/flavors/macos/Runner/Info.plist
+++ b/dev/integration_tests/flavors/macos/Runner/Info.plist
@@ -29,6 +29,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist b/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist
+++ b/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/dev/integration_tests/ui/macos/Runner/Info.plist b/dev/integration_tests/ui/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/dev/integration_tests/ui/macos/Runner/Info.plist
+++ b/dev/integration_tests/ui/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/dev/manual_tests/macos/Runner/Info.plist b/dev/manual_tests/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/dev/manual_tests/macos/Runner/Info.plist
+++ b/dev/manual_tests/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/examples/api/macos/Runner/Info.plist b/examples/api/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/examples/api/macos/Runner/Info.plist
+++ b/examples/api/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/examples/flutter_view/macos/Runner/Info.plist b/examples/flutter_view/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/examples/flutter_view/macos/Runner/Info.plist
+++ b/examples/flutter_view/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/examples/hello_world/macos/Runner/Info.plist b/examples/hello_world/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/examples/hello_world/macos/Runner/Info.plist
+++ b/examples/hello_world/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/examples/image_list/macos/Runner/Info.plist b/examples/image_list/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/examples/image_list/macos/Runner/Info.plist
+++ b/examples/image_list/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/examples/layers/macos/Runner/Info.plist b/examples/layers/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/examples/layers/macos/Runner/Info.plist
+++ b/examples/layers/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/examples/platform_view/macos/Runner/Info.plist b/examples/platform_view/macos/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/examples/platform_view/macos/Runner/Info.plist
+++ b/examples/platform_view/macos/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/packages/flutter_tools/lib/src/ios/plist_parser.dart b/packages/flutter_tools/lib/src/ios/plist_parser.dart
index f10aa4554c1..c12de5e3ea7 100644
--- a/packages/flutter_tools/lib/src/ios/plist_parser.dart
+++ b/packages/flutter_tools/lib/src/ios/plist_parser.dart
@@ -30,6 +30,9 @@ class PlistParser {
static const String kCFBundleVersionKey = 'CFBundleVersion';
static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName';
static const String kMinimumOSVersionKey = 'MinimumOSVersion';
+ static const String kNSPrincipalClassKey = 'NSPrincipalClass';
+
+ static const String _plutilExecutable = '/usr/bin/plutil';
/// Returns the content, converted to XML, of the plist file located at
/// [plistFilePath].
@@ -39,12 +42,11 @@ class PlistParser {
///
/// The [plistFilePath] argument must not be null.
String? plistXmlContent(String plistFilePath) {
- const String executable = '/usr/bin/plutil';
- if (!_fileSystem.isFileSync(executable)) {
- throw const FileNotFoundException(executable);
+ if (!_fileSystem.isFileSync(_plutilExecutable)) {
+ throw const FileNotFoundException(_plutilExecutable);
}
final List args = [
- executable, '-convert', 'xml1', '-o', '-', plistFilePath,
+ _plutilExecutable, '-convert', 'xml1', '-o', '-', plistFilePath,
];
try {
final String xmlContent = _processUtils.runSync(
@@ -53,11 +55,42 @@ class PlistParser {
).stdout.trim();
return xmlContent;
} on ProcessException catch (error) {
- _logger.printTrace('$error');
+ _logger.printError('$error');
return null;
}
}
+ /// Replaces the string key in the given plist file with the given value.
+ ///
+ /// If the value is null, then the key will be removed.
+ ///
+ /// Returns true if successful.
+ bool replaceKey(String plistFilePath, {required String key, String? value }) {
+ if (!_fileSystem.isFileSync(_plutilExecutable)) {
+ throw const FileNotFoundException(_plutilExecutable);
+ }
+ final List args;
+ if (value == null) {
+ args = [
+ _plutilExecutable, '-remove', key, plistFilePath,
+ ];
+ } else {
+ args = [
+ _plutilExecutable, '-replace', key, '-string', value, plistFilePath,
+ ];
+ }
+ try {
+ _processUtils.runSync(
+ args,
+ throwOnError: true,
+ );
+ } on ProcessException catch (error) {
+ _logger.printError('$error');
+ return false;
+ }
+ return true;
+ }
+
/// Parses the plist file located at [plistFilePath] and returns the
/// associated map of key/value property list pairs.
///
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 6b9c39eb862..85bf8369d50 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -17,6 +17,7 @@ import '../migrations/xcode_script_build_phase_migration.dart';
import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart';
import '../project.dart';
import 'cocoapod_utils.dart';
+import 'migrations/flutter_application_migration.dart';
import 'migrations/macos_deployment_target_migration.dart';
import 'migrations/remove_macos_framework_link_and_embedding_migration.dart';
@@ -57,6 +58,7 @@ Future buildMacOS({
XcodeProjectObjectVersionMigration(flutterProject.macos, globals.logger),
XcodeScriptBuildPhaseMigration(flutterProject.macos, globals.logger),
XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger),
+ FlutterApplicationMigration(flutterProject.macos, globals.logger),
];
final ProjectMigration migration = ProjectMigration(migrators);
diff --git a/packages/flutter_tools/lib/src/macos/migrations/flutter_application_migration.dart b/packages/flutter_tools/lib/src/macos/migrations/flutter_application_migration.dart
new file mode 100644
index 00000000000..14ea4d64ffe
--- /dev/null
+++ b/packages/flutter_tools/lib/src/macos/migrations/flutter_application_migration.dart
@@ -0,0 +1,48 @@
+// Copyright 2014 The Flutter 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/project_migrator.dart';
+import '../../globals.dart' as globals;
+import '../../ios/plist_parser.dart';
+import '../../xcode_project.dart';
+
+/// Update the minimum macOS deployment version to the minimum allowed by Xcode without causing a warning.
+class FlutterApplicationMigration extends ProjectMigrator {
+ FlutterApplicationMigration(
+ MacOSProject project,
+ super.logger,
+ ) : _infoPlistFile = project.defaultHostInfoPlist;
+
+ final File _infoPlistFile;
+
+ @override
+ void migrate() {
+ if (_infoPlistFile.existsSync()) {
+ final String? principleClass =
+ globals.plistParser.getStringValueFromFile(_infoPlistFile.path, PlistParser.kNSPrincipalClassKey);
+ if (principleClass == null || principleClass == 'FlutterApplication') {
+ // No NSPrincipalClass defined, or already converted, so no migration
+ // needed.
+ return;
+ }
+ if (principleClass != 'NSApplication') {
+ // Only replace NSApplication values, since we don't know why they might
+ // have substituted something else.
+ logger.printTrace('${_infoPlistFile.basename} has an '
+ '${PlistParser.kNSPrincipalClassKey} of $principleClass, not '
+ 'NSApplication, skipping FlutterApplication migration.\nYou will need '
+ 'to modify your application class to derive from FlutterApplication.');
+ return;
+ }
+ logger.printStatus('Updating ${_infoPlistFile.basename} to use FlutterApplication instead of NSApplication.');
+ final bool success = globals.plistParser.replaceKey(_infoPlistFile.path, key: PlistParser.kNSPrincipalClassKey, value: 'FlutterApplication');
+ if (!success) {
+ logger.printError('Updating ${_infoPlistFile.basename} failed.');
+ }
+ } else {
+ logger.printTrace('${_infoPlistFile.basename} not found, skipping FlutterApplication migration.');
+ }
+ }
+}
diff --git a/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist b/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist
index 4789daa6a44..55c6534ef34 100644
--- a/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist
+++ b/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist
@@ -27,6 +27,6 @@
NSMainNibFile
MainMenu
NSPrincipalClass
- NSApplication
+ FlutterApplication
diff --git a/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart
index 2b8491d751d..cfcd32e623c 100644
--- a/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart
@@ -5,13 +5,17 @@
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/ios/plist_parser.dart';
+import 'package:flutter_tools/src/macos/migrations/flutter_application_migration.dart';
import 'package:flutter_tools/src/macos/migrations/macos_deployment_target_migration.dart';
import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart';
+import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
-import 'package:flutter_tools/src/xcode_project.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/fakes.dart';
void main() {
group('remove link and embed migration', () {
@@ -275,12 +279,107 @@ platform :osx, '10.14'
expect('Updating minimum macOS deployment target to 10.14'.allMatches(testLogger.statusText).length, 1);
});
});
+
+ group('update NSPrincipalClass to FlutterApplication', () {
+ late MemoryFileSystem memoryFileSystem;
+ late BufferLogger testLogger;
+ late FakeMacOSProject project;
+ late File infoPlistFile;
+ late FakePlistParser fakePlistParser;
+ late FlutterProjectFactory flutterProjectFactory;
+
+ setUp(() {
+ memoryFileSystem = MemoryFileSystem();
+ fakePlistParser = FakePlistParser();
+ testLogger = BufferLogger.test();
+ project = FakeMacOSProject();
+ infoPlistFile = memoryFileSystem.file('Info.plist');
+ project.defaultHostInfoPlist = infoPlistFile;
+ flutterProjectFactory = FlutterProjectFactory(
+ fileSystem: memoryFileSystem,
+ logger: testLogger,
+ );
+ });
+
+ void testWithMocks(String description, Future Function() testMethod) {
+ testUsingContext(description, testMethod, overrides: {
+ FileSystem: () => memoryFileSystem,
+ ProcessManager: () => FakeProcessManager.any(),
+ PlistParser: () => fakePlistParser,
+ FlutterProjectFactory: () => flutterProjectFactory,
+ });
+ }
+
+ testWithMocks('skipped if files are missing', () async {
+ final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
+ project,
+ testLogger,
+ );
+ macOSProjectMigration.migrate();
+ expect(infoPlistFile.existsSync(), isFalse);
+
+ expect(testLogger.traceText, contains('${infoPlistFile.basename} not found, skipping FlutterApplication migration.'));
+ expect(testLogger.statusText, isEmpty);
+ });
+
+ testWithMocks('skipped if no NSPrincipalClass key exists to upgrade', () async {
+ final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
+ project,
+ testLogger,
+ );
+ infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
+ macOSProjectMigration.migrate();
+ expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull);
+ expect(testLogger.statusText, isEmpty);
+ });
+
+ testWithMocks('skipped if already upgraded', () async {
+ fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'FlutterApplication');
+ final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
+ project,
+ testLogger,
+ );
+ infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
+ macOSProjectMigration.migrate();
+ expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'FlutterApplication');
+ expect(testLogger.statusText, isEmpty);
+ });
+
+ testWithMocks('Info.plist migrated to use FlutterApplication', () async {
+ fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'NSApplication');
+ final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
+ project,
+ testLogger,
+ );
+ infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
+ macOSProjectMigration.migrate();
+ expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'FlutterApplication');
+ // Only print once.
+ expect('Updating ${infoPlistFile.basename} to use FlutterApplication instead of NSApplication.'.allMatches(testLogger.statusText).length, 1);
+ });
+
+ testWithMocks('Skip if NSPrincipalClass is not NSApplication', () async {
+ const String differentApp = 'DIFFERENTApplication';
+ fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, differentApp);
+ final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
+ project,
+ testLogger,
+ );
+ infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
+ macOSProjectMigration.migrate();
+ expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp);
+ expect(testLogger.traceText, contains('${infoPlistFile.basename} has an ${PlistParser.kNSPrincipalClassKey} of $differentApp, not NSApplication, skipping FlutterApplication migration'));
+ });
+ });
}
class FakeMacOSProject extends Fake implements MacOSProject {
@override
File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile');
+ @override
+ File defaultHostInfoPlist = MemoryFileSystem.test().file('InfoplistFile');
+
@override
File podfile = MemoryFileSystem.test().file('Podfile');
}
diff --git a/packages/flutter_tools/test/integration.shard/plist_parser_test.dart b/packages/flutter_tools/test/integration.shard/plist_parser_test.dart
index dc8d2e8fa56..9f176dc0117 100644
--- a/packages/flutter_tools/test/integration.shard/plist_parser_test.dart
+++ b/packages/flutter_tools/test/integration.shard/plist_parser_test.dart
@@ -74,7 +74,7 @@ void main() {
file.deleteSync();
});
- testWithoutContext('PlistParser.getStringValueFromFile works with xml file', () {
+ testWithoutContext('PlistParser.getStringValueFromFile works with an XML file', () {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
@@ -83,7 +83,7 @@ void main() {
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
- testWithoutContext('PlistParser.getStringValueFromFile works with binary file', () {
+ testWithoutContext('PlistParser.getStringValueFromFile works with a binary file', () {
file.writeAsBytesSync(base64.decode(base64PlistBinary));
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
@@ -92,7 +92,7 @@ void main() {
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
- testWithoutContext('PlistParser.getStringValueFromFile works with json file', () {
+ testWithoutContext('PlistParser.getStringValueFromFile works with a JSON file', () {
file.writeAsBytesSync(base64.decode(base64PlistJson));
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
@@ -101,13 +101,13 @@ void main() {
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
- testWithoutContext('PlistParser.getStringValueFromFile returns null for non-existent plist file', () {
+ testWithoutContext('PlistParser.getStringValueFromFile returns null for a non-existent plist file', () {
expect(parser.getStringValueFromFile('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 non-existent key within plist', () {
+ testWithoutContext('PlistParser.getStringValueFromFile returns null for a non-existent key within a plist', () {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(parser.getStringValueFromFile(file.path, 'BadKey'), null);
@@ -116,12 +116,15 @@ void main() {
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
- testWithoutContext('PlistParser.getStringValueFromFile returns null for malformed plist file', () {
+ testWithoutContext('PlistParser.getStringValueFromFile returns null for a malformed plist file', () {
file.writeAsBytesSync(const [1, 2, 3, 4, 5, 6]);
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), null);
- expect(logger.statusText, isNotEmpty);
- expect(logger.errorText, isEmpty);
+ 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'));
+ expect(logger.errorText, 'ProcessException: The command failed\n'
+ ' 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 {
@@ -133,7 +136,68 @@ void main() {
);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
- }, skip: platform.isMacOS); // [intended] requires macos tool chain.
+ }, skip: platform.isMacOS); // [intended] requires absence of macos tool chain.
+
+ 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.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'));
+ }, 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.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'));
+ }, skip: !platform.isMacOS); // [intended] requires macos tool chain.
+
+ testWithoutContext('PlistParser.replaceKey can delete a key', () async {
+ file.writeAsBytesSync(base64.decode(base64PlistXml));
+
+ expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier'), isTrue);
+ expect(logger.statusText, isEmpty);
+ expect(logger.errorText, isEmpty);
+ expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), isNull);
+ }, skip: !platform.isMacOS); // [intended] requires macos tool chain.
+
+ testWithoutContext('PlistParser.replaceKey throws when /usr/bin/plutil is not found', () async {
+ file.writeAsBytesSync(base64.decode(base64PlistXml));
+
+ expect(
+ () => parser.replaceKey(file.path, key: 'CFBundleIdentifier', value: 'dev.flutter.fake'),
+ throwsA(isA()),
+ );
+ expect(logger.statusText, isEmpty);
+ expect(logger.errorText, isEmpty);
+ }, skip: platform.isMacOS); // [intended] requires absence of macos tool chain.
+
+ testWithoutContext('PlistParser.replaceKey returns false for a malformed plist file', () {
+ file.writeAsBytesSync(const [1, 2, 3, 4, 5, 6]);
+
+ expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier', value: 'dev.flutter.fake'), isFalse);
+ expect(logger.statusText, contains('foo.plist: 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'));
+ expect(logger.errorText, equals('ProcessException: The command failed\n'
+ ' Command: /usr/bin/plutil -replace CFBundleIdentifier -string dev.flutter.fake foo.plist\n'));
+ }, skip: !platform.isMacOS); // [intended] requires macos tool chain.
+
+ 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.replaceKey(file.path, key:'CFBundleIdentifier', value: 'dev.flutter.fake'), isTrue);
+ expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'dev.flutter.fake');
+ expect(logger.statusText, isEmpty);
+ expect(logger.errorText, isEmpty);
+ }, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.parseFile can handle different datatypes', () async {
file.writeAsBytesSync(base64.decode(base64PlistXmlWithComplexDatatypes));
diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart
index 4bb5733e751..808d5c6bf88 100644
--- a/packages/flutter_tools/test/src/fakes.dart
+++ b/packages/flutter_tools/test/src/fakes.dart
@@ -298,6 +298,16 @@ class FakePlistParser implements PlistParser {
String? getStringValueFromFile(String plistFilePath, String key) {
return _underlyingValues[key] as String?;
}
+
+ @override
+ bool replaceKey(String plistFilePath, {required String key, String? value}) {
+ if (value == null) {
+ _underlyingValues.remove(key);
+ return true;
+ }
+ setProperty(key, value);
+ return true;
+ }
}
class FakeBotDetector implements BotDetector {