Teach flutter msbuild for Windows (#32335)

Eliminates the need for a build.bat in the Windows build workflow, adding
preliminary support for building using msbuild. The handling of
vcvars64.bat may be refined in the future, but this serves as a starting point.
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 2decfcf..8ac323e 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -592,13 +592,19 @@
 
   final FlutterProject project;
 
-  bool existsSync() => project.directory.childDirectory('windows').existsSync();
+  bool existsSync() => _editableDirectory.existsSync();
 
-  // Note: The build script file exists as a temporary shim.
-  File get buildScript => project.directory.childDirectory('windows').childFile('build.bat');
+  Directory get _editableDirectory => project.directory.childDirectory('windows');
+
+  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
+  /// the build.
+  File get generatedPropertySheetFile => _editableDirectory.childDirectory('flutter').childFile('Generated.props');
+
+  // The MSBuild project file.
+  File get vcprojFile => _editableDirectory.childFile('Runner.vcxproj');
 
   // Note: The name script file exists as a temporary shim.
-  File get nameScript => project.directory.childDirectory('windows').childFile('name_output.bat');
+  File get nameScript => _editableDirectory.childFile('name_output.bat');
 }
 
 /// The Linux sub project.
diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart
index d2aabfe..985f9b7 100644
--- a/packages/flutter_tools/lib/src/windows/build_windows.dart
+++ b/packages/flutter_tools/lib/src/windows/build_windows.dart
@@ -11,14 +11,26 @@
 import '../convert.dart';
 import '../globals.dart';
 import '../project.dart';
+import 'msbuild_utils.dart';
 
-/// Builds the Windows project through the project bat script.
+/// Builds the Windows project using msbuild.
 Future<void> buildWindows(WindowsProject windowsProject, BuildInfo buildInfo) async {
+  final Map<String, String> environment = <String, String>{
+    'FLUTTER_ROOT': Cache.flutterRoot,
+    'EXTRA_BUNDLE_FLAGS': buildInfo?.trackWidgetCreation == true ? '--track-widget-creation' : '',
+  };
+  writePropertySheet(windowsProject.generatedPropertySheetFile, environment);
+
+  final String vcvarsScript = await findVcvars();
+  if (vcvarsScript == null) {
+    throwToolExit('Unable to build: could not find vcvars64.bat');
+  }
+
+  final String configuration = buildInfo.isDebug ? 'Debug' : 'Release';
   final Process process = await processManager.start(<String>[
-    windowsProject.buildScript.path,
-    Cache.flutterRoot,
-    buildInfo.isDebug ? 'debug' : 'release',
-    buildInfo?.trackWidgetCreation == true ? 'track-widget-creation' : 'no-track-widget-creation',
+    vcvarsScript, '&&', 'msbuild',
+    windowsProject.vcprojFile.path,
+    '/p:Configuration=$configuration',
   ], runInShell: true);
   final Status status = logger.startProgress(
     'Building Windows application...',
diff --git a/packages/flutter_tools/lib/src/windows/msbuild_utils.dart b/packages/flutter_tools/lib/src/windows/msbuild_utils.dart
new file mode 100644
index 0000000..f816d2c
--- /dev/null
+++ b/packages/flutter_tools/lib/src/windows/msbuild_utils.dart
@@ -0,0 +1,103 @@
+// Copyright 2019 The Chromium 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 'package:xml/xml.dart' as xml;
+
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/platform.dart';
+import '../base/process_manager.dart';
+
+/// The supported versions of Visual Studio.
+const List<String> _visualStudioVersions = <String>['2017', '2019'];
+
+/// The supported flavors of Visual Studio.
+const List<String> _visualStudioFlavors = <String>[
+  'Community',
+  'Professional',
+  'Enterprise',
+  'Preview'
+];
+
+/// Returns the path to an installed vcvars64.bat script if found, or null.
+Future<String> findVcvars() async {
+  final String programDir = platform.environment['PROGRAMFILES(X86)'];
+  final String pathPrefix = fs.path.join(programDir, 'Microsoft Visual Studio');
+  const String vcvarsScriptName = 'vcvars64.bat';
+  final String pathSuffix =
+      fs.path.join('VC', 'Auxiliary', 'Build', vcvarsScriptName);
+  for (final String version in _visualStudioVersions) {
+    for (final String flavor in _visualStudioFlavors) {
+      final String testPath =
+          fs.path.join(pathPrefix, version, flavor, pathSuffix);
+      if (fs.file(testPath).existsSync()) {
+        return testPath;
+      }
+    }
+  }
+
+  // If it can't be found manually, check the path.
+  final ProcessResult whereResult = await processManager.run(<String>[
+    'where.exe',
+    vcvarsScriptName,
+  ]);
+  if (whereResult.exitCode == 0) {
+    return whereResult.stdout.trim();
+  }
+
+  return null;
+}
+
+/// Writes a property sheet (.props) file to expose all of the key/value
+/// pairs in [variables] as enivornment variables.
+void writePropertySheet(File propertySheetFile, Map<String, String> variables) {
+  final xml.XmlBuilder builder = xml.XmlBuilder();
+  builder.processing('xml', 'version="1.0" encoding="utf-8"');
+  builder.element('Project', nest: () {
+    builder.attribute('ToolsVersion', '4.0');
+    builder.attribute(
+        'xmlns', 'http://schemas.microsoft.com/developer/msbuild/2003');
+    builder.element('ImportGroup', nest: () {
+      builder.attribute('Label', 'PropertySheets');
+    });
+    _addUserMacros(builder, variables);
+    builder.element('PropertyGroup');
+    builder.element('ItemDefinitionGroup');
+    _addItemGroup(builder, variables);
+  });
+
+  propertySheetFile.createSync(recursive: true);
+  propertySheetFile.writeAsStringSync(
+      builder.build().toXmlString(pretty: true, indent: '  '));
+}
+
+/// Adds the UserMacros PropertyGroup that defines [variables] to [builder].
+void _addUserMacros(xml.XmlBuilder builder, Map<String, String> variables) {
+  builder.element('PropertyGroup', nest: () {
+    builder.attribute('Label', 'UserMacros');
+    for (final MapEntry<String, String> variable in variables.entries) {
+      builder.element(variable.key, nest: () {
+        builder.text(variable.value);
+      });
+    }
+  });
+}
+
+/// Adds the ItemGroup to expose the given [variables] as environment variables
+/// to [builder].
+void _addItemGroup(xml.XmlBuilder builder, Map<String, String> variables) {
+  builder.element('ItemGroup', nest: () {
+    for (final String name in variables.keys) {
+      builder.element('BuildMacro', nest: () {
+        builder.attribute('Include', name);
+        builder.element('Value', nest: () {
+          builder.text('\$($name)');
+        });
+        builder.element('EnvironmentVariable', nest: () {
+          builder.text('true');
+        });
+      });
+    }
+  });
+}