diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index f2eedde630e..d974984229a 100644 --- a/packages/flutter_tools/lib/src/base/user_messages.dart +++ b/packages/flutter_tools/lib/src/base/user_messages.dart @@ -194,18 +194,22 @@ class UserMessages { // Messages used in VisualStudioValidator String visualStudioVersion(String name, String version) => '$name version $version'; String visualStudioLocation(String location) => 'Visual Studio at $location'; + String windows10SdkVersion(String version) => 'Windows 10 SDK version $version'; String visualStudioMissingComponents(String workload, List components) => 'Visual Studio is missing necessary components. Please re-run the ' 'Visual Studio installer for the "$workload" workload, and include these components:\n' - ' ${components.join('\n ')}'; - String visualStudioMissing(String workload, List components) => + ' ${components.join('\n ')}\n' + ' Windows 10 SDK'; + String get windows10SdkNotFound => + 'Unable to locate a Windows 10 SDK. If building fails, install the Windows 10 SDK in Visual Studio.'; + String visualStudioMissing(String workload) => 'Visual Studio not installed; this is necessary for Windows development.\n' 'Download at https://visualstudio.microsoft.com/downloads/.\n' - 'Please install the "$workload" workload, including the following components:\n ${components.join('\n ')}'; - String visualStudioTooOld(String minimumVersion, String workload, List components) => + 'Please install the "$workload" workload, including all of its default components'; + String visualStudioTooOld(String minimumVersion, String workload) => 'Visual Studio $minimumVersion or later is required.\n' 'Download at https://visualstudio.microsoft.com/downloads/.\n' - 'Please install the "$workload" workload, including the following components:\n ${components.join('\n ')}'; + 'Please install the "$workload" workload, including all of its default components'; String get visualStudioIsPrerelease => 'The current Visual Studio installation is a pre-release version. It may not be ' 'supported by Flutter yet.'; String get visualStudioNotLaunchable => diff --git a/packages/flutter_tools/lib/src/windows/visual_studio.dart b/packages/flutter_tools/lib/src/windows/visual_studio.dart index 21a47aa3fb8..3cf58c85ba8 100644 --- a/packages/flutter_tools/lib/src/windows/visual_studio.dart +++ b/packages/flutter_tools/lib/src/windows/visual_studio.dart @@ -10,6 +10,7 @@ import '../base/io.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../base/process.dart'; +import '../base/version.dart'; import '../convert.dart'; /// Encapsulates information about the installed copy of Visual Studio, if any. @@ -99,6 +100,37 @@ class VisualStudio { /// The name of the recommended Visual Studio installer workload. String get workloadDescription => 'Desktop development with C++'; + /// Returns the highest installed Windows 10 SDK version, or null if none is + /// found. + /// + /// For instance: 10.0.18362.0 + String getWindows10SDKVersion() { + final String sdkLocation = _getWindows10SdkLocation(); + if (sdkLocation == null) { + return null; + } + final Directory sdkIncludeDirectory = _fileSystem.directory(sdkLocation).childDirectory('Include'); + if (!sdkIncludeDirectory.existsSync()) { + return null; + } + // The directories in this folder are named by the SDK version. + Version highestVersion; + for (final FileSystemEntity versionEntry in sdkIncludeDirectory.listSync()) { + if (versionEntry.basename.startsWith('10.')) { + // Version only handles 3 components; strip off the '10.' to leave three + // components, since they all start with that. + final Version version = Version.parse(versionEntry.basename.substring(3)); + if (highestVersion == null || version > highestVersion) { + highestVersion = version; + } + } + } + if (highestVersion == null) { + return null; + } + return '10.$highestVersion'; + } + /// The names of the components within the workload that must be installed. /// /// The descriptions of some components differ from version to version. When @@ -147,6 +179,11 @@ class VisualStudio { 'vswhere.exe', ); + /// Workload ID for use with vswhere requirements. + /// + /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids + static const String _requiredWorkload = 'Microsoft.VisualStudio.Workload.NativeDesktop'; + /// Components for use with vswhere requirements. /// /// Maps from component IDs to description in the installer UI. @@ -170,14 +207,11 @@ class VisualStudio { // wrong after each VC++ toolchain update, so just instruct people to install the // latest version. cppToolchainDescription += '\n - If there are multiple build tool versions available, install the latest'; + // Things which are required by the workload (e.g., MSBuild) don't need to + // be included here. return { - // The MSBuild tool and related command-line toolchain. - 'Microsoft.Component.MSBuild': 'MSBuild', // The C++ toolchain required by the template. 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64': cppToolchainDescription, - // The Windows SDK version used by the template. - 'Microsoft.VisualStudio.Component.Windows10SDK.17763': - 'Windows 10 SDK (10.0.17763.0)', }; } @@ -217,13 +251,27 @@ class VisualStudio { /// This key is under the 'catalog' entry. static const String _catalogDisplayVersionKey = 'productDisplayVersion'; - /// Returns the details dictionary for the newest version of Visual Studio - /// that includes all of [requiredComponents], if there is one. - Map _visualStudioDetails( - {Iterable requiredComponents, List additionalArguments}) { - final List requirementArguments = requiredComponents == null - ? [] - : ['-requires', ...requiredComponents]; + /// The registry path for Windows 10 SDK installation details. + static const String _windows10SdkRegistryPath = r'HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0'; + + /// The registry key in _windows10SdkRegistryPath for the folder where the + /// SDKs are installed. + static const String _windows10SdkRegistryKey = 'InstallationFolder'; + + /// Returns the details dictionary for the newest version of Visual Studio. + /// If [validateRequirements] is set, the search will be limited to versions + /// that have all of the required workloads and components. + Map _visualStudioDetails({ + bool validateRequirements = false, + List additionalArguments, + }) { + final List requirementArguments = validateRequirements + ? [ + '-requires', + _requiredWorkload, + ..._requiredComponents(_minimumSupportedVersion).keys + ] + : []; try { final List defaultArguments = [ '-format', 'json', @@ -290,11 +338,11 @@ class VisualStudio { _minimumSupportedVersion.toString(), ]; Map visualStudioDetails = _visualStudioDetails( - requiredComponents: _requiredComponents(_minimumSupportedVersion).keys, + validateRequirements: true, additionalArguments: minimumVersionArguments); // If a stable version is not found, try searching for a pre-release version. visualStudioDetails ??= _visualStudioDetails( - requiredComponents: _requiredComponents(_minimumSupportedVersion).keys, + validateRequirements: true, additionalArguments: [...minimumVersionArguments, _vswherePrereleaseArgument]); if (visualStudioDetails != null) { @@ -336,4 +384,56 @@ class VisualStudio { } return _anyVisualStudioDetails; } + + /// Returns the installation location of the Windows 10 SDKs, or null if the + /// registry doesn't contain that information. + String _getWindows10SdkLocation() { + try { + final RunResult result = _processUtils.runSync([ + 'reg', + 'query', + _windows10SdkRegistryPath, + '/v', + _windows10SdkRegistryKey, + ]); + if (result.exitCode == 0) { + final RegExp pattern = RegExp(r'InstallationFolder\s+REG_SZ\s+(.+)'); + final RegExpMatch match = pattern.firstMatch(result.stdout); + if (match != null) { + return match.group(1).trim(); + } + } + } on ArgumentError { + // Thrown if reg somehow doesn't exist; ignore and return null below. + } on ProcessException { + // Ignored, return null below. + } + return null; + } + + /// Returns the highest-numbered SDK version in [dir], which should be the + /// Windows 10 SDK installation directory. + /// + /// Returns null if no Windows 10 SDKs are found. + String findHighestVersionInSdkDirectory(Directory dir) { + // This contains subfolders that are named by the SDK version. + final Directory includeDir = dir.childDirectory('Includes'); + if (!includeDir.existsSync()) { + return null; + } + Version highestVersion; + for (final FileSystemEntity versionEntry in includeDir.listSync()) { + if (!versionEntry.basename.startsWith('10.')) { + continue; + } + // Version only handles 3 components; strip off the '10.' to leave three + // components, since they all start with that. + final Version version = Version.parse(versionEntry.basename.substring(3)); + if (highestVersion == null || version > highestVersion) { + highestVersion = version; + } + } + // Re-add the leading '10.' that was removed for comparison. + return highestVersion == null ? null : '10.$highestVersion'; + } } diff --git a/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart b/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart index d1604bbdfcb..a1426608a6c 100644 --- a/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart +++ b/packages/flutter_tools/lib/src/windows/visual_studio_validator.dart @@ -44,6 +44,11 @@ class VisualStudioValidator extends DoctorValidator { messages.add(ValidationMessage(_userMessages.visualStudioIsPrerelease)); } + final String windows10SdkVersion = _visualStudio.getWindows10SDKVersion(); + if (windows10SdkVersion != null) { + messages.add(ValidationMessage(_userMessages.windows10SdkVersion(windows10SdkVersion))); + } + // Messages for faulty installations. if (!_visualStudio.isAtLeastMinimumVersion) { status = ValidationType.partial; @@ -51,7 +56,6 @@ class VisualStudioValidator extends DoctorValidator { _userMessages.visualStudioTooOld( _visualStudio.minimumVersionDescription, _visualStudio.workloadDescription, - _visualStudio.necessaryComponentDescriptions(), ), )); } else if (_visualStudio.isRebootRequired) { @@ -71,6 +75,9 @@ class VisualStudioValidator extends DoctorValidator { _visualStudio.necessaryComponentDescriptions(), ), )); + } else if (windows10SdkVersion == null) { + status = ValidationType.partial; + messages.add(ValidationMessage.hint(_userMessages.windows10SdkNotFound)); } versionInfo = '${_visualStudio.displayName} ${_visualStudio.displayVersion}'; } else { @@ -78,7 +85,6 @@ class VisualStudioValidator extends DoctorValidator { messages.add(ValidationMessage.error( _userMessages.visualStudioMissing( _visualStudio.workloadDescription, - _visualStudio.necessaryComponentDescriptions(), ), )); } diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart index f7bfbd4e53a..e9079e9b082 100644 --- a/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart +++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_test.dart @@ -66,11 +66,11 @@ const Map _missingStatusResponse = { }, }; -// Arguments for a vswhere query to search for an installation with the required components. -const List _requiredComponents = [ - 'Microsoft.Component.MSBuild', +// Arguments for a vswhere query to search for an installation with the +// requirements. +const List _requirements = [ + 'Microsoft.VisualStudio.Workload.NativeDesktop', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', - 'Microsoft.VisualStudio.Component.Windows10SDK.17763', ]; // Sets up the mock environment so that searching for Visual Studio with @@ -116,7 +116,7 @@ void setMockCompatibleVisualStudioInstallation( setMockVswhereResponse( fileSystem, processManager, - _requiredComponents, + _requirements, ['-version', '16'], response, ); @@ -132,7 +132,7 @@ void setMockPrereleaseVisualStudioInstallation( setMockVswhereResponse( fileSystem, processManager, - _requiredComponents, + _requirements, ['-version', '16', '-prerelease'], response, ); @@ -170,6 +170,50 @@ void setMockEncodedAnyVisualStudioInstallation( ); } +// Sets up the mock environment for a Windows 10 SDK query. +// +// registryPresent controls whether or not the registry key is found. +// filesPresent controles where or not there are any SDK folders at the location +// returned by the registry query. +void setMockSdkRegResponse( + FileSystem fileSystem, + FakeProcessManager processManager, { + bool registryPresent = true, + bool filesPresent = true, +}) { + const String registryPath = r'HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0'; + const String registryKey = r'InstallationFolder'; + const String installationPath = r'C:\Program Files (x86)\Windows Kits\10\'; + final String stdout = registryPresent + ? ''' +$registryPath + $registryKey REG_SZ $installationPath +''' + : ''' + +ERROR: The system was unable to find the specified registry key or value. +'''; + + if (filesPresent) { + final Directory includeDirectory = fileSystem.directory(installationPath).childDirectory('Include'); + includeDirectory.childDirectory('10.0.17763.0').createSync(recursive: true); + includeDirectory.childDirectory('10.0.18362.0').createSync(recursive: true); + // Not an actual version; added to ensure that version comparison is number, not string-based. + includeDirectory.childDirectory('10.0.184.0').createSync(recursive: true); + } + + processManager.addCommand(FakeCommand( + command: const [ + 'reg', + 'query', + registryPath, + '/v', + registryKey, + ], + stdout: stdout, + )); +} + // Create a visual studio instance with a FakeProcessManager. VisualStudioFixture setUpVisualStudio() { final FakeProcessManager processManager = FakeProcessManager.list([]); @@ -283,7 +327,7 @@ void main() { fixture.processManager, ); - final String toolsString = visualStudio.necessaryComponentDescriptions()[1]; + final String toolsString = visualStudio.necessaryComponentDescriptions()[0]; expect(toolsString.contains('v142'), true); }); @@ -308,7 +352,7 @@ void main() { fixture.processManager, ); - final String toolsString = visualStudio.necessaryComponentDescriptions()[1]; + final String toolsString = visualStudio.necessaryComponentDescriptions()[0]; expect(toolsString.contains('v142'), true); }); @@ -717,6 +761,44 @@ void main() { expect(visualStudio.displayName, equals('Visual Studio Community 2017')); expect(visualStudio.displayVersion, equals('15.9.12')); }); + + testWithoutContext('SDK version returns the latest version when present', () { + final VisualStudioFixture fixture = setUpVisualStudio(); + final VisualStudio visualStudio = fixture.visualStudio; + + setMockSdkRegResponse( + fixture.fileSystem, + fixture.processManager, + ); + + expect(visualStudio.getWindows10SDKVersion(), '10.0.18362.0'); + }); + + testWithoutContext('SDK version returns null when the registry key is not present', () { + final VisualStudioFixture fixture = setUpVisualStudio(); + final VisualStudio visualStudio = fixture.visualStudio; + + setMockSdkRegResponse( + fixture.fileSystem, + fixture.processManager, + registryPresent: false, + ); + + expect(visualStudio.getWindows10SDKVersion(), null); + }); + + testWithoutContext('SDK version returns null when there are no SDK files present', () { + final VisualStudioFixture fixture = setUpVisualStudio(); + final VisualStudio visualStudio = fixture.visualStudio; + + setMockSdkRegResponse( + fixture.fileSystem, + fixture.processManager, + filesPresent: false, + ); + + expect(visualStudio.getWindows10SDKVersion(), null); + }); }); } diff --git a/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart index b118e0d9cef..7b9461bbae2 100644 --- a/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/windows/visual_studio_validator_test.dart @@ -35,6 +35,7 @@ void main() { when(mockVisualStudio.hasNecessaryComponents).thenReturn(true); when(mockVisualStudio.fullVersion).thenReturn('16.2'); when(mockVisualStudio.displayName).thenReturn('Visual Studio Community 2019'); + when(mockVisualStudio.getWindows10SDKVersion()).thenReturn('10.0.18362.0'); } // Assigns default values for a complete VS installation that is too old. @@ -48,6 +49,7 @@ void main() { when(mockVisualStudio.hasNecessaryComponents).thenReturn(true); when(mockVisualStudio.fullVersion).thenReturn('15.1'); when(mockVisualStudio.displayName).thenReturn('Visual Studio Community 2017'); + when(mockVisualStudio.getWindows10SDKVersion()).thenReturn('10.0.17763.0'); } // Assigns default values for a missing VS installation. @@ -59,6 +61,7 @@ void main() { when(mockVisualStudio.isLaunchable).thenReturn(false); when(mockVisualStudio.isRebootRequired).thenReturn(false); when(mockVisualStudio.hasNecessaryComponents).thenReturn(false); + when(mockVisualStudio.getWindows10SDKVersion()).thenReturn(null); } testWithoutContext('Emits a message when Visual Studio is a pre-release version', () async { @@ -132,7 +135,6 @@ void main() { userMessages.visualStudioTooOld( mockVisualStudio.minimumVersionDescription, mockVisualStudio.workloadDescription, - mockVisualStudio.necessaryComponentDescriptions(), ), ); @@ -152,6 +154,18 @@ void main() { expect(result.type, ValidationType.partial); }); + testWithoutContext('Emits partial status when Visual Studio is installed but the SDK cannot be found', () async { + final VisualStudioValidator validator = VisualStudioValidator( + userMessages: userMessages, + visualStudio: mockVisualStudio, + ); + _configureMockVisualStudioAsInstalled(); + when(mockVisualStudio.getWindows10SDKVersion()).thenReturn(null); + final ValidationResult result = await validator.validate(); + + expect(result.type, ValidationType.partial); + }); + testWithoutContext('Emits installed status when Visual Studio is installed with necessary components', () async { final VisualStudioValidator validator = VisualStudioValidator( userMessages: userMessages, @@ -178,7 +192,6 @@ void main() { final ValidationMessage expectedMessage = ValidationMessage.error( userMessages.visualStudioMissing( mockVisualStudio.workloadDescription, - mockVisualStudio.necessaryComponentDescriptions(), ), );