visual_studio.dart 17.1 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 6 7 8
import 'package:meta/meta.dart';
import 'package:process/process.dart';

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 19 20 21 22 23 24 25 26 27 28 29 30
  VisualStudio({
    @required FileSystem fileSystem,
    @required ProcessManager processManager,
    @required Platform platform,
    @required Logger logger,
  }) : _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 39 40 41 42
  bool get isAtLeastMinimumVersion {
    final int installedMajorVersion = _majorVersion;
    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 56 57 58
  String get displayVersion {
    if (_bestVisualStudioDetails[_catalogKey] == null) {
      return null;
    }
59
    return _bestVisualStudioDetails[_catalogKey][_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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
  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 > highestVersion) {
          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) {
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
  /// The major version of the Visual Studio install, as an integer.
  int get _majorVersion => fullVersion != null ? int.tryParse(fullVersion.split('.')[0]) : null;

173 174 175 176 177 178
  /// 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.
179 180
  String get _vswherePath => _fileSystem.path.join(
    _platform.environment['PROGRAMFILES(X86)'],
181 182 183 184 185
    'Microsoft Visual Studio',
    'Installer',
    'vswhere.exe',
  );

186 187
  /// Workload ID for use with vswhere requirements.
  ///
188
  /// Workload ID is different between Visual Studio IDE and Build Tools.
189
  /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
190 191 192 193
  static const List<String> _requiredWorkloads = <String>[
    'Microsoft.VisualStudio.Workload.NativeDesktop',
    'Microsoft.VisualStudio.Workload.VCTools'
  ];
194

195
  /// Components for use with vswhere requirements.
196 197 198
  ///
  /// Maps from component IDs to description in the installer UI.
  /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
199
  Map<String, String> _requiredComponents([int majorVersion]) {
200 201
    // The description of the C++ toolchain required by the template. The
    // component name is significantly different in different versions.
202
    // When a new major version of VS is supported, its toolchain description
203 204 205 206 207 208 209 210
    // 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';
211
    }
212 213 214 215 216
    // 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.
217
    cppToolchainDescription += '\n   - If there are multiple build tool versions available, install the latest';
218 219
    // Things which are required by the workload (e.g., MSBuild) don't need to
    // be included here.
220 221 222
    return <String, String>{
      // The C++ toolchain required by the template.
      'Microsoft.VisualStudio.Component.VC.Tools.x86.x64': cppToolchainDescription,
223 224
      // CMake
      'Microsoft.VisualStudio.Component.VC.CMake.Project': 'C++ CMake tools for Windows',
225 226 227
    };
  }

228 229 230
  /// The minimum supported major version.
  static const int _minimumSupportedVersion = 16;  // '16' is VS 2019.

231
  /// vswhere argument to specify the minimum version.
232 233 234 235 236
  static const String _vswhereMinVersionArgument = '-version';

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

237 238 239 240 241 242 243 244 245 246 247
  // 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';

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

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

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

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

264 265 266 267 268 269 270 271 272 273 274 275 276
  /// 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.
  Map<String, dynamic> _visualStudioDetails({
      bool validateRequirements = false,
      List<String> additionalArguments,
277
      String requiredWorkload
278 279 280 281
    }) {
    final List<String> requirementArguments = validateRequirements
        ? <String>[
            '-requires',
282
            requiredWorkload,
283 284 285
            ..._requiredComponents(_minimumSupportedVersion).keys
          ]
        : <String>[];
286
    try {
287
      final List<String> defaultArguments = <String>[
288
        '-format', 'json',
289
        '-products', '*',
290 291
        '-utf8',
        '-latest',
292
      ];
293
      final RunResult whereResult = _processUtils.runSync(<String>[
294 295 296
        _vswherePath,
        ...defaultArguments,
        ...?additionalArguments,
297
        ...?requirementArguments,
298
      ], encoding: utf8);
299
      if (whereResult.exitCode == 0) {
300
        final List<Map<String, dynamic>> installations =
301
            (json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
302 303 304 305
        if (installations.isNotEmpty) {
          return installations[0];
        }
      }
306 307
    } on ArgumentError {
      // Thrown if vswhere doesn't exist; ignore and return null below.
308 309
    } on ProcessException {
      // Ignored, return null below.
310 311
    } on FormatException {
      // may be thrown if invalid JSON is returned.
312 313 314 315
    }
    return null;
  }

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

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

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

334
    return false;
335 336
  }

337
  /// Returns the details dictionary for the latest version of Visual Studio
338 339
  /// that has all required components and is a supported version, or {} if
  /// there is no such installation.
340 341 342
  ///
  /// 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.
343 344
  Map<String, dynamic> _cachedUsableVisualStudioDetails;
  Map<String, dynamic> get _usableVisualStudioDetails {
345 346 347
    if (_cachedUsableVisualStudioDetails != null) {
      return _cachedUsableVisualStudioDetails;
    }
348 349 350 351
    final List<String> minimumVersionArguments = <String>[
      _vswhereMinVersionArgument,
      _minimumSupportedVersion.toString(),
    ];
352 353 354 355 356 357 358 359 360 361 362 363
    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(
          validateRequirements: true,
          additionalArguments: checkForPrerelease
              ? <String>[...minimumVersionArguments, _vswherePrereleaseArgument]
              : minimumVersionArguments,
          requiredWorkload: requiredWorkload);
      }
    }
364 365 366 367 368 369

    if (visualStudioDetails != null) {
      if (installationHasIssues(visualStudioDetails)) {
        _cachedAnyVisualStudioDetails = visualStudioDetails;
      } else {
        _cachedUsableVisualStudioDetails = visualStudioDetails;
370 371
      }
    }
372
    _cachedUsableVisualStudioDetails ??= <String, dynamic>{};
373 374 375 376
    return _cachedUsableVisualStudioDetails;
  }

  /// Returns the details dictionary of the latest version of Visual Studio,
377 378
  /// regardless of components and version, or {} if no such installation is
  /// found.
379
  ///
380 381 382
  /// 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.
383 384
  Map<String, dynamic> _cachedAnyVisualStudioDetails;
  Map<String, dynamic> get _anyVisualStudioDetails {
385 386
    // Search for all types of installations.
    _cachedAnyVisualStudioDetails ??= _visualStudioDetails(
387
        additionalArguments: <String>[_vswherePrereleaseArgument, '-all']);
388 389
    // Add a sentinel empty value to avoid querying vswhere again.
    _cachedAnyVisualStudioDetails ??= <String, dynamic>{};
390 391 392 393
    return _cachedAnyVisualStudioDetails;
  }

  /// Returns the details dictionary of the best available version of Visual
394 395 396
  /// Studio.
  ///
  /// If there's a version that has all the required components, that
397
  /// will be returned, otherwise returns the latest installed version (if any).
398
  Map<String, dynamic> get _bestVisualStudioDetails {
399
    if (_usableVisualStudioDetails.isNotEmpty) {
400 401 402 403
      return _usableVisualStudioDetails;
    }
    return _anyVisualStudioDetails;
  }
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422

  /// 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();
        }
      }
423 424
    } on ArgumentError {
      // Thrown if reg somehow doesn't exist; ignore and return null below.
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
    } 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 > highestVersion) {
        highestVersion = version;
      }
    }
    // Re-add the leading '10.' that was removed for comparison.
    return highestVersion == null ? null : '10.$highestVersion';
  }
456
}