diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart index 986b5950fc..c31708b15d 100644 --- a/packages/flutter_tools/lib/src/windows/build_windows.dart +++ b/packages/flutter_tools/lib/src/windows/build_windows.dart @@ -26,7 +26,7 @@ Future buildWindows(WindowsProject windowsProject, BuildInfo buildInfo, {S final String vcvarsScript = await findVcvars(); if (vcvarsScript == null) { - throwToolExit('Unable to build: could not find vcvars64.bat'); + throwToolExit('Unable to build: could not find suitable toolchain.'); } final String buildScript = fs.path.join( diff --git a/packages/flutter_tools/lib/src/windows/msbuild_utils.dart b/packages/flutter_tools/lib/src/windows/msbuild_utils.dart index f816d2cbd7..5a5d173521 100644 --- a/packages/flutter_tools/lib/src/windows/msbuild_utils.dart +++ b/packages/flutter_tools/lib/src/windows/msbuild_utils.dart @@ -8,45 +8,70 @@ 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 _visualStudioVersions = ['2017', '2019']; - -/// The supported flavors of Visual Studio. -const List _visualStudioFlavors = [ - 'Community', - 'Professional', - 'Enterprise', - 'Preview' -]; +import '../globals.dart'; /// Returns the path to an installed vcvars64.bat script if found, or null. Future 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; - } - } + final String vswherePath = fs.path.join( + platform.environment['PROGRAMFILES(X86)'], + 'Microsoft Visual Studio', + 'Installer', + 'vswhere.exe', + ); + // The "Desktop development with C++" workload. This is a coarse check, since + // it doesn't validate that the specific pieces are available, but should be + // a reasonable first-pass approximation. + // In the future, a set of more targetted checks will be used to provide + // clear validation feedback (e.g., VS is installed, but missing component X). + const String requiredComponent = 'Microsoft.VisualStudio.Workload.NativeDesktop'; + + const String visualStudioInstallMessage = + 'Ensure that you have Visual Studio 2017 or later installed, including ' + 'the "Desktop development with C++" workload.'; + + if (!fs.file(vswherePath).existsSync()) { + printError( + 'Unable to locate Visual Studio: vswhere.exe not found\n' + '$visualStudioInstallMessage', + emphasis: true, + ); + return null; } - // If it can't be found manually, check the path. final ProcessResult whereResult = await processManager.run([ - 'where.exe', - vcvarsScriptName, + vswherePath, + '-latest', + '-requires', requiredComponent, + '-property', 'installationPath', ]); - if (whereResult.exitCode == 0) { - return whereResult.stdout.trim(); + if (whereResult.exitCode != 0) { + printError( + 'Unable to locate Visual Studio:\n' + '${whereResult.stdout}\n' + '$visualStudioInstallMessage', + emphasis: true, + ); + return null; + } + final String visualStudioPath = whereResult.stdout.trim(); + if (visualStudioPath.isEmpty) { + printError( + 'No suitable Visual Studio found. $visualStudioInstallMessage\n', + emphasis: true, + ); + return null; + } + final String vcvarsPath = + fs.path.join(visualStudioPath, 'VC', 'Auxiliary', 'Build', 'vcvars64.bat'); + if (!fs.file(vcvarsPath).existsSync()) { + printError( + 'vcvars64.bat does not exist at $vcvarsPath.\n', + emphasis: true, + ); + return null; } - return null; + return vcvarsPath; } /// Writes a property sheet (.props) file to expose all of the key/value diff --git a/packages/flutter_tools/test/commands/build_windows_test.dart b/packages/flutter_tools/test/commands/build_windows_test.dart index 5733135e21..1fde6dc4bd 100644 --- a/packages/flutter_tools/test/commands/build_windows_test.dart +++ b/packages/flutter_tools/test/commands/build_windows_test.dart @@ -26,8 +26,8 @@ void main() { ..environment['PROGRAMFILES(X86)'] = r'C:\Program Files (x86)\'; final MockPlatform notWindowsPlatform = MockPlatform(); 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'; + const String visualStudioPath = r'C:\Program Files (x86)\Microsoft Visual Studio\2017\Community'; + const String vcvarsPath = visualStudioPath + r'\VC\Auxiliary\Build\vcvars64.bat'; when(mockProcess.exitCode).thenAnswer((Invocation invocation) async { return 0; @@ -41,6 +41,25 @@ void main() { when(windowsPlatform.isWindows).thenReturn(true); when(notWindowsPlatform.isWindows).thenReturn(false); + // Sets up the mock environment so that lookup of vcvars64.bat will succeed. + void enableVcvarsMocking() { + const String vswherePath = r'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe'; + fs.file(vswherePath).createSync(recursive: true); + fs.file(vcvarsPath).createSync(recursive: true); + + final MockProcessResult result = MockProcessResult(); + when(result.exitCode).thenReturn(0); + when(result.stdout).thenReturn(visualStudioPath); + when(mockProcessManager.run([ + vswherePath, + '-latest', + '-requires', 'Microsoft.VisualStudio.Workload.NativeDesktop', + '-property', 'installationPath', + ])).thenAnswer((Invocation invocation) async { + return result; + }); + } + testUsingContext('Windows build fails when there is no vcvars64.bat', () async { final BuildCommand command = BuildCommand(); applyMocksToCommand(command); @@ -56,7 +75,7 @@ void main() { testUsingContext('Windows build fails when there is no windows project', () async { final BuildCommand command = BuildCommand(); applyMocksToCommand(command); - fs.file(vcvarsPath).createSync(recursive: true); + enableVcvarsMocking(); expect(createTestCommandRunner(command).run( const ['build', 'windows'] ), throwsA(isInstanceOf())); @@ -69,7 +88,7 @@ void main() { final BuildCommand command = BuildCommand(); applyMocksToCommand(command); fs.file(projectPath).createSync(recursive: true); - fs.file(vcvarsPath).createSync(recursive: true); + enableVcvarsMocking(); fs.file('pubspec.yaml').createSync(); fs.file('.packages').createSync(); @@ -85,7 +104,7 @@ void main() { final BuildCommand command = BuildCommand(); applyMocksToCommand(command); fs.file(projectPath).createSync(recursive: true); - fs.file(vcvarsPath).createSync(recursive: true); + enableVcvarsMocking(); fs.file('pubspec.yaml').createSync(); fs.file('.packages').createSync(); @@ -102,7 +121,7 @@ void main() { const ['build', 'windows'] ); - // Spot-check important elemenst from the properties file. + // Spot-check important elements from the properties file. final File propsFile = fs.file(r'C:\windows\flutter\Generated.props'); expect(propsFile.existsSync(), true); final xml.XmlDocument props = xml.parse(propsFile.readAsStringSync()); @@ -118,6 +137,7 @@ void main() { class MockProcessManager extends Mock implements ProcessManager {} class MockProcess extends Mock implements Process {} +class MockProcessResult extends Mock implements ProcessResult {} class MockPlatform extends Mock implements Platform { @override Map environment = {