// Copyright 2014 The Flutter Authors. All rights reserved. // 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'; import '../base/file_system.dart'; 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. class VisualStudio { VisualStudio({ required FileSystem fileSystem, required ProcessManager processManager, required Platform platform, required Logger logger, }) : _platform = platform, _fileSystem = fileSystem, _processUtils = ProcessUtils(processManager: processManager, logger: logger), _logger = logger; final FileSystem _fileSystem; final Platform _platform; final ProcessUtils _processUtils; final Logger _logger; /// Matches the description property from the vswhere.exe JSON output. final RegExp _vswhereDescriptionProperty = RegExp(r'\s*"description"\s*:\s*".*"\s*,?'); /// True if Visual Studio installation was found. /// /// 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 != null; bool get isAtLeastMinimumVersion { final int? installedMajorVersion = _majorVersion; return installedMajorVersion != null && installedMajorVersion >= _minimumSupportedVersion; } /// True if there is a version of Visual Studio with all the components /// necessary to build the project. bool get hasNecessaryComponents => _bestVisualStudioDetails?.isUsable ?? false; /// The name of the Visual Studio install. /// /// For instance: "Visual Studio Community 2019". This should only be used for /// display purposes. String? get displayName => _bestVisualStudioDetails?.displayName; /// The user-friendly version number of the Visual Studio install. /// /// For instance: "15.4.0". This should only be used for display purposes. /// Logic based off the installation's version should use the `fullVersion`. String? get displayVersion => _bestVisualStudioDetails?.catalogDisplayVersion; /// The directory where Visual Studio is installed. String? get installLocation => _bestVisualStudioDetails?.installationPath; /// The full version of the Visual Studio install. /// /// For instance: "15.4.27004.2002". 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 // avoid false negatives. /// True if there is a complete installation of Visual Studio. /// /// False if installation is not found. bool get isComplete { if (_bestVisualStudioDetails == null) { return false; } return _bestVisualStudioDetails!.isComplete ?? true; } /// True if Visual Studio is launchable. /// /// False if installation is not found. bool get isLaunchable { if (_bestVisualStudioDetails == null) { return false; } return _bestVisualStudioDetails!.isLaunchable ?? true; } /// 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?.isRebootRequired ?? false; /// 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 != 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 /// a supported version is present, the descriptions used will be for that /// version. List<String> necessaryComponentDescriptions() { return _requiredComponents().values.toList(); } /// The consumer-facing version name of the minimum supported version. /// /// E.g., for Visual Studio 2019 this returns "2019" rather than "16". String get minimumVersionDescription { return '2019'; } /// The path to CMake, or null if no Visual Studio installation has /// the components necessary to build. String? get cmakePath { final VswhereDetails? details = _bestVisualStudioDetails; if (details == null || !details.isUsable || details.installationPath == null) { return null; } return _fileSystem.path.joinAll(<String>[ details.installationPath!, 'Common7', 'IDE', 'CommonExtensions', 'Microsoft', 'CMake', 'CMake', 'bin', 'cmake.exe', ]); } /// The generator string to pass to CMake to select this Visual Studio /// version. String? get cmakeGenerator { // From https://cmake.org/cmake/help/v3.22/manual/cmake-generators.7.html#visual-studio-generators switch (_majorVersion) { case 17: return 'Visual Studio 17 2022'; case 16: default: return 'Visual Studio 16 2019'; } } /// The major version of the Visual Studio install, as an integer. int? get _majorVersion => fullVersion != null ? int.tryParse(fullVersion!.split('.')[0]) : null; /// The path to vswhere.exe. /// /// vswhere should be installed for VS 2017 Update 2 and later; if it's not /// 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 { 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. /// /// Workload ID is different between Visual Studio IDE and Build Tools. /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids static const List<String> _requiredWorkloads = <String>[ 'Microsoft.VisualStudio.Workload.NativeDesktop', 'Microsoft.VisualStudio.Workload.VCTools', ]; /// Components for use with vswhere requirements. /// /// Maps from component IDs to description in the installer UI. /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids Map<String, String> _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 // should be added below. It should also be made the default, so that when // there is no installation, the message shows the string that will be // relevant for the most likely fresh install case). String cppToolchainDescription; switch (majorVersion ?? _majorVersion) { case 16: default: cppToolchainDescription = 'MSVC v142 - VS 2019 C++ x64/x86 build tools'; } // The 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64' ID is assigned to the latest // release of the toolchain, and there can be minor updates within a given version of // Visual Studio. Since it changes over time, listing a precise version would become // 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 <String, String>{ // The C++ toolchain required by the template. 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64': cppToolchainDescription, // CMake 'Microsoft.VisualStudio.Component.VC.CMake.Project': 'C++ CMake tools for Windows', }; } /// The minimum supported major version. static const int _minimumSupportedVersion = 16; // '16' is VS 2019. /// vswhere argument to specify the minimum version. static const String _vswhereMinVersionArgument = '-version'; /// vswhere argument to allow prerelease versions. static const String _vswherePrereleaseArgument = '-prerelease'; /// 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 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. VswhereDetails? _visualStudioDetails({ bool validateRequirements = false, List<String>? additionalArguments, String? requiredWorkload }) { final List<String> requirementArguments = validateRequirements ? <String>[ if (requiredWorkload != null) ...<String>[ '-requires', requiredWorkload, ], ..._requiredComponents(_minimumSupportedVersion).keys, ] : <String>[]; try { final List<String> defaultArguments = <String>[ '-format', 'json', '-products', '*', '-utf8', '-latest', ]; // Ignore replacement characters as vswhere.exe is known to output them. // See: https://github.com/flutter/flutter/issues/102451 const Encoding encoding = Utf8Codec(reportErrors: false); final RunResult whereResult = _processUtils.runSync(<String>[ _vswherePath, ...defaultArguments, ...?additionalArguments, ...requirementArguments, ], encoding: encoding); if (whereResult.exitCode == 0) { final List<Map<String, dynamic>>? installations = _tryDecodeVswhereJson(whereResult.stdout); if (installations != null && installations.isNotEmpty) { return VswhereDetails.fromJson(validateRequirements, installations[0]); } } } on ArgumentError { // Thrown if vswhere doesn't exist; ignore and return null below. } on ProcessException { // Ignored, return null below. } return null; } List<Map<String, dynamic>>? _tryDecodeVswhereJson(String vswhereJson) { List<dynamic>? result; FormatException? originalError; try { // Some versions of vswhere.exe are known to encode their output incorrectly, // resulting in invalid JSON in the 'description' property when interpreted // as UTF-8. First, try to decode without any pre-processing. try { result = json.decode(vswhereJson) as List<dynamic>; } on FormatException catch (error) { // If that fails, remove the 'description' property and try again. // See: https://github.com/flutter/flutter/issues/106601 vswhereJson = vswhereJson.replaceFirst(_vswhereDescriptionProperty, ''); _logger.printTrace('Failed to decode vswhere.exe JSON output. $error' 'Retrying after removing the unused description property:\n$vswhereJson'); originalError = error; result = json.decode(vswhereJson) as List<dynamic>; } } on FormatException { // Removing the description property didn't help. // Report the original decoding error on the unprocessed JSON. _logger.printWarning('Warning: Unexpected vswhere.exe JSON output. $originalError' 'To see the full JSON, run flutter doctor -vv.'); return null; } return result.cast<Map<String, dynamic>>(); } /// Returns the details 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 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 satisfies // 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(), ]; for (final bool checkForPrerelease in <bool>[false, true]) { for (final String requiredWorkload in _requiredWorkloads) { final VswhereDetails? result = _visualStudioDetails( validateRequirements: true, additionalArguments: checkForPrerelease ? <String>[...minimumVersionArguments, _vswherePrereleaseArgument] : minimumVersionArguments, requiredWorkload: requiredWorkload); if (result != null) { return result; } } } // An installation that satisfies 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. String? _getWindows10SdkLocation() { try { final RunResult result = _processUtils.runSync(<String>[ '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 != null && version > highestVersion)) { highestVersion = version; } } // Re-add the leading '10.' that was removed for comparison. 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, isComplete: details['isComplete'] as bool?, isLaunchable: details['isLaunchable'] as bool?, isRebootRequired: details['isRebootRequired'] as bool?, isPrerelease: details['isPrerelease'] as bool?, // Below are strings that must be well-formed without replacement characters. installationPath: _validateString(details['installationPath'] as String?), fullVersion: _validateString(details['installationVersion'] as String?), // Below are strings that are used only for display purposes and are allowed to // contain replacement characters. displayName: details['displayName'] as String?, catalogDisplayVersion: catalog == null ? null : catalog['productDisplayVersion'] as String?, ); } /// Verify JSON strings from vswhere.exe output are valid. /// /// The output of vswhere.exe is known to output replacement characters. /// Use this to ensure values that must be well-formed are valid. Strings that /// are only used for display purposes should skip this check. /// See: https://github.com/flutter/flutter/issues/102451 static String? _validateString(String? value) { if (value != null && value.contains('\u{FFFD}')) { throwToolExit( 'Bad UTF-8 encoding (U+FFFD; REPLACEMENT CHARACTER) found in string: $value. ' 'The Flutter team would greatly appreciate if you could file a bug explaining ' 'exactly what you were doing when this happened:\n' 'https://github.com/flutter/flutter/issues/new/choose\n'); } return value; } /// 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; } }