visual_studio.dart 17.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:process/process.dart';
6

7
import '../base/common.dart';
8
import '../base/file_system.dart';
9
import '../base/io.dart';
10
import '../base/logger.dart';
11
import '../base/platform.dart';
12
import '../base/process.dart';
13
import '../base/version.dart';
14 15 16 17
import '../convert.dart';

/// Encapsulates information about the installed copy of Visual Studio, if any.
class VisualStudio {
18
  VisualStudio({
19 20 21 22
    required FileSystem fileSystem,
    required ProcessManager processManager,
    required Platform platform,
    required Logger logger,
23 24 25 26 27 28 29 30
  }) : _platform = platform,
       _fileSystem = fileSystem,
       _processUtils = ProcessUtils(processManager: processManager, logger: logger);

  final FileSystem _fileSystem;
  final Platform _platform;
  final ProcessUtils _processUtils;

31
  /// True if Visual Studio installation was found.
32 33 34 35
  ///
  /// 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.
36
  bool get isInstalled => _bestVisualStudioDetails.isNotEmpty;
37

38
  bool get isAtLeastMinimumVersion {
39
    final int? installedMajorVersion = _majorVersion;
40 41 42
    return installedMajorVersion != null && installedMajorVersion >= _minimumSupportedVersion;
  }

43 44
  /// True if there is a version of Visual Studio with all the components
  /// necessary to build the project.
45
  bool get hasNecessaryComponents => _usableVisualStudioDetails.isNotEmpty;
46 47 48

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

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

  /// The directory where Visual Studio is installed.
63
  String? get installLocation => _bestVisualStudioDetails[_installationPathKey] as String?;
64 65 66 67

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

70 71 72 73
  // 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.

74 75 76 77 78 79 80
  /// True if there is a complete installation of Visual Studio.
  ///
  /// False if installation is not found.
  bool get isComplete {
    if (_bestVisualStudioDetails.isEmpty) {
      return false;
    }
81
    return _bestVisualStudioDetails[_isCompleteKey] as bool? ?? true;
82
  }
83 84

  /// True if Visual Studio is launchable.
85 86 87 88 89 90
  ///
  /// False if installation is not found.
  bool get isLaunchable {
    if (_bestVisualStudioDetails.isEmpty) {
      return false;
    }
91
    return _bestVisualStudioDetails[_isLaunchableKey] as bool? ?? true;
92
  }
93

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

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

100 101 102
  /// The name of the recommended Visual Studio installer workload.
  String get workloadDescription => 'Desktop development with C++';

103 104 105
  /// Returns the highest installed Windows 10 SDK version, or null if none is
  /// found.
  ///
106
  /// For instance: 10.0.18362.0.
107 108
  String? getWindows10SDKVersion() {
    final String? sdkLocation = _getWindows10SdkLocation();
109 110 111 112 113 114 115 116
    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.
117
    Version? highestVersion;
118 119 120 121
    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.
122 123
        final Version? version = Version.parse(versionEntry.basename.substring(3));
        if (highestVersion == null || (version != null && version > highestVersion)) {
124 125 126 127 128 129 130 131 132 133
          highestVersion = version;
        }
      }
    }
    if (highestVersion == null) {
      return null;
    }
    return '10.$highestVersion';
  }

134 135
  /// The names of the components within the workload that must be installed.
  ///
136 137 138 139 140 141 142
  /// 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();
  }

143
  /// The consumer-facing version name of the minimum supported version.
144 145 146 147
  ///
  /// E.g., for Visual Studio 2019 this returns "2019" rather than "16".
  String get minimumVersionDescription {
    return '2019';
148 149
  }

150
  /// The path to CMake, or null if no Visual Studio installation has
151
  /// the components necessary to build.
152
  String? get cmakePath {
153
    final Map<String, dynamic> details = _usableVisualStudioDetails;
154
    if (details.isEmpty || _usableVisualStudioDetails[_installationPathKey] == null) {
155 156
      return null;
    }
157
    return _fileSystem.path.joinAll(<String>[
158
      _usableVisualStudioDetails[_installationPathKey] as String,
159 160 161 162 163 164 165 166 167
      'Common7',
      'IDE',
      'CommonExtensions',
      'Microsoft',
      'CMake',
      'CMake',
      'bin',
      'cmake.exe',
    ]);
168 169
  }

170 171 172 173 174 175 176 177 178 179 180 181 182
  /// 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';
    }
  }

183
  /// The major version of the Visual Studio install, as an integer.
184
  int? get _majorVersion => fullVersion != null ? int.tryParse(fullVersion!.split('.')[0]) : null;
185

186 187 188 189 190 191
  /// 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.
192 193 194 195 196 197 198 199 200 201 202 203
  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',
    );
  }
204

205 206
  /// Workload ID for use with vswhere requirements.
  ///
207
  /// Workload ID is different between Visual Studio IDE and Build Tools.
208
  /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
209 210 211 212
  static const List<String> _requiredWorkloads = <String>[
    'Microsoft.VisualStudio.Workload.NativeDesktop',
    'Microsoft.VisualStudio.Workload.VCTools'
  ];
213

214
  /// Components for use with vswhere requirements.
215 216 217
  ///
  /// Maps from component IDs to description in the installer UI.
  /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
218
  Map<String, String> _requiredComponents([int? majorVersion]) {
219 220
    // The description of the C++ toolchain required by the template. The
    // component name is significantly different in different versions.
221
    // When a new major version of VS is supported, its toolchain description
222 223 224 225 226 227 228 229
    // 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';
230
    }
231 232 233 234 235
    // 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.
236
    cppToolchainDescription += '\n   - If there are multiple build tool versions available, install the latest';
237 238
    // Things which are required by the workload (e.g., MSBuild) don't need to
    // be included here.
239 240 241
    return <String, String>{
      // The C++ toolchain required by the template.
      'Microsoft.VisualStudio.Component.VC.Tools.x86.x64': cppToolchainDescription,
242 243
      // CMake
      'Microsoft.VisualStudio.Component.VC.CMake.Project': 'C++ CMake tools for Windows',
244 245 246
    };
  }

247 248 249
  /// The minimum supported major version.
  static const int _minimumSupportedVersion = 16;  // '16' is VS 2019.

250
  /// vswhere argument to specify the minimum version.
251 252 253 254 255
  static const String _vswhereMinVersionArgument = '-version';

  /// vswhere argument to allow prerelease versions.
  static const String _vswherePrereleaseArgument = '-prerelease';

256 257 258 259 260 261 262 263 264 265 266
  // 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';

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

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

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

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

283 284 285 286 287 288 289 290 291 292
  /// 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.
  /// If [validateRequirements] is set, the search will be limited to versions
  /// that have all of the required workloads and components.
293
  Map<String, dynamic>? _visualStudioDetails({
294
      bool validateRequirements = false,
295 296
      List<String>? additionalArguments,
      String? requiredWorkload
297 298 299
    }) {
    final List<String> requirementArguments = validateRequirements
        ? <String>[
300 301 302 303
            if (requiredWorkload != null) ...<String>[
              '-requires',
              requiredWorkload,
            ],
304 305 306
            ..._requiredComponents(_minimumSupportedVersion).keys
          ]
        : <String>[];
307
    try {
308
      final List<String> defaultArguments = <String>[
309
        '-format', 'json',
310
        '-products', '*',
311 312
        '-utf8',
        '-latest',
313
      ];
314
      final RunResult whereResult = _processUtils.runSync(<String>[
315 316 317
        _vswherePath,
        ...defaultArguments,
        ...?additionalArguments,
318
        ...requirementArguments,
319
      ], encoding: utf8);
320
      if (whereResult.exitCode == 0) {
321
        final List<Map<String, dynamic>> installations =
322
            (json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
323 324 325 326
        if (installations.isNotEmpty) {
          return installations[0];
        }
      }
327 328
    } on ArgumentError {
      // Thrown if vswhere doesn't exist; ignore and return null below.
329 330
    } on ProcessException {
      // Ignored, return null below.
331 332
    } on FormatException {
      // may be thrown if invalid JSON is returned.
333 334 335 336
    }
    return null;
  }

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

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

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

355
    return false;
356 357
  }

358
  /// Returns the details dictionary for the latest version of Visual Studio
359 360
  /// that has all required components and is a supported version, or {} if
  /// there is no such installation.
361 362 363
  ///
  /// 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.
364
  late final Map<String, dynamic> _usableVisualStudioDetails = (){
365 366 367 368
    final List<String> minimumVersionArguments = <String>[
      _vswhereMinVersionArgument,
      _minimumSupportedVersion.toString(),
    ];
369
    Map<String, dynamic>? visualStudioDetails;
370 371 372 373 374 375 376 377 378 379 380
    // 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(
          validateRequirements: true,
          additionalArguments: checkForPrerelease
              ? <String>[...minimumVersionArguments, _vswherePrereleaseArgument]
              : minimumVersionArguments,
          requiredWorkload: requiredWorkload);
      }
    }
381

382
    Map<String, dynamic>? usableVisualStudioDetails;
383 384 385 386
    if (visualStudioDetails != null) {
      if (installationHasIssues(visualStudioDetails)) {
        _cachedAnyVisualStudioDetails = visualStudioDetails;
      } else {
387
        usableVisualStudioDetails = visualStudioDetails;
388 389
      }
    }
390 391
    return usableVisualStudioDetails ?? <String, dynamic>{};
  }();
392 393

  /// Returns the details dictionary of the latest version of Visual Studio,
394 395
  /// regardless of components and version, or {} if no such installation is
  /// found.
396
  ///
397 398 399
  /// 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.
400
  Map<String, dynamic>? _cachedAnyVisualStudioDetails;
401
  Map<String, dynamic> get _anyVisualStudioDetails {
402 403
    // Search for all types of installations.
    _cachedAnyVisualStudioDetails ??= _visualStudioDetails(
404
        additionalArguments: <String>[_vswherePrereleaseArgument, '-all']);
405 406
    // Add a sentinel empty value to avoid querying vswhere again.
    _cachedAnyVisualStudioDetails ??= <String, dynamic>{};
407
    return _cachedAnyVisualStudioDetails!;
408 409 410
  }

  /// Returns the details dictionary of the best available version of Visual
411 412 413
  /// Studio.
  ///
  /// 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 418 419 420
      return _usableVisualStudioDetails;
    }
    return _anyVisualStudioDetails;
  }
421 422 423

  /// Returns the installation location of the Windows 10 SDKs, or null if the
  /// registry doesn't contain that information.
424
  String? _getWindows10SdkLocation() {
425 426 427 428 429 430 431 432 433 434
    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+(.+)');
435
        final RegExpMatch? match = pattern.firstMatch(result.stdout);
436
        if (match != null) {
437
          return match.group(1)!.trim();
438 439
        }
      }
440 441
    } on ArgumentError {
      // Thrown if reg somehow doesn't exist; ignore and return null below.
442 443 444 445 446 447 448 449 450 451
    } 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.
452
  String? findHighestVersionInSdkDirectory(Directory dir) {
453 454 455 456 457
    // This contains subfolders that are named by the SDK version.
    final Directory includeDir = dir.childDirectory('Includes');
    if (!includeDir.existsSync()) {
      return null;
    }
458
    Version? highestVersion;
459 460 461 462 463 464
    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.
465 466
      final Version? version = Version.parse(versionEntry.basename.substring(3));
      if (highestVersion == null || (version != null && version > highestVersion)) {
467 468 469 470 471 472
        highestVersion = version;
      }
    }
    // Re-add the leading '10.' that was removed for comparison.
    return highestVersion == null ? null : '10.$highestVersion';
  }
473
}