Skip to content

Commit 52e19ef

Browse files
authored
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 flutter/flutter#102451.
1 parent 874b6c0 commit 52e19ef

File tree

2 files changed

+214
-118
lines changed

2 files changed

+214
-118
lines changed

packages/flutter_tools/lib/src/windows/visual_studio.dart

+122-118
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:meta/meta.dart';
56
import 'package:process/process.dart';
67

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

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

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

4748
/// The name of the Visual Studio install.
4849
///
4950
/// For instance: "Visual Studio Community 2019".
50-
String? get displayName => _bestVisualStudioDetails[_displayNameKey] as String?;
51+
String? get displayName => _bestVisualStudioDetails?.displayName;
5152

5253
/// The user-friendly version number of the Visual Studio install.
5354
///
5455
/// For instance: "15.4.0".
55-
String? get displayVersion {
56-
if (_bestVisualStudioDetails[_catalogKey] == null) {
57-
return null;
58-
}
59-
return (_bestVisualStudioDetails[_catalogKey] as Map<String, dynamic>)[_catalogDisplayVersionKey] as String?;
60-
}
56+
String? get displayVersion => _bestVisualStudioDetails?.catalogDisplayVersion;
6157

6258
/// The directory where Visual Studio is installed.
63-
String? get installLocation => _bestVisualStudioDetails[_installationPathKey] as String?;
59+
String? get installLocation => _bestVisualStudioDetails?.installationPath;
6460

6561
/// The full version of the Visual Studio install.
6662
///
6763
/// For instance: "15.4.27004.2002".
68-
String? get fullVersion => _bestVisualStudioDetails[_fullVersionKey] as String?;
64+
String? get fullVersion => _bestVisualStudioDetails?.fullVersion;
6965

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

8480
/// True if Visual Studio is launchable.
8581
///
8682
/// False if installation is not found.
8783
bool get isLaunchable {
88-
if (_bestVisualStudioDetails.isEmpty) {
84+
if (_bestVisualStudioDetails == null) {
8985
return false;
9086
}
91-
return _bestVisualStudioDetails[_isLaunchableKey] as bool? ?? true;
87+
return _bestVisualStudioDetails!.isLaunchable ?? true;
9288
}
9389

94-
/// True if the Visual Studio installation is as pre-release version.
95-
bool get isPrerelease => _bestVisualStudioDetails[_isPrereleaseKey] as bool? ?? false;
90+
/// True if the Visual Studio installation is a pre-release version.
91+
bool get isPrerelease => _bestVisualStudioDetails?.isPrerelease ?? false;
9692

9793
/// True if a reboot is required to complete the Visual Studio installation.
98-
bool get isRebootRequired => _bestVisualStudioDetails[_isRebootRequiredKey] as bool? ?? false;
94+
bool get isRebootRequired => _bestVisualStudioDetails?.isRebootRequired ?? false;
9995

10096
/// The name of the recommended Visual Studio installer workload.
10197
String get workloadDescription => 'Desktop development with C++';
@@ -150,12 +146,13 @@ class VisualStudio {
150146
/// The path to CMake, or null if no Visual Studio installation has
151147
/// the components necessary to build.
152148
String? get cmakePath {
153-
final Map<String, dynamic> details = _usableVisualStudioDetails;
154-
if (details.isEmpty || _usableVisualStudioDetails[_installationPathKey] == null) {
149+
final VswhereDetails? details = _bestVisualStudioDetails;
150+
if (details == null || !details.isUsable || details.installationPath == null) {
155151
return null;
156152
}
153+
157154
return _fileSystem.path.joinAll(<String>[
158-
_usableVisualStudioDetails[_installationPathKey] as String,
155+
details.installationPath!,
159156
'Common7',
160157
'IDE',
161158
'CommonExtensions',
@@ -253,44 +250,18 @@ class VisualStudio {
253250
/// vswhere argument to allow prerelease versions.
254251
static const String _vswherePrereleaseArgument = '-prerelease';
255252

256-
// Keys in a VS details dictionary returned from vswhere.
257-
258-
/// The root directory of the Visual Studio installation.
259-
static const String _installationPathKey = 'installationPath';
260-
261-
/// The user-friendly name of the installation.
262-
static const String _displayNameKey = 'displayName';
263-
264-
/// The complete version.
265-
static const String _fullVersionKey = 'installationVersion';
266-
267-
/// Keys for the status of the installation.
268-
static const String _isCompleteKey = 'isComplete';
269-
static const String _isLaunchableKey = 'isLaunchable';
270-
static const String _isRebootRequiredKey = 'isRebootRequired';
271-
272-
/// The 'catalog' entry containing more details.
273-
static const String _catalogKey = 'catalog';
274-
275-
/// The key for a pre-release version.
276-
static const String _isPrereleaseKey = 'isPrerelease';
277-
278-
/// The user-friendly version.
279-
///
280-
/// This key is under the 'catalog' entry.
281-
static const String _catalogDisplayVersionKey = 'productDisplayVersion';
282-
283253
/// The registry path for Windows 10 SDK installation details.
284254
static const String _windows10SdkRegistryPath = r'HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Microsoft SDKs\Windows\v10.0';
285255

286256
/// The registry key in _windows10SdkRegistryPath for the folder where the
287257
/// SDKs are installed.
288258
static const String _windows10SdkRegistryKey = 'InstallationFolder';
289259

290-
/// Returns the details dictionary for the newest version of Visual Studio.
260+
/// Returns the details of the newest version of Visual Studio.
261+
///
291262
/// If [validateRequirements] is set, the search will be limited to versions
292263
/// that have all of the required workloads and components.
293-
Map<String, dynamic>? _visualStudioDetails({
264+
VswhereDetails? _visualStudioDetails({
294265
bool validateRequirements = false,
295266
List<String>? additionalArguments,
296267
String? requiredWorkload
@@ -321,7 +292,7 @@ class VisualStudio {
321292
final List<Map<String, dynamic>> installations =
322293
(json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
323294
if (installations.isNotEmpty) {
324-
return installations[0];
295+
return VswhereDetails.fromJson(validateRequirements, installations[0]);
325296
}
326297
}
327298
} on ArgumentError {
@@ -334,90 +305,39 @@ class VisualStudio {
334305
return null;
335306
}
336307

337-
/// Checks if the given installation has issues that the user must resolve.
338-
///
339-
/// Returns false if the required information is missing since older versions
340-
/// of Visual Studio might not include them.
341-
bool installationHasIssues(Map<String, dynamic>installationDetails) {
342-
assert(installationDetails != null);
343-
if (installationDetails[_isCompleteKey] != null && !(installationDetails[_isCompleteKey] as bool)) {
344-
return true;
345-
}
346-
347-
if (installationDetails[_isLaunchableKey] != null && !(installationDetails[_isLaunchableKey] as bool)) {
348-
return true;
349-
}
350-
351-
if (installationDetails[_isRebootRequiredKey] != null && installationDetails[_isRebootRequiredKey] as bool) {
352-
return true;
353-
}
354-
355-
return false;
356-
}
357-
358-
/// Returns the details dictionary for the latest version of Visual Studio
359-
/// that has all required components and is a supported version, or {} if
360-
/// there is no such installation.
308+
/// Returns the details of the best available version of Visual Studio.
361309
///
362-
/// If no installation is found, the cached VS details are set to an empty map
363-
/// to avoid repeating vswhere queries that have already not found an installation.
364-
late final Map<String, dynamic> _usableVisualStudioDetails = (){
310+
/// If there's a version that has all the required components, that
311+
/// will be returned, otherwise returns the latest installed version regardless
312+
/// of components and version, or null if no such installation is found.
313+
late final VswhereDetails? _bestVisualStudioDetails = () {
314+
// First, attempt to find the latest version of Visual Studio that satifies
315+
// both the minimum supported version and the required workloads.
316+
// Check in the order of stable VS, stable BT, pre-release VS, pre-release BT.
365317
final List<String> minimumVersionArguments = <String>[
366318
_vswhereMinVersionArgument,
367319
_minimumSupportedVersion.toString(),
368320
];
369-
Map<String, dynamic>? visualStudioDetails;
370-
// Check in the order of stable VS, stable BT, pre-release VS, pre-release BT
371321
for (final bool checkForPrerelease in <bool>[false, true]) {
372322
for (final String requiredWorkload in _requiredWorkloads) {
373-
visualStudioDetails ??= _visualStudioDetails(
323+
final VswhereDetails? result = _visualStudioDetails(
374324
validateRequirements: true,
375325
additionalArguments: checkForPrerelease
376326
? <String>[...minimumVersionArguments, _vswherePrereleaseArgument]
377327
: minimumVersionArguments,
378328
requiredWorkload: requiredWorkload);
379-
}
380-
}
381329

382-
Map<String, dynamic>? usableVisualStudioDetails;
383-
if (visualStudioDetails != null) {
384-
if (installationHasIssues(visualStudioDetails)) {
385-
_cachedAnyVisualStudioDetails = visualStudioDetails;
386-
} else {
387-
usableVisualStudioDetails = visualStudioDetails;
330+
if (result != null) {
331+
return result;
332+
}
388333
}
389334
}
390-
return usableVisualStudioDetails ?? <String, dynamic>{};
391-
}();
392335

393-
/// Returns the details dictionary of the latest version of Visual Studio,
394-
/// regardless of components and version, or {} if no such installation is
395-
/// found.
396-
///
397-
/// If no installation is found, the cached VS details are set to an empty map
398-
/// to avoid repeating vswhere queries that have already not found an
399-
/// installation.
400-
Map<String, dynamic>? _cachedAnyVisualStudioDetails;
401-
Map<String, dynamic> get _anyVisualStudioDetails {
402-
// Search for all types of installations.
403-
_cachedAnyVisualStudioDetails ??= _visualStudioDetails(
336+
// An installation that satifies requirements could not be found.
337+
// Fallback to the latest Visual Studio installation.
338+
return _visualStudioDetails(
404339
additionalArguments: <String>[_vswherePrereleaseArgument, '-all']);
405-
// Add a sentinel empty value to avoid querying vswhere again.
406-
_cachedAnyVisualStudioDetails ??= <String, dynamic>{};
407-
return _cachedAnyVisualStudioDetails!;
408-
}
409-
410-
/// Returns the details dictionary of the best available version of Visual
411-
/// Studio.
412-
///
413-
/// If there's a version that has all the required components, that
414-
/// will be returned, otherwise returns the latest installed version (if any).
415-
Map<String, dynamic> get _bestVisualStudioDetails {
416-
if (_usableVisualStudioDetails.isNotEmpty) {
417-
return _usableVisualStudioDetails;
418-
}
419-
return _anyVisualStudioDetails;
420-
}
340+
}();
421341

422342
/// Returns the installation location of the Windows 10 SDKs, or null if the
423343
/// registry doesn't contain that information.
@@ -471,3 +391,87 @@ class VisualStudio {
471391
return highestVersion == null ? null : '10.$highestVersion';
472392
}
473393
}
394+
395+
/// The details of a Visual Studio installation according to vswhere.
396+
@visibleForTesting
397+
class VswhereDetails {
398+
const VswhereDetails({
399+
required this.meetsRequirements,
400+
required this.installationPath,
401+
required this.displayName,
402+
required this.fullVersion,
403+
required this.isComplete,
404+
required this.isLaunchable,
405+
required this.isRebootRequired,
406+
required this.isPrerelease,
407+
required this.catalogDisplayVersion,
408+
});
409+
410+
/// Create a `VswhereDetails` from the JSON output of vswhere.exe.
411+
factory VswhereDetails.fromJson(
412+
bool meetsRequirements,
413+
Map<String, dynamic> details
414+
) {
415+
final Map<String, dynamic>? catalog = details['catalog'] as Map<String, dynamic>?;
416+
417+
return VswhereDetails(
418+
meetsRequirements: meetsRequirements,
419+
installationPath: details['installationPath'] as String?,
420+
displayName: details['displayName'] as String?,
421+
fullVersion: details['installationVersion'] as String?,
422+
isComplete: details['isComplete'] as bool?,
423+
isLaunchable: details['isLaunchable'] as bool?,
424+
isRebootRequired: details['isRebootRequired'] as bool?,
425+
isPrerelease: details['isPrerelease'] as bool?,
426+
catalogDisplayVersion: catalog == null ? null : catalog['productDisplayVersion'] as String?,
427+
);
428+
}
429+
430+
/// Whether the installation satisfies the required workloads and minimum version.
431+
final bool meetsRequirements;
432+
433+
/// The root directory of the Visual Studio installation.
434+
final String? installationPath;
435+
436+
/// The user-friendly name of the installation.
437+
final String? displayName;
438+
439+
/// The complete version.
440+
final String? fullVersion;
441+
442+
/// Keys for the status of the installation.
443+
final bool? isComplete;
444+
final bool? isLaunchable;
445+
final bool? isRebootRequired;
446+
447+
/// The key for a pre-release version.
448+
final bool? isPrerelease;
449+
450+
/// The user-friendly version.
451+
final String? catalogDisplayVersion;
452+
453+
/// Checks if the Visual Studio installation can be used by Flutter.
454+
///
455+
/// Returns false if the installation has issues the user must resolve.
456+
/// This may return true even if required information is missing as older
457+
/// versions of Visual Studio might not include them.
458+
bool get isUsable {
459+
if (!meetsRequirements) {
460+
return false;
461+
}
462+
463+
if (!(isComplete ?? true)) {
464+
return false;
465+
}
466+
467+
if (!(isLaunchable ?? true)) {
468+
return false;
469+
}
470+
471+
if (isRebootRequired ?? false) {
472+
return false;
473+
}
474+
475+
return true;
476+
}
477+
}

0 commit comments

Comments
 (0)