From ac17873a5e7ead6dc3ad1b6de534128024b99fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Sharma?= <737941+loic-sharma@users.noreply.github.com> Date: Wed, 17 Apr 2024 20:08:36 -0700 Subject: [PATCH] [macOS] Migrate @NSApplicationMain attribute to @main (#146848) This migrates Flutter to use the `@main` attribute introduced in Swift 5.3. The `@NSApplicationMain` attribute is deprecated and will be removed in Swift 6. See: https://github.com/apple/swift-evolution/blob/main/proposals/0383-deprecate-uiapplicationmain-and-nsapplicationmain.md This change is split into two commits: 1. https://github.com/flutter/flutter/commit/a508d3e5038c36849448f6b0b2567e8d416842fa - This updates the macOS app template and adds a migration to replace `@NSApplicationMain` uses with `@main`. 2. https://github.com/flutter/flutter/commit/f43482786edafc48ee90bea607704c4a0c8cb96f - I ran `flutter run -d macos` on each Flutter macOS app in this repository to verify the app migrates and launches successfully. Follow-up to https://github.com/flutter/flutter/pull/146707 Fixes https://github.com/flutter/flutter/issues/143044 --- .../macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- .../channels/macos/Runner/AppDelegate.swift | 2 +- .../flavors/macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- .../ui/macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- examples/api/macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- .../image_list/macos/Runner/AppDelegate.swift | 2 +- .../layers/macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- .../macos/Runner/AppDelegate.swift | 2 +- .../lib/src/macos/build_macos.dart | 2 + ...applicationmain_deprecation_migration.dart | 51 +++++++++++ .../flutter_tools/lib/src/xcode_project.dart | 3 + .../macos.tmpl/Runner/AppDelegate.swift | 2 +- .../macos/macos_project_migration_test.dart | 90 +++++++++++++++++++ 20 files changed, 162 insertions(+), 16 deletions(-) create mode 100644 packages/flutter_tools/lib/src/macos/migrations/nsapplicationmain_deprecation_migration.dart diff --git a/dev/a11y_assessments/macos/Runner/AppDelegate.swift b/dev/a11y_assessments/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/a11y_assessments/macos/Runner/AppDelegate.swift +++ b/dev/a11y_assessments/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/dev/benchmarks/complex_layout/macos/Runner/AppDelegate.swift b/dev/benchmarks/complex_layout/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/benchmarks/complex_layout/macos/Runner/AppDelegate.swift +++ b/dev/benchmarks/complex_layout/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/dev/benchmarks/macrobenchmarks/macos/Runner/AppDelegate.swift b/dev/benchmarks/macrobenchmarks/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/benchmarks/macrobenchmarks/macos/Runner/AppDelegate.swift +++ b/dev/benchmarks/macrobenchmarks/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/dev/integration_tests/channels/macos/Runner/AppDelegate.swift b/dev/integration_tests/channels/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/integration_tests/channels/macos/Runner/AppDelegate.swift +++ b/dev/integration_tests/channels/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/dev/integration_tests/flavors/macos/Runner/AppDelegate.swift b/dev/integration_tests/flavors/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/integration_tests/flavors/macos/Runner/AppDelegate.swift +++ b/dev/integration_tests/flavors/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/dev/integration_tests/flutter_gallery/macos/Runner/AppDelegate.swift b/dev/integration_tests/flutter_gallery/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/integration_tests/flutter_gallery/macos/Runner/AppDelegate.swift +++ b/dev/integration_tests/flutter_gallery/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/dev/integration_tests/ui/macos/Runner/AppDelegate.swift b/dev/integration_tests/ui/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/integration_tests/ui/macos/Runner/AppDelegate.swift +++ b/dev/integration_tests/ui/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/dev/manual_tests/macos/Runner/AppDelegate.swift b/dev/manual_tests/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/dev/manual_tests/macos/Runner/AppDelegate.swift +++ b/dev/manual_tests/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/examples/api/macos/Runner/AppDelegate.swift b/examples/api/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/examples/api/macos/Runner/AppDelegate.swift +++ b/examples/api/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/examples/flutter_view/macos/Runner/AppDelegate.swift b/examples/flutter_view/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/examples/flutter_view/macos/Runner/AppDelegate.swift +++ b/examples/flutter_view/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/examples/hello_world/macos/Runner/AppDelegate.swift b/examples/hello_world/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/examples/hello_world/macos/Runner/AppDelegate.swift +++ b/examples/hello_world/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/examples/image_list/macos/Runner/AppDelegate.swift b/examples/image_list/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/examples/image_list/macos/Runner/AppDelegate.swift +++ b/examples/image_list/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/examples/layers/macos/Runner/AppDelegate.swift b/examples/layers/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/examples/layers/macos/Runner/AppDelegate.swift +++ b/examples/layers/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/examples/platform_channel/macos/Runner/AppDelegate.swift b/examples/platform_channel/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/examples/platform_channel/macos/Runner/AppDelegate.swift +++ b/examples/platform_channel/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/examples/platform_view/macos/Runner/AppDelegate.swift b/examples/platform_view/macos/Runner/AppDelegate.swift index d080d41951d3..73cc5fd1077c 100644 --- a/examples/platform_view/macos/Runner/AppDelegate.swift +++ b/examples/platform_view/macos/Runner/AppDelegate.swift @@ -5,7 +5,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index dbbff107c8d1..e4307170b1c8 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -24,6 +24,7 @@ import 'application_package.dart'; import 'cocoapod_utils.dart'; import 'migrations/flutter_application_migration.dart'; import 'migrations/macos_deployment_target_migration.dart'; +import 'migrations/nsapplicationmain_deprecation_migration.dart'; import 'migrations/remove_macos_framework_link_and_embedding_migration.dart'; /// When run in -quiet mode, Xcode should only print from the underlying tasks to stdout. @@ -83,6 +84,7 @@ Future buildMacOS({ XcodeScriptBuildPhaseMigration(flutterProject.macos, globals.logger), XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger), FlutterApplicationMigration(flutterProject.macos, globals.logger), + NSApplicationMainDeprecationMigration(flutterProject.macos, globals.logger), ]; final ProjectMigration migration = ProjectMigration(migrators); diff --git a/packages/flutter_tools/lib/src/macos/migrations/nsapplicationmain_deprecation_migration.dart b/packages/flutter_tools/lib/src/macos/migrations/nsapplicationmain_deprecation_migration.dart new file mode 100644 index 000000000000..3bca342eb57a --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/migrations/nsapplicationmain_deprecation_migration.dart @@ -0,0 +1,51 @@ +// 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 '../../xcode_project.dart'; + +const String _appDelegateFileBefore = r''' +@NSApplicationMain +class AppDelegate'''; + +const String _appDelegateFileAfter = r''' +@main +class AppDelegate'''; + +/// Replace the deprecated `@NSApplicationMain` attribute with `@main`. +/// +/// See: +/// https://github.com/apple/swift-evolution/blob/main/proposals/0383-deprecate-uiapplicationmain-and-nsapplicationmain.md +class NSApplicationMainDeprecationMigration extends ProjectMigrator { + NSApplicationMainDeprecationMigration( + MacOSProject project, + super.logger, + ) : _appDelegateSwift = project.appDelegateSwift; + + final File _appDelegateSwift; + + @override + Future migrate() async { + // Skip this migration if the project uses Objective-C. + if (!_appDelegateSwift.existsSync()) { + logger.printTrace( + 'macos/Runner/AppDelegate.swift not found, skipping @main migration.', + ); + return; + } + + // Migrate the macos/Runner/AppDelegate.swift file. + final String original = _appDelegateSwift.readAsStringSync(); + final String migrated = original.replaceFirst(_appDelegateFileBefore, _appDelegateFileAfter); + if (original == migrated) { + return; + } + + logger.printWarning( + 'macos/Runner/AppDelegate.swift uses the deprecated @NSApplicationMain attribute, updating.', + ); + _appDelegateSwift.writeAsStringSync(migrated); + } +} diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart index 9852592fa7e6..2b5194107714 100644 --- a/packages/flutter_tools/lib/src/xcode_project.dart +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -715,6 +715,9 @@ class MacOSProject extends XcodeBasedProject { File get pluginRegistrantImplementation => managedDirectory.childFile('GeneratedPluginRegistrant.swift'); + /// The 'AppDelegate.swift' file of the host app. This file might not exist if the app project uses Objective-C. + File get appDelegateSwift => hostAppRoot.childDirectory('Runner').childFile('AppDelegate.swift'); + @override File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig'); diff --git a/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/AppDelegate.swift b/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/AppDelegate.swift index d53ef6437726..8e02df288835 100644 --- a/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/AppDelegate.swift +++ b/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true 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 0348ec3bdfed..027147a5cae0 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 @@ -8,6 +8,7 @@ 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/nsapplicationmain_deprecation_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'; @@ -400,6 +401,92 @@ platform :osx, '10.14' expect(testLogger.traceText, isEmpty); }); }); + + group('migrate @NSApplicationMain attribute to @main', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger testLogger; + late FakeMacOSProject project; + late File appDelegateFile; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + testLogger = BufferLogger.test(); + project = FakeMacOSProject(); + appDelegateFile = memoryFileSystem.file('AppDelegate.swift'); + project.appDelegateSwift = appDelegateFile; + }); + + testWithoutContext('skipped if files are missing', () async { + final NSApplicationMainDeprecationMigration migration = NSApplicationMainDeprecationMigration( + project, + testLogger, + ); + await migration.migrate(); + expect(appDelegateFile.existsSync(), isFalse); + + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('skipped if nothing to upgrade', () async { + const String appDelegateContents = ''' +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} +'''; + appDelegateFile.writeAsStringSync(appDelegateContents); + final DateTime lastModified = appDelegateFile.lastModifiedSync(); + + final NSApplicationMainDeprecationMigration migration = NSApplicationMainDeprecationMigration( + project, + testLogger, + ); + await migration.migrate(); + + expect(appDelegateFile.lastModifiedSync(), lastModified); + expect(appDelegateFile.readAsStringSync(), appDelegateContents); + + expect(testLogger.statusText, isEmpty); + }); + + testWithoutContext('updates AppDelegate.swift', () async { + appDelegateFile.writeAsStringSync(''' +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} +'''); + + final NSApplicationMainDeprecationMigration migration = NSApplicationMainDeprecationMigration( + project, + testLogger, + ); + await migration.migrate(); + + expect(appDelegateFile.readAsStringSync(), ''' +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} +'''); + expect(testLogger.warningText, contains('uses the deprecated @NSApplicationMain attribute, updating')); + }); + }); } class FakeMacOSProject extends Fake implements MacOSProject { @@ -411,4 +498,7 @@ class FakeMacOSProject extends Fake implements MacOSProject { @override File podfile = MemoryFileSystem.test().file('Podfile'); + + @override + File appDelegateSwift = MemoryFileSystem.test().file('AppDelegate.swift'); }