Add Xcode build script for macOS target (#31329)

diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart
index cc446c2..b2080eb 100644
--- a/packages/flutter_tools/lib/src/artifacts.dart
+++ b/packages/flutter_tools/lib/src/artifacts.dart
@@ -18,6 +18,7 @@
   flutterTester,
   snapshotDart,
   flutterFramework,
+  flutterMacOSFramework,
   vmSnapshotData,
   isolateSnapshotData,
   platformKernelDill,
@@ -44,6 +45,8 @@
       return 'snapshot.dart';
     case Artifact.flutterFramework:
       return 'Flutter.framework';
+    case Artifact.flutterMacOSFramework:
+      return 'FlutterMacOS.framework';
     case Artifact.vmSnapshotData:
       // Flutter 'debug' and 'dynamic profile' modes for all target platforms use Dart
       // RELEASE VM snapshot that comes from host debug build and has the metadata
@@ -211,6 +214,10 @@
         return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
       case Artifact.kernelWorkerSnapshot:
         return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
+      case Artifact.flutterMacOSFramework:
+        final String engineArtifactsPath = cache.getArtifactDirectory('engine').path;
+        final String platformDirName = getNameForTargetPlatform(platform);
+        return fs.path.join(engineArtifactsPath, platformDirName, _artifactToFileName(artifact, platform, mode));
       default:
         assert(false, 'Artifact $artifact not available for platform $platform.');
         return null;
@@ -279,6 +286,8 @@
         return fs.path.join(_getFlutterPatchedSdkPath(mode), 'lib', _artifactToFileName(artifact));
       case Artifact.flutterFramework:
         return fs.path.join(engineOutPath, _artifactToFileName(artifact));
+      case Artifact.flutterMacOSFramework:
+        return fs.path.join(engineOutPath, _artifactToFileName(artifact));
       case Artifact.flutterPatchedSdkPath:
         // When using local engine always use [BuildMode.debug] regardless of
         // what was specified in [mode] argument because local engine will
diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart
index a95da5b..d0ad3d6 100644
--- a/packages/flutter_tools/lib/src/build_info.dart
+++ b/packages/flutter_tools/lib/src/build_info.dart
@@ -382,6 +382,11 @@
   return fs.path.join(getBuildDirectory(), 'ios');
 }
 
+/// Returns the macOS build output directory.
+String getMacOSBuildDirectory() {
+  return fs.path.join(getBuildDirectory(), 'macos');
+}
+
 /// Returns the web build output directory.
 String getWebBuildDirectory() {
   return fs.path.join(getBuildDirectory(), 'web');
diff --git a/packages/flutter_tools/lib/src/commands/build_bundle.dart b/packages/flutter_tools/lib/src/commands/build_bundle.dart
index 091f9fd..3d909ed 100644
--- a/packages/flutter_tools/lib/src/commands/build_bundle.dart
+++ b/packages/flutter_tools/lib/src/commands/build_bundle.dart
@@ -8,6 +8,7 @@
 import '../build_info.dart';
 import '../bundle.dart';
 import '../runner/flutter_command.dart' show FlutterOptions, FlutterCommandResult;
+import '../version.dart';
 import 'build.dart';
 
 class BuildBundleCommand extends BuildSubCommand {
@@ -27,7 +28,16 @@
       ..addOption('depfile', defaultsTo: defaultDepfilePath)
       ..addOption('target-platform',
         defaultsTo: 'android-arm',
-        allowed: <String>['android-arm', 'android-arm64', 'android-x86', 'android-x64', 'ios'],
+        allowed: const <String>[
+          'android-arm',
+          'android-arm64',
+          'android-x86',
+          'android-x64',
+          'ios',
+          'darwin-x64',
+          'linux-x64',
+          'windows-x64',
+        ],
       )
       ..addFlag('track-widget-creation',
         hide: !verboseHelp,
@@ -64,8 +74,21 @@
   Future<FlutterCommandResult> runCommand() async {
     final String targetPlatform = argResults['target-platform'];
     final TargetPlatform platform = getTargetPlatformForName(targetPlatform);
-    if (platform == null)
+    if (platform == null) {
       throwToolExit('Unknown platform: $targetPlatform');
+    }
+    // Check for target platforms that are only allowed on unstable Flutter.
+    switch (platform) {
+      case TargetPlatform.darwin_x64:
+      case TargetPlatform.windows_x64:
+      case TargetPlatform.linux_x64:
+        if (FlutterVersion.instance.isStable) {
+          throwToolExit('$targetPlatform is not supported on stable Flutter.');
+        }
+        break;
+      default:
+        break;
+    }
 
     final BuildMode buildMode = getBuildMode();
 
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index c929186..e6e3ac8 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -27,14 +27,23 @@
       Artifact.flutterFramework, platform: TargetPlatform.ios, mode: mode)));
 }
 
+String flutterMacOSFrameworkDir(BuildMode mode) {
+  return fs.path.normalize(fs.path.dirname(artifacts.getArtifactPath(
+      Artifact.flutterMacOSFramework, platform: TargetPlatform.darwin_x64, mode: mode)));
+}
+
 /// Writes or rewrites Xcode property files with the specified information.
 ///
+/// useMacOSConfig: Optional parameter that controls whether we use the macOS
+/// project file instead. Defaults to false.
+///
 /// targetOverride: Optional parameter, if null or unspecified the default value
 /// from xcode_backend.sh is used 'lib/main.dart'.
 Future<void> updateGeneratedXcodeProperties({
   @required FlutterProject project,
   @required BuildInfo buildInfo,
   String targetOverride,
+  bool useMacOSConfig = false,
 }) async {
   final StringBuffer localsBuffer = StringBuffer();
 
@@ -53,14 +62,20 @@
   // The build outputs directory, relative to FLUTTER_APPLICATION_PATH.
   localsBuffer.writeln('FLUTTER_BUILD_DIR=${getBuildDirectory()}');
 
-  localsBuffer.writeln('SYMROOT=\${SOURCE_ROOT}/../${getIosBuildDirectory()}');
+  final String buildDirectory = useMacOSConfig
+      ? getMacOSBuildDirectory()
+      : getIosBuildDirectory();
+  localsBuffer.writeln('SYMROOT=\${SOURCE_ROOT}/../$buildDirectory');
 
   if (!project.isModule) {
     // For module projects we do not want to write the FLUTTER_FRAMEWORK_DIR
     // explicitly. Rather we rely on the xcode backend script and the Podfile
     // logic to derive it from FLUTTER_ROOT and FLUTTER_BUILD_MODE.
     // However, this is necessary for regular projects using Cocoapods.
-    localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=${flutterFrameworkDir(buildInfo.mode)}');
+    final String frameworkDir = useMacOSConfig
+        ? flutterMacOSFrameworkDir(buildInfo.mode)
+        : flutterFrameworkDir(buildInfo.mode);
+    localsBuffer.writeln('FLUTTER_FRAMEWORK_DIR=$frameworkDir');
   }
 
   final String buildName = validatedBuildNameForPlatform(TargetPlatform.ios, buildInfo?.buildName ?? project.manifest.buildName);
@@ -85,15 +100,21 @@
     // NOTE: this assumes that local engine binary paths are consistent with
     // the conventions uses in the engine: 32-bit iOS engines are built to
     // paths ending in _arm, 64-bit builds are not.
-    final String arch = engineOutPath.endsWith('_arm') ? 'armv7' : 'arm64';
-    localsBuffer.writeln('ARCHS=$arch');
+    //
+    // Skip this step for macOS builds.
+    if (!useMacOSConfig) {
+      final String arch = engineOutPath.endsWith('_arm') ? 'armv7' : 'arm64';
+      localsBuffer.writeln('ARCHS=$arch');
+    }
   }
 
   if (buildInfo.trackWidgetCreation) {
     localsBuffer.writeln('TRACK_WIDGET_CREATION=true');
   }
 
-  final File generatedXcodePropertiesFile = project.ios.generatedXcodePropertiesFile;
+  final File generatedXcodePropertiesFile = useMacOSConfig
+      ? project.macos.generatedXcodePropertiesFile
+      : project.ios.generatedXcodePropertiesFile;
   generatedXcodePropertiesFile.createSync(recursive: true);
   generatedXcodePropertiesFile.writeAsStringSync(localsBuffer.toString());
 }
diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart
index 4c149be..bba4df0 100644
--- a/packages/flutter_tools/lib/src/macos/build_macos.dart
+++ b/packages/flutter_tools/lib/src/macos/build_macos.dart
@@ -3,22 +3,46 @@
 // found in the LICENSE file.
 
 import '../base/common.dart';
+import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
 import '../base/process_manager.dart';
 import '../build_info.dart';
-import '../cache.dart';
 import '../convert.dart';
 import '../globals.dart';
+import '../ios/xcodeproj.dart';
 import '../project.dart';
 
-/// Builds the macOS project through the project shell script.
+/// Builds the macOS project through xcode build.
+// TODO(jonahwilliams): support target option.
+// TODO(jonahwilliams): refactor to share code with the existing iOS code.
 Future<void> buildMacOS(FlutterProject flutterProject, BuildInfo buildInfo) async {
+  // Write configuration to an xconfig file in a standard location.
+  await updateGeneratedXcodeProperties(
+    project: flutterProject,
+    buildInfo: buildInfo,
+    useMacOSConfig: true,
+  );
+  // Set debug or release mode.
+  String config = 'Debug';
+  if (buildInfo.isRelease) {
+    config = 'Release';
+  }
+  final Directory flutterBuildDir = fs.directory(getMacOSBuildDirectory());
+  if (!flutterBuildDir.existsSync()) {
+    flutterBuildDir.createSync(recursive: true);
+  }
+  // Run build script provided by application.
   final Process process = await processManager.start(<String>[
-    flutterProject.macos.buildScript.path,
-    Cache.flutterRoot,
-    buildInfo?.isDebug == true ? 'debug' : 'release',
-    buildInfo?.trackWidgetCreation == true ? 'track-widget-creation' : 'no-track-widget-creation',
+    '/usr/bin/env',
+    'xcrun',
+    'xcodebuild',
+    '-project', flutterProject.macos.xcodeProjectFile.path,
+    '-configuration', '$config',
+    '-scheme', 'Runner',
+    '-derivedDataPath', flutterBuildDir.absolute.path,
+    'OBJROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',
+    'SYMROOT=${fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}',
   ], runInShell: true);
   final Status status = logger.startProgress(
     'Building macOS application...',
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 2cd22e8..d387527 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -536,8 +536,14 @@
 
   bool existsSync() => project.directory.childDirectory('macos').existsSync();
 
-  // Note: The build script file exists as a temporary shim.
-  File get buildScript => project.directory.childDirectory('macos').childFile('build.sh');
+  Directory get _editableDirectory => project.directory.childDirectory('macos');
+
+  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
+  /// the Xcode build.
+  File get generatedXcodePropertiesFile => _editableDirectory.childDirectory('Flutter').childFile('Generated.xcconfig');
+
+  /// The Xcode project file.
+  Directory get xcodeProjectFile => _editableDirectory.childDirectory('Runner.xcodeproj');
 
   // Note: The name script file exists as a temporary shim.
   File get nameScript => project.directory.childDirectory('macos').childFile('name_output.sh');