Wrap Windows build invocation in a batch script (#33443)

Invoking msbuild with runInShell makes handling path escaping more
error-prone, and substantially increases the chances of running into
maximum path limits. This replaces the direct call with a .bat wrapper
that calls vsvars64.bat then msbuild, and uses relative paths within the
script to keep command lengths short.

Fixes https://github.com/flutter/flutter/issues/32792
diff --git a/packages/flutter_tools/bin/vs_build.bat b/packages/flutter_tools/bin/vs_build.bat
new file mode 100644
index 0000000..474ccb9
--- /dev/null
+++ b/packages/flutter_tools/bin/vs_build.bat
@@ -0,0 +1,16 @@
+:: Copyright 2018 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.
+
+:: Calls vcvars64.bat to configure a command-line build environment, then builds
+:: a project with msbuild.
+@echo off
+
+set VCVARS=%~1
+set PROJECT=%~2
+set CONFIG=%~3
+
+call "%VCVARS%"
+if %errorlevel% neq 0 exit /b %errorlevel%
+
+msbuild "%PROJECT%" /p:Configuration=%CONFIG%
diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart
index 776d864..986b595 100644
--- a/packages/flutter_tools/lib/src/windows/build_windows.dart
+++ b/packages/flutter_tools/lib/src/windows/build_windows.dart
@@ -3,6 +3,7 @@
 // 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';
@@ -28,12 +29,25 @@
     throwToolExit('Unable to build: could not find vcvars64.bat');
   }
 
+  final String buildScript = fs.path.join(
+    Cache.flutterRoot,
+    'packages',
+    'flutter_tools',
+    'bin',
+    'vs_build.bat',
+  );
+
   final String configuration = buildInfo.isDebug ? 'Debug' : 'Release';
+  final String projectPath = windowsProject.vcprojFile.path;
+  // Run the script with a relative path to the project using the enclosing
+  // directory as the workingDirectory, to avoid hitting the limit on command
+  // lengths in batch scripts if the absolute path to the project is long.
   final Process process = await processManager.start(<String>[
-    vcvarsScript, '&&', 'msbuild',
-    windowsProject.vcprojFile.path,
-    '/p:Configuration=$configuration',
-  ], runInShell: true);
+    buildScript,
+    vcvarsScript,
+    fs.path.basename(projectPath),
+    configuration,
+  ], workingDirectory: fs.path.dirname(projectPath));
   final Status status = logger.startProgress(
     'Building Windows application...',
     timeout: null,
diff --git a/packages/flutter_tools/test/commands/build_windows_test.dart b/packages/flutter_tools/test/commands/build_windows_test.dart
index 9371b37..5733135 100644
--- a/packages/flutter_tools/test/commands/build_windows_test.dart
+++ b/packages/flutter_tools/test/commands/build_windows_test.dart
@@ -25,7 +25,7 @@
   final MockPlatform windowsPlatform = MockPlatform()
       ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\';
   final MockPlatform notWindowsPlatform = MockPlatform();
-  const String projectPath = r'windows\Runner.vcxproj';
+  const String projectPath = r'C:\windows\Runner.vcxproj';
   // A vcvars64.bat location that will be found by the lookup method.
   const String vcvarsPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat';
 
@@ -90,12 +90,11 @@
     fs.file('.packages').createSync();
 
     when(mockProcessManager.start(<String>[
+      r'C:\packages\flutter_tools\bin\vs_build.bat',
       vcvarsPath,
-      '&&',
-      'msbuild',
-      'C:\\$projectPath',
-      '/p:Configuration=Release',
-    ], runInShell: true)).thenAnswer((Invocation invocation) async {
+      fs.path.basename(projectPath),
+      'Release',
+    ], workingDirectory: fs.path.dirname(projectPath))).thenAnswer((Invocation invocation) async {
       return mockProcess;
     });