Skip to content

Commit c2f5bf9

Browse files
authored
Add macos project auto migration code for FlutterApplication (#122336)
Add macos project auto migration code for FlutterApplication
1 parent 2ba0d0d commit c2f5bf9

File tree

20 files changed

+285
-29
lines changed

20 files changed

+285
-29
lines changed

dev/benchmarks/complex_layout/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

dev/integration_tests/channels/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

dev/integration_tests/flavors/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
<key>NSMainNibFile</key>
3030
<string>MainMenu</string>
3131
<key>NSPrincipalClass</key>
32-
<string>NSApplication</string>
32+
<string>FlutterApplication</string>
3333
</dict>
3434
</plist>

dev/integration_tests/flutter_gallery/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

dev/integration_tests/ui/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

dev/manual_tests/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

examples/api/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

examples/flutter_view/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

examples/hello_world/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

examples/image_list/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

examples/layers/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

examples/platform_view/macos/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

packages/flutter_tools/lib/src/ios/plist_parser.dart

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class PlistParser {
3030
static const String kCFBundleVersionKey = 'CFBundleVersion';
3131
static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName';
3232
static const String kMinimumOSVersionKey = 'MinimumOSVersion';
33+
static const String kNSPrincipalClassKey = 'NSPrincipalClass';
34+
35+
static const String _plutilExecutable = '/usr/bin/plutil';
3336

3437
/// Returns the content, converted to XML, of the plist file located at
3538
/// [plistFilePath].
@@ -39,12 +42,11 @@ class PlistParser {
3942
///
4043
/// The [plistFilePath] argument must not be null.
4144
String? plistXmlContent(String plistFilePath) {
42-
const String executable = '/usr/bin/plutil';
43-
if (!_fileSystem.isFileSync(executable)) {
44-
throw const FileNotFoundException(executable);
45+
if (!_fileSystem.isFileSync(_plutilExecutable)) {
46+
throw const FileNotFoundException(_plutilExecutable);
4547
}
4648
final List<String> args = <String>[
47-
executable, '-convert', 'xml1', '-o', '-', plistFilePath,
49+
_plutilExecutable, '-convert', 'xml1', '-o', '-', plistFilePath,
4850
];
4951
try {
5052
final String xmlContent = _processUtils.runSync(
@@ -53,11 +55,42 @@ class PlistParser {
5355
).stdout.trim();
5456
return xmlContent;
5557
} on ProcessException catch (error) {
56-
_logger.printTrace('$error');
58+
_logger.printError('$error');
5759
return null;
5860
}
5961
}
6062

63+
/// Replaces the string key in the given plist file with the given value.
64+
///
65+
/// If the value is null, then the key will be removed.
66+
///
67+
/// Returns true if successful.
68+
bool replaceKey(String plistFilePath, {required String key, String? value }) {
69+
if (!_fileSystem.isFileSync(_plutilExecutable)) {
70+
throw const FileNotFoundException(_plutilExecutable);
71+
}
72+
final List<String> args;
73+
if (value == null) {
74+
args = <String>[
75+
_plutilExecutable, '-remove', key, plistFilePath,
76+
];
77+
} else {
78+
args = <String>[
79+
_plutilExecutable, '-replace', key, '-string', value, plistFilePath,
80+
];
81+
}
82+
try {
83+
_processUtils.runSync(
84+
args,
85+
throwOnError: true,
86+
);
87+
} on ProcessException catch (error) {
88+
_logger.printError('$error');
89+
return false;
90+
}
91+
return true;
92+
}
93+
6194
/// Parses the plist file located at [plistFilePath] and returns the
6295
/// associated map of key/value property list pairs.
6396
///

packages/flutter_tools/lib/src/macos/build_macos.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '../migrations/xcode_script_build_phase_migration.dart';
1717
import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart';
1818
import '../project.dart';
1919
import 'cocoapod_utils.dart';
20+
import 'migrations/flutter_application_migration.dart';
2021
import 'migrations/macos_deployment_target_migration.dart';
2122
import 'migrations/remove_macos_framework_link_and_embedding_migration.dart';
2223

@@ -57,6 +58,7 @@ Future<void> buildMacOS({
5758
XcodeProjectObjectVersionMigration(flutterProject.macos, globals.logger),
5859
XcodeScriptBuildPhaseMigration(flutterProject.macos, globals.logger),
5960
XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger),
61+
FlutterApplicationMigration(flutterProject.macos, globals.logger),
6062
];
6163

6264
final ProjectMigration migration = ProjectMigration(migrators);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import '../../base/file_system.dart';
6+
import '../../base/project_migrator.dart';
7+
import '../../globals.dart' as globals;
8+
import '../../ios/plist_parser.dart';
9+
import '../../xcode_project.dart';
10+
11+
/// Update the minimum macOS deployment version to the minimum allowed by Xcode without causing a warning.
12+
class FlutterApplicationMigration extends ProjectMigrator {
13+
FlutterApplicationMigration(
14+
MacOSProject project,
15+
super.logger,
16+
) : _infoPlistFile = project.defaultHostInfoPlist;
17+
18+
final File _infoPlistFile;
19+
20+
@override
21+
void migrate() {
22+
if (_infoPlistFile.existsSync()) {
23+
final String? principleClass =
24+
globals.plistParser.getStringValueFromFile(_infoPlistFile.path, PlistParser.kNSPrincipalClassKey);
25+
if (principleClass == null || principleClass == 'FlutterApplication') {
26+
// No NSPrincipalClass defined, or already converted, so no migration
27+
// needed.
28+
return;
29+
}
30+
if (principleClass != 'NSApplication') {
31+
// Only replace NSApplication values, since we don't know why they might
32+
// have substituted something else.
33+
logger.printTrace('${_infoPlistFile.basename} has an '
34+
'${PlistParser.kNSPrincipalClassKey} of $principleClass, not '
35+
'NSApplication, skipping FlutterApplication migration.\nYou will need '
36+
'to modify your application class to derive from FlutterApplication.');
37+
return;
38+
}
39+
logger.printStatus('Updating ${_infoPlistFile.basename} to use FlutterApplication instead of NSApplication.');
40+
final bool success = globals.plistParser.replaceKey(_infoPlistFile.path, key: PlistParser.kNSPrincipalClassKey, value: 'FlutterApplication');
41+
if (!success) {
42+
logger.printError('Updating ${_infoPlistFile.basename} failed.');
43+
}
44+
} else {
45+
logger.printTrace('${_infoPlistFile.basename} not found, skipping FlutterApplication migration.');
46+
}
47+
}
48+
}

packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@
2727
<key>NSMainNibFile</key>
2828
<string>MainMenu</string>
2929
<key>NSPrincipalClass</key>
30-
<string>NSApplication</string>
30+
<string>FlutterApplication</string>
3131
</dict>
3232
</plist>

packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
import 'package:file/file.dart';
66
import 'package:file/memory.dart';
77
import 'package:flutter_tools/src/base/logger.dart';
8+
import 'package:flutter_tools/src/ios/plist_parser.dart';
9+
import 'package:flutter_tools/src/macos/migrations/flutter_application_migration.dart';
810
import 'package:flutter_tools/src/macos/migrations/macos_deployment_target_migration.dart';
911
import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart';
12+
import 'package:flutter_tools/src/project.dart';
1013
import 'package:flutter_tools/src/reporting/reporting.dart';
11-
import 'package:flutter_tools/src/xcode_project.dart';
1214
import 'package:test/fake.dart';
1315

1416
import '../../src/common.dart';
17+
import '../../src/context.dart';
18+
import '../../src/fakes.dart';
1519

1620
void main() {
1721
group('remove link and embed migration', () {
@@ -275,12 +279,107 @@ platform :osx, '10.14'
275279
expect('Updating minimum macOS deployment target to 10.14'.allMatches(testLogger.statusText).length, 1);
276280
});
277281
});
282+
283+
group('update NSPrincipalClass to FlutterApplication', () {
284+
late MemoryFileSystem memoryFileSystem;
285+
late BufferLogger testLogger;
286+
late FakeMacOSProject project;
287+
late File infoPlistFile;
288+
late FakePlistParser fakePlistParser;
289+
late FlutterProjectFactory flutterProjectFactory;
290+
291+
setUp(() {
292+
memoryFileSystem = MemoryFileSystem();
293+
fakePlistParser = FakePlistParser();
294+
testLogger = BufferLogger.test();
295+
project = FakeMacOSProject();
296+
infoPlistFile = memoryFileSystem.file('Info.plist');
297+
project.defaultHostInfoPlist = infoPlistFile;
298+
flutterProjectFactory = FlutterProjectFactory(
299+
fileSystem: memoryFileSystem,
300+
logger: testLogger,
301+
);
302+
});
303+
304+
void testWithMocks(String description, Future<void> Function() testMethod) {
305+
testUsingContext(description, testMethod, overrides: <Type, Generator>{
306+
FileSystem: () => memoryFileSystem,
307+
ProcessManager: () => FakeProcessManager.any(),
308+
PlistParser: () => fakePlistParser,
309+
FlutterProjectFactory: () => flutterProjectFactory,
310+
});
311+
}
312+
313+
testWithMocks('skipped if files are missing', () async {
314+
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
315+
project,
316+
testLogger,
317+
);
318+
macOSProjectMigration.migrate();
319+
expect(infoPlistFile.existsSync(), isFalse);
320+
321+
expect(testLogger.traceText, contains('${infoPlistFile.basename} not found, skipping FlutterApplication migration.'));
322+
expect(testLogger.statusText, isEmpty);
323+
});
324+
325+
testWithMocks('skipped if no NSPrincipalClass key exists to upgrade', () async {
326+
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
327+
project,
328+
testLogger,
329+
);
330+
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
331+
macOSProjectMigration.migrate();
332+
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull);
333+
expect(testLogger.statusText, isEmpty);
334+
});
335+
336+
testWithMocks('skipped if already upgraded', () async {
337+
fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'FlutterApplication');
338+
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
339+
project,
340+
testLogger,
341+
);
342+
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
343+
macOSProjectMigration.migrate();
344+
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'FlutterApplication');
345+
expect(testLogger.statusText, isEmpty);
346+
});
347+
348+
testWithMocks('Info.plist migrated to use FlutterApplication', () async {
349+
fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'NSApplication');
350+
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
351+
project,
352+
testLogger,
353+
);
354+
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
355+
macOSProjectMigration.migrate();
356+
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'FlutterApplication');
357+
// Only print once.
358+
expect('Updating ${infoPlistFile.basename} to use FlutterApplication instead of NSApplication.'.allMatches(testLogger.statusText).length, 1);
359+
});
360+
361+
testWithMocks('Skip if NSPrincipalClass is not NSApplication', () async {
362+
const String differentApp = 'DIFFERENTApplication';
363+
fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, differentApp);
364+
final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration(
365+
project,
366+
testLogger,
367+
);
368+
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
369+
macOSProjectMigration.migrate();
370+
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp);
371+
expect(testLogger.traceText, contains('${infoPlistFile.basename} has an ${PlistParser.kNSPrincipalClassKey} of $differentApp, not NSApplication, skipping FlutterApplication migration'));
372+
});
373+
});
278374
}
279375

280376
class FakeMacOSProject extends Fake implements MacOSProject {
281377
@override
282378
File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile');
283379

380+
@override
381+
File defaultHostInfoPlist = MemoryFileSystem.test().file('InfoplistFile');
382+
284383
@override
285384
File podfile = MemoryFileSystem.test().file('Podfile');
286385
}

0 commit comments

Comments
 (0)