Skip to content

Commit

Permalink
Refactor vswhere.exe integration (#104133)
Browse files Browse the repository at this point in the history
`VisualStudio` calls `vswhere.exe` to find Visual Studio installations and determine if they satisfy Flutter's requirements. Previously, `VisualStudio` stored the JSON output from `vswhere.exe` as `Map`s, resulting in duplicated logic to read the JSON output (once to validate values, second to expose values). Also, `VisualStudio` stored two copies of the JSON output (the latest valid installation as well as the latest VS installation).

This change simplifies `VisualStudio` by introducing a new `VswhereDetails`. This type contains the logic to read `vswhere.exe`'s JSON output, and, understand whether an installation is usable by Flutter. In the future, this `VswhereDetails` type will be used to make Flutter doctor resilient to bad UTF-8 output from `vswhere.exe`.

Part of #102451.
  • Loading branch information
loic-sharma authored May 20, 2022
1 parent 874b6c0 commit 52e19ef
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 118 deletions.
240 changes: 122 additions & 118 deletions packages/flutter_tools/lib/src/windows/visual_studio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:meta/meta.dart';
import 'package:process/process.dart';

import '../base/common.dart';
Expand Down Expand Up @@ -33,7 +34,7 @@ class VisualStudio {
/// Versions older than 2017 Update 2 won't be detected, so error messages to
/// users should take into account that [false] may mean that the user may
/// have an old version rather than no installation at all.
bool get isInstalled => _bestVisualStudioDetails.isNotEmpty;
bool get isInstalled => _bestVisualStudioDetails != null;

bool get isAtLeastMinimumVersion {
final int? installedMajorVersion = _majorVersion;
Expand All @@ -42,30 +43,25 @@ class VisualStudio {

/// True if there is a version of Visual Studio with all the components
/// necessary to build the project.
bool get hasNecessaryComponents => _usableVisualStudioDetails.isNotEmpty;
bool get hasNecessaryComponents => _bestVisualStudioDetails?.isUsable ?? false;

/// The name of the Visual Studio install.
///
/// For instance: "Visual Studio Community 2019".
String? get displayName => _bestVisualStudioDetails[_displayNameKey] as String?;
String? get displayName => _bestVisualStudioDetails?.displayName;

/// The user-friendly version number of the Visual Studio install.
///
/// For instance: "15.4.0".
String? get displayVersion {
if (_bestVisualStudioDetails[_catalogKey] == null) {
return null;
}
return (_bestVisualStudioDetails[_catalogKey] as Map<String, dynamic>)[_catalogDisplayVersionKey] as String?;
}
String? get displayVersion => _bestVisualStudioDetails?.catalogDisplayVersion;

/// The directory where Visual Studio is installed.
String? get installLocation => _bestVisualStudioDetails[_installationPathKey] as String?;
String? get installLocation => _bestVisualStudioDetails?.installationPath;

/// The full version of the Visual Studio install.
///
/// For instance: "15.4.27004.2002".
String? get fullVersion => _bestVisualStudioDetails[_fullVersionKey] as String?;
String? get fullVersion => _bestVisualStudioDetails?.fullVersion;

// Properties that determine the status of the installation. There might be
// Visual Studio versions that don't include them, so default to a "valid" value to
Expand All @@ -75,27 +71,27 @@ class VisualStudio {
///
/// False if installation is not found.
bool get isComplete {
if (_bestVisualStudioDetails.isEmpty) {
if (_bestVisualStudioDetails == null) {
return false;
}
return _bestVisualStudioDetails[_isCompleteKey] as bool? ?? true;
return _bestVisualStudioDetails!.isComplete ?? true;
}

/// True if Visual Studio is launchable.
///
/// False if installation is not found.
bool get isLaunchable {
if (_bestVisualStudioDetails.isEmpty) {
if (_bestVisualStudioDetails == null) {
return false;
}
return _bestVisualStudioDetails[_isLaunchableKey] as bool? ?? true;
return _bestVisualStudioDetails!.isLaunchable ?? true;
}

/// True if the Visual Studio installation is as pre-release version.
bool get isPrerelease => _bestVisualStudioDetails[_isPrereleaseKey] as bool? ?? false;
/// True if the Visual Studio installation is a pre-release version.
bool get isPrerelease => _bestVisualStudioDetails?.isPrerelease ?? 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?.isRebootRequired ?? false;

/// The name of the recommended Visual Studio installer workload.
String get workloadDescription => 'Desktop development with C++';
Expand Down Expand Up @@ -150,12 +146,13 @@ class VisualStudio {
/// The path to CMake, or null if no Visual Studio installation has
/// the components necessary to build.
String? get cmakePath {
final Map<String, dynamic> details = _usableVisualStudioDetails;
if (details.isEmpty || _usableVisualStudioDetails[_installationPathKey] == null) {
final VswhereDetails? details = _bestVisualStudioDetails;
if (details == null || !details.isUsable || details.installationPath == null) {
return null;
}

return _fileSystem.path.joinAll(<String>[
_usableVisualStudioDetails[_installationPathKey] as String,
details.installationPath!,
'Common7',
'IDE',
'CommonExtensions',
Expand Down Expand Up @@ -253,44 +250,18 @@ class VisualStudio {
/// vswhere argument to allow prerelease versions.
static const String _vswherePrereleaseArgument = '-prerelease';

// Keys in a VS details dictionary returned from vswhere.

/// The root directory of the Visual Studio installation.
static const String _installationPathKey = 'installationPath';

/// The user-friendly name of the installation.
static const String _displayNameKey = 'displayName';

/// The complete version.
static const String _fullVersionKey = 'installationVersion';

/// Keys for the status of the installation.
static const String _isCompleteKey = 'isComplete';
static const String _isLaunchableKey = 'isLaunchable';
static const String _isRebootRequiredKey = 'isRebootRequired';

/// The 'catalog' entry containing more details.
static const String _catalogKey = 'catalog';

/// The key for a pre-release version.
static const String _isPrereleaseKey = 'isPrerelease';

/// The user-friendly version.
///
/// This key is under the 'catalog' entry.
static const String _catalogDisplayVersionKey = 'productDisplayVersion';

/// 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.
/// Returns the details of 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<String, dynamic>? _visualStudioDetails({
VswhereDetails? _visualStudioDetails({
bool validateRequirements = false,
List<String>? additionalArguments,
String? requiredWorkload
Expand Down Expand Up @@ -321,7 +292,7 @@ class VisualStudio {
final List<Map<String, dynamic>> installations =
(json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
if (installations.isNotEmpty) {
return installations[0];
return VswhereDetails.fromJson(validateRequirements, installations[0]);
}
}
} on ArgumentError {
Expand All @@ -334,90 +305,39 @@ class VisualStudio {
return null;
}

/// Checks if the given installation has issues that the user must resolve.
///
/// Returns false if the required information is missing since older versions
/// of Visual Studio might not include them.
bool installationHasIssues(Map<String, dynamic>installationDetails) {
assert(installationDetails != null);
if (installationDetails[_isCompleteKey] != null && !(installationDetails[_isCompleteKey] as bool)) {
return true;
}

if (installationDetails[_isLaunchableKey] != null && !(installationDetails[_isLaunchableKey] as bool)) {
return true;
}

if (installationDetails[_isRebootRequiredKey] != null && installationDetails[_isRebootRequiredKey] as bool) {
return true;
}

return false;
}

/// Returns the details dictionary for the latest version of Visual Studio
/// that has all required components and is a supported version, or {} if
/// there is no such installation.
/// Returns the details of the best available version of Visual Studio.
///
/// 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.
late final Map<String, dynamic> _usableVisualStudioDetails = (){
/// If there's a version that has all the required components, that
/// will be returned, otherwise returns the latest installed version regardless
/// of components and version, or null if no such installation is found.
late final VswhereDetails? _bestVisualStudioDetails = () {
// First, attempt to find the latest version of Visual Studio that satifies
// both the minimum supported version and the required workloads.
// Check in the order of stable VS, stable BT, pre-release VS, pre-release BT.
final List<String> minimumVersionArguments = <String>[
_vswhereMinVersionArgument,
_minimumSupportedVersion.toString(),
];
Map<String, dynamic>? visualStudioDetails;
// Check in the order of stable VS, stable BT, pre-release VS, pre-release BT
for (final bool checkForPrerelease in <bool>[false, true]) {
for (final String requiredWorkload in _requiredWorkloads) {
visualStudioDetails ??= _visualStudioDetails(
final VswhereDetails? result = _visualStudioDetails(
validateRequirements: true,
additionalArguments: checkForPrerelease
? <String>[...minimumVersionArguments, _vswherePrereleaseArgument]
: minimumVersionArguments,
requiredWorkload: requiredWorkload);
}
}

Map<String, dynamic>? usableVisualStudioDetails;
if (visualStudioDetails != null) {
if (installationHasIssues(visualStudioDetails)) {
_cachedAnyVisualStudioDetails = visualStudioDetails;
} else {
usableVisualStudioDetails = visualStudioDetails;
if (result != null) {
return result;
}
}
}
return usableVisualStudioDetails ?? <String, dynamic>{};
}();

/// Returns the details dictionary of the latest version of Visual Studio,
/// regardless of components and version, or {} if no such installation is
/// found.
///
/// 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<String, dynamic>? _cachedAnyVisualStudioDetails;
Map<String, dynamic> get _anyVisualStudioDetails {
// Search for all types of installations.
_cachedAnyVisualStudioDetails ??= _visualStudioDetails(
// An installation that satifies requirements could not be found.
// Fallback to the latest Visual Studio installation.
return _visualStudioDetails(
additionalArguments: <String>[_vswherePrereleaseArgument, '-all']);
// Add a sentinel empty value to avoid querying vswhere again.
_cachedAnyVisualStudioDetails ??= <String, dynamic>{};
return _cachedAnyVisualStudioDetails!;
}

/// Returns the details dictionary of the best available version of Visual
/// Studio.
///
/// If there's a version that has all the required components, that
/// will be returned, otherwise returns the latest installed version (if any).
Map<String, dynamic> get _bestVisualStudioDetails {
if (_usableVisualStudioDetails.isNotEmpty) {
return _usableVisualStudioDetails;
}
return _anyVisualStudioDetails;
}
}();

/// Returns the installation location of the Windows 10 SDKs, or null if the
/// registry doesn't contain that information.
Expand Down Expand Up @@ -471,3 +391,87 @@ class VisualStudio {
return highestVersion == null ? null : '10.$highestVersion';
}
}

/// The details of a Visual Studio installation according to vswhere.
@visibleForTesting
class VswhereDetails {
const VswhereDetails({
required this.meetsRequirements,
required this.installationPath,
required this.displayName,
required this.fullVersion,
required this.isComplete,
required this.isLaunchable,
required this.isRebootRequired,
required this.isPrerelease,
required this.catalogDisplayVersion,
});

/// Create a `VswhereDetails` from the JSON output of vswhere.exe.
factory VswhereDetails.fromJson(
bool meetsRequirements,
Map<String, dynamic> details
) {
final Map<String, dynamic>? catalog = details['catalog'] as Map<String, dynamic>?;

return VswhereDetails(
meetsRequirements: meetsRequirements,
installationPath: details['installationPath'] as String?,
displayName: details['displayName'] as String?,
fullVersion: details['installationVersion'] as String?,
isComplete: details['isComplete'] as bool?,
isLaunchable: details['isLaunchable'] as bool?,
isRebootRequired: details['isRebootRequired'] as bool?,
isPrerelease: details['isPrerelease'] as bool?,
catalogDisplayVersion: catalog == null ? null : catalog['productDisplayVersion'] as String?,
);
}

/// Whether the installation satisfies the required workloads and minimum version.
final bool meetsRequirements;

/// The root directory of the Visual Studio installation.
final String? installationPath;

/// The user-friendly name of the installation.
final String? displayName;

/// The complete version.
final String? fullVersion;

/// Keys for the status of the installation.
final bool? isComplete;
final bool? isLaunchable;
final bool? isRebootRequired;

/// The key for a pre-release version.
final bool? isPrerelease;

/// The user-friendly version.
final String? catalogDisplayVersion;

/// Checks if the Visual Studio installation can be used by Flutter.
///
/// Returns false if the installation has issues the user must resolve.
/// This may return true even if required information is missing as older
/// versions of Visual Studio might not include them.
bool get isUsable {
if (!meetsRequirements) {
return false;
}

if (!(isComplete ?? true)) {
return false;
}

if (!(isLaunchable ?? true)) {
return false;
}

if (isRebootRequired ?? false) {
return false;
}

return true;
}
}
Loading

0 comments on commit 52e19ef

Please sign in to comment.