Unverified Commit 52e19ef7 authored by Loïc Sharma's avatar Loïc Sharma Committed by GitHub

Refactor vswhere.exe integration (#104133)

`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 https://github.com/flutter/flutter/issues/102451.
parent 874b6c08
......@@ -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';
......@@ -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;
......@@ -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
......@@ -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++';
......@@ -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',
......@@ -253,33 +250,6 @@ 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';
......@@ -287,10 +257,11 @@ class VisualStudio {
/// 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
......@@ -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 {
......@@ -334,90 +305,39 @@ class VisualStudio {
return null;
}
/// Checks if the given installation has issues that the user must resolve.
/// Returns the details of the best available version of Visual Studio.
///
/// 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.
///
/// 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(
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;
}
// An installation that satifies requirements could not be found.
// Fallback to the latest Visual Studio installation.
return _visualStudioDetails(
additionalArguments: <String>[_vswherePrereleaseArgument, '-all']);
}();
/// Returns the installation location of the Windows 10 SDKs, or null if the
/// registry doesn't contain that information.
......@@ -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;
}
}
......@@ -863,6 +863,98 @@ void main() {
expect(visualStudio.getWindows10SDKVersion(), null);
});
});
group(VswhereDetails, () {
test('Accepts empty JSON', () {
const bool meetsRequirements = true;
final Map<String, dynamic> json = <String, dynamic>{};
final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json);
expect(result.installationPath, null);
expect(result.displayName, null);
expect(result.fullVersion, null);
expect(result.isComplete, null);
expect(result.isLaunchable, null);
expect(result.isRebootRequired, null);
expect(result.isPrerelease, null);
expect(result.catalogDisplayVersion, null);
expect(result.isUsable, isTrue);
});
test('Ignores unknown JSON properties', () {
const bool meetsRequirements = true;
final Map<String, dynamic> json = <String, dynamic>{
'hello': 'world',
};
final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json);
expect(result.installationPath, null);
expect(result.displayName, null);
expect(result.fullVersion, null);
expect(result.isComplete, null);
expect(result.isLaunchable, null);
expect(result.isRebootRequired, null);
expect(result.isPrerelease, null);
expect(result.catalogDisplayVersion, null);
expect(result.isUsable, isTrue);
});
test('Accepts JSON', () {
const bool meetsRequirements = true;
final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, _defaultResponse);
expect(result.installationPath, visualStudioPath);
expect(result.displayName, 'Visual Studio Community 2019');
expect(result.fullVersion, '16.2.29306.81');
expect(result.isComplete, true);
expect(result.isLaunchable, true);
expect(result.isRebootRequired, false);
expect(result.isPrerelease, false);
expect(result.catalogDisplayVersion, '16.2.5');
expect(result.isUsable, isTrue);
});
test('Installation that does not satisfy requirements is not usable', () {
const bool meetsRequirements = false;
final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, _defaultResponse);
expect(result.isUsable, isFalse);
});
test('Incomplete installation is not usable', () {
const bool meetsRequirements = true;
final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse)
..['isComplete'] = false;
final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json);
expect(result.isUsable, isFalse);
});
test('Unlaunchable installation is not usable', () {
const bool meetsRequirements = true;
final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse)
..['isLaunchable'] = false;
final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json);
expect(result.isUsable, isFalse);
});
test('Installation that requires reboot is not usable', () {
const bool meetsRequirements = true;
final Map<String, dynamic> json = Map<String, dynamic>.of(_defaultResponse)
..['isRebootRequired'] = true;
final VswhereDetails result = VswhereDetails.fromJson(meetsRequirements, json);
expect(result.isUsable, isFalse);
});
});
}
class VisualStudioFixture {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment