diff --git a/packages/flutter_tools/lib/src/windows/visual_studio.dart b/packages/flutter_tools/lib/src/windows/visual_studio.dart index 0aedcbb1a67..68c666b50b2 100644 --- a/packages/flutter_tools/lib/src/windows/visual_studio.dart +++ b/packages/flutter_tools/lib/src/windows/visual_studio.dart @@ -2,11 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - -import 'package:meta/meta.dart'; import 'package:process/process.dart'; +import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; @@ -18,10 +16,10 @@ import '../convert.dart'; /// Encapsulates information about the installed copy of Visual Studio, if any. class VisualStudio { VisualStudio({ - @required FileSystem fileSystem, - @required ProcessManager processManager, - @required Platform platform, - @required Logger logger, + required FileSystem fileSystem, + required ProcessManager processManager, + required Platform platform, + required Logger logger, }) : _platform = platform, _fileSystem = fileSystem, _processUtils = ProcessUtils(processManager: processManager, logger: logger); @@ -38,7 +36,7 @@ class VisualStudio { bool get isInstalled => _bestVisualStudioDetails.isNotEmpty; bool get isAtLeastMinimumVersion { - final int installedMajorVersion = _majorVersion; + final int? installedMajorVersion = _majorVersion; return installedMajorVersion != null && installedMajorVersion >= _minimumSupportedVersion; } @@ -54,7 +52,7 @@ class VisualStudio { /// The user-friendly version number of the Visual Studio install. /// /// For instance: "15.4.0". - String get displayVersion { + String? get displayVersion { if (_bestVisualStudioDetails[_catalogKey] == null) { return null; } @@ -80,7 +78,7 @@ class VisualStudio { if (_bestVisualStudioDetails.isEmpty) { return false; } - return _bestVisualStudioDetails[_isCompleteKey] as bool ?? true; + return _bestVisualStudioDetails[_isCompleteKey] as bool? ?? true; } /// True if Visual Studio is launchable. @@ -90,14 +88,14 @@ class VisualStudio { if (_bestVisualStudioDetails.isEmpty) { return false; } - return _bestVisualStudioDetails[_isLaunchableKey] as bool ?? true; + return _bestVisualStudioDetails[_isLaunchableKey] as bool? ?? true; } /// True if the Visual Studio installation is as pre-release version. - bool get isPrerelease => _bestVisualStudioDetails[_isPrereleaseKey] as bool ?? false; + bool get isPrerelease => _bestVisualStudioDetails[_isPrereleaseKey] as bool? ?? false; /// True if a reboot is required to complete the Visual Studio installation. - bool get isRebootRequired => _bestVisualStudioDetails[_isRebootRequiredKey] as bool ?? false; + bool get isRebootRequired => _bestVisualStudioDetails[_isRebootRequiredKey] as bool? ?? false; /// The name of the recommended Visual Studio installer workload. String get workloadDescription => 'Desktop development with C++'; @@ -106,8 +104,8 @@ class VisualStudio { /// found. /// /// For instance: 10.0.18362.0. - String getWindows10SDKVersion() { - final String sdkLocation = _getWindows10SdkLocation(); + String? getWindows10SDKVersion() { + final String? sdkLocation = _getWindows10SdkLocation(); if (sdkLocation == null) { return null; } @@ -116,13 +114,13 @@ class VisualStudio { return null; } // The directories in this folder are named by the SDK version. - Version highestVersion; + 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) { + final Version? version = Version.parse(versionEntry.basename.substring(3)); + if (highestVersion == null || (version != null && version > highestVersion)) { highestVersion = version; } } @@ -151,7 +149,7 @@ class VisualStudio { /// The path to CMake, or null if no Visual Studio installation has /// the components necessary to build. - String get cmakePath { + String? get cmakePath { final Map details = _usableVisualStudioDetails; if (details.isEmpty) { return null; @@ -170,7 +168,7 @@ class VisualStudio { } /// The major version of the Visual Studio install, as an integer. - int get _majorVersion => fullVersion != null ? int.tryParse(fullVersion.split('.')[0]) : null; + int? get _majorVersion => fullVersion != null ? int.tryParse(fullVersion.split('.')[0]) : null; /// The path to vswhere.exe. /// @@ -178,12 +176,18 @@ class VisualStudio { /// present then there isn't a new enough installation of VS. This path is /// not user-controllable, unlike the install location of Visual Studio /// itself. - String get _vswherePath => _fileSystem.path.join( - _platform.environment['PROGRAMFILES(X86)'], - 'Microsoft Visual Studio', - 'Installer', - 'vswhere.exe', - ); + String get _vswherePath { + const String programFilesEnv = 'PROGRAMFILES(X86)'; + if (!_platform.environment.containsKey(programFilesEnv)) { + throwToolExit('%$programFilesEnv% environment variable not found.'); + } + return _fileSystem.path.join( + _platform.environment[programFilesEnv]!, + 'Microsoft Visual Studio', + 'Installer', + 'vswhere.exe', + ); + } /// Workload ID for use with vswhere requirements. /// @@ -198,7 +202,7 @@ class VisualStudio { /// /// Maps from component IDs to description in the installer UI. /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids - Map _requiredComponents([int majorVersion]) { + Map _requiredComponents([int? majorVersion]) { // The description of the C++ toolchain required by the template. The // component name is significantly different in different versions. // When a new major version of VS is supported, its toolchain description @@ -273,15 +277,17 @@ class VisualStudio { /// 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({ + Map? _visualStudioDetails({ bool validateRequirements = false, - List additionalArguments, - String requiredWorkload + List? additionalArguments, + String? requiredWorkload }) { final List requirementArguments = validateRequirements ? [ - '-requires', - requiredWorkload, + if (requiredWorkload != null) ...[ + '-requires', + requiredWorkload, + ], ..._requiredComponents(_minimumSupportedVersion).keys ] : []; @@ -296,7 +302,7 @@ class VisualStudio { _vswherePath, ...defaultArguments, ...?additionalArguments, - ...?requirementArguments, + ...requirementArguments, ], encoding: utf8); if (whereResult.exitCode == 0) { final List> installations = @@ -342,16 +348,16 @@ class VisualStudio { /// /// If no installation is found, the cached VS details are set to an empty map /// to avoid repeating vswhere queries that have already not found an installation. - Map _cachedUsableVisualStudioDetails; + Map? _cachedUsableVisualStudioDetails; Map get _usableVisualStudioDetails { if (_cachedUsableVisualStudioDetails != null) { - return _cachedUsableVisualStudioDetails; + return _cachedUsableVisualStudioDetails!; } final List minimumVersionArguments = [ _vswhereMinVersionArgument, _minimumSupportedVersion.toString(), ]; - Map visualStudioDetails; + Map? visualStudioDetails; // Check in the order of stable VS, stable BT, pre-release VS, pre-release BT for (final bool checkForPrerelease in [false, true]) { for (final String requiredWorkload in _requiredWorkloads) { @@ -372,7 +378,7 @@ class VisualStudio { } } _cachedUsableVisualStudioDetails ??= {}; - return _cachedUsableVisualStudioDetails; + return _cachedUsableVisualStudioDetails!; } /// Returns the details dictionary of the latest version of Visual Studio, @@ -382,14 +388,14 @@ class VisualStudio { /// If no installation is found, the cached VS details are set to an empty map /// to avoid repeating vswhere queries that have already not found an /// installation. - Map _cachedAnyVisualStudioDetails; + Map? _cachedAnyVisualStudioDetails; Map get _anyVisualStudioDetails { // Search for all types of installations. _cachedAnyVisualStudioDetails ??= _visualStudioDetails( additionalArguments: [_vswherePrereleaseArgument, '-all']); // Add a sentinel empty value to avoid querying vswhere again. _cachedAnyVisualStudioDetails ??= {}; - return _cachedAnyVisualStudioDetails; + return _cachedAnyVisualStudioDetails!; } /// Returns the details dictionary of the best available version of Visual @@ -406,7 +412,7 @@ class VisualStudio { /// Returns the installation location of the Windows 10 SDKs, or null if the /// registry doesn't contain that information. - String _getWindows10SdkLocation() { + String? _getWindows10SdkLocation() { try { final RunResult result = _processUtils.runSync([ 'reg', @@ -417,9 +423,9 @@ class VisualStudio { ]); if (result.exitCode == 0) { final RegExp pattern = RegExp(r'InstallationFolder\s+REG_SZ\s+(.+)'); - final RegExpMatch match = pattern.firstMatch(result.stdout); + final RegExpMatch? match = pattern.firstMatch(result.stdout); if (match != null) { - return match.group(1).trim(); + return match.group(1)!.trim(); } } } on ArgumentError { @@ -434,21 +440,21 @@ class VisualStudio { /// Windows 10 SDK installation directory. /// /// Returns null if no Windows 10 SDKs are found. - String findHighestVersionInSdkDirectory(Directory dir) { + 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; + 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) { + final Version? version = Version.parse(versionEntry.basename.substring(3)); + if (highestVersion == null || (version != null && version > highestVersion)) { highestVersion = version; } } 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 e35428527e7..853bb84e4dd 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 @@ -342,6 +342,18 @@ void setNoViableToolchainInstallation( void main() { group('Visual Studio', () { + testWithoutContext('isInstalled throws when PROGRAMFILES(X86) env not set', () { + final VisualStudio visualStudio = VisualStudio( + logger: BufferLogger.test(), + fileSystem: MemoryFileSystem.test(style: FileSystemStyle.windows), + platform: FakePlatform(operatingSystem: 'windows'), + processManager: FakeProcessManager.any(), + ); + + expect(() => visualStudio.isInstalled, + throwsToolExit(message: '%PROGRAMFILES(X86)% environment variable not found')); + }); + testWithoutContext('isInstalled and cmakePath correct when vswhere is missing', () { final FileSystem fileSystem = MemoryFileSystem.test(style: FileSystemStyle.windows); const Exception exception = ProcessException('vswhere', []);