Unverified Commit 0412977e authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Support create for macOS (app and plugin) (#40851)

Adds macOS support for `flutter create`:
- Currently it is behind a hidden flag.
- Adds a TargetPlatform workaround to lib/main.dart in the standard app template when enabled.
- Supports `app` and `plugin`; `module` support doesn't yet exist for macOS in general.

This will eliminate the need to use FDE's examples as templates on macOS. The templates are based on the current state of FDE's examples, with templating support added (and with adoption of the new application delegate in the app, which hadn't been done yet in FDE, eliminating some boilerplate from the template).

Fixes #30703 
parent 86c342ec
......@@ -145,6 +145,14 @@ class CreateCommand extends FlutterCommand {
help: 'Generate a project using the AndroidX support libraries',
);
// Deprecated
argParser.addFlag(
'macos',
negatable: true,
defaultsTo: false,
hide: true,
help: 'Include support for building a macOS application',
);
// Deprecated
argParser.addFlag(
'web',
negatable: true,
......@@ -385,6 +393,7 @@ class CreateCommand extends FlutterCommand {
androidLanguage: argResults['android-language'],
iosLanguage: argResults['ios-language'],
web: featureFlags.isWebEnabled,
macos: argResults['macos'],
);
final String relativeDirPath = fs.path.relative(projectDirPath);
......@@ -595,6 +604,7 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi
bool renderDriverTest = false,
bool withPluginHook = false,
bool web = false,
bool macos = false,
}) {
flutterRoot = fs.path.normalize(flutterRoot);
......@@ -602,12 +612,14 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi
final String pluginClass = pluginDartClass.endsWith('Plugin')
? pluginDartClass
: pluginDartClass + 'Plugin';
final String appleIdentifier = _createUTIIdentifier(organization, projectName);
return <String, dynamic>{
'organization': organization,
'projectName': projectName,
'androidIdentifier': _createAndroidIdentifier(organization, projectName),
'iosIdentifier': _createUTIIdentifier(organization, projectName),
'iosIdentifier': appleIdentifier,
'macosIdentifier': appleIdentifier,
'description': projectDescription,
'dartSdk': '$flutterRoot/bin/cache/dart-sdk',
'androidX': androidX,
......@@ -623,6 +635,14 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi
'flutterRevision': FlutterVersion.instance.frameworkRevision,
'flutterChannel': FlutterVersion.instance.channel,
'web': web,
'macos': macos,
'year': DateTime.now().year,
// For now, the new plugin schema is only used when a desktop plugin is
// enabled. Once the new schema is supported on stable, this should be
// removed, and the new schema should always be used.
'useNewPluginSchema': macos,
// If a desktop platform is included, add a workaround for #31366.
'includeTargetPlatformWorkaround': macos,
};
}
......
......@@ -101,6 +101,11 @@ class Template {
if (relativeDestinationPath.contains('web') && !web) {
return null;
}
// Only build a macOS project if explicitly asked.
final bool macOS = context['macos'];
if (relativeDestinationPath.startsWith('macos.tmpl') && !macOS) {
return null;
}
final String projectName = context['projectName'];
final String androidIdentifier = context['androidIdentifier'];
final String pluginClass = context['pluginClass'];
......
{{#includeTargetPlatformWorkaround}}
import 'dart:io';
import 'package:flutter/foundation.dart';
{{/includeTargetPlatformWorkaround}}
import 'package:flutter/material.dart';
{{#withDriverTest}}
import 'package:flutter_driver/driver_extension.dart';
......@@ -8,12 +13,33 @@ import 'dart:async';
import 'package:flutter/services.dart';
import 'package:{{pluginProjectName}}/{{pluginProjectName}}.dart';
{{/withPluginHook}}
{{#includeTargetPlatformWorkaround}}
// Sets a platform override for desktop to avoid exceptions. See
// https://flutter.dev/desktop#target-platform-override for more info.
void _enablePlatformOverrideForDesktop() {
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
}
}
{{/includeTargetPlatformWorkaround}}
{{^withDriverTest}}
{{^includeTargetPlatformWorkaround}}
void main() => runApp(MyApp());
{{/includeTargetPlatformWorkaround}}
{{#includeTargetPlatformWorkaround}}
void main() {
_enablePlatformOverrideForDesktop();
runApp(MyApp());
}
{{/includeTargetPlatformWorkaround}}
{{/withDriverTest}}
{{#withDriverTest}}
void main() {
{{#includeTargetPlatformWorkaround}}
_enablePlatformOverrideForDesktop();
{{/includeTargetPlatformWorkaround}}
// Enable integration testing with the Flutter Driver extension.
// See https://flutter.dev/testing/ for more info.
enableFlutterDriverExtension();
......
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/xcuserdata/
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:/Users/stuartmorgan/src/embedder-opensource/flutter-desktop-embedding/example/macos/Runner.xcodeproj">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "{{projectName}}.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00380F9121DF178D00097171"
BuildableName = "RunnerUITests.xctest"
BlueprintName = "RunnerUITests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "{{projectName}}.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "{{projectName}}.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "{{projectName}}.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
import Cocoa
import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = {{projectName}}
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = {{macosIdentifier}}
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © {{year}} {{organization}}. All rights reserved.
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
import Cocoa
import FlutterMacOS
public class {{pluginClass}}: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "{{projectName}}", binaryMessenger: registrar.messenger)
let instance = {{pluginClass}}()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString)
default:
result(FlutterMethodNotImplemented)
}
}
}
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = '{{projectName}}'
s.version = '0.0.1'
s.summary = '{{description}}'
s.description = <<-DESC
{{description}}
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
s.platform = :osx
s.osx.deployment_target = '10.11'
end
......@@ -21,12 +21,33 @@ dev_dependencies:
# The following section is specific to Flutter.
flutter:
# This section identifies this Flutter project as a plugin project.
{{^useNewPluginSchema}}
# The androidPackage and pluginClass identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
androidPackage: {{androidIdentifier}}
pluginClass: {{pluginClass}}
{{/useNewPluginSchema}}
{{#useNewPluginSchema}}
# The 'pluginClass' and Android 'package' identifiers should not ordinarily
# be modified. They are used by the tooling to maintain consistency when
# adding or updating assets for this project.
#
# NOTE: This new plugin description format is not supported on Flutter's
# stable channel as of 1.9.1. A plugin published using this format will not
# work for most clients until the next major stable release.
# However, it is required in order to declare macOS support.
plugin:
platforms:
android:
package: {{androidIdentifier}}
pluginClass: {{pluginClass}}
ios:
pluginClass: {{pluginClass}}
macos:
pluginClass: {{pluginClass}}
{{/useNewPluginSchema}}
# To add assets to your plugin package, add an assets section, like this:
# assets:
......
......@@ -528,6 +528,58 @@ void main() {
expect(actualContents.contains('useAndroidX'), false);
}, timeout: allowForCreateFlutterProject);
testUsingContext('app supports macOS if requested', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--macos', projectDir.path]);
expect(projectDir.childDirectory('macos').childDirectory('Runner.xcworkspace').existsSync(), true);
}, timeout: allowForCreateFlutterProject);
testUsingContext('app does not include macOS by default', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', projectDir.path]);
expect(projectDir.childDirectory('macos').childDirectory('Runner.xcworkspace').existsSync(), false);
}, timeout: allowForCreateFlutterProject);
testUsingContext('plugin supports macOS if requested', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', '--macos', projectDir.path]);
expect(projectDir.childDirectory('macos').childFile('flutter_project.podspec').existsSync(), true);
}, timeout: allowForCreateFlutterProject);
testUsingContext('plugin does not include macOS by default', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);
final CreateCommand command = CreateCommand();
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>['create', '--no-pub', '--template=plugin', projectDir.path]);
expect(projectDir.childDirectory('macos').childFile('flutter_project.podspec').existsSync(), false);
}, timeout: allowForCreateFlutterProject);
testUsingContext('has correct content and formatting with module template', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment