visual_studio.dart 13.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 14 15 16
import '../convert.dart';

/// Encapsulates information about the installed copy of Visual Studio, if any.
class VisualStudio {
17 18 19 20 21 22 23 24 25 26 27 28 29
  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;

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

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

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

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

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

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

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

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

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

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

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

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

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

  /// The names of the components within the workload that must be installed.
  ///
104 105 106 107 108 109 110 111 112 113 114 115
  /// 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 minumum supported version.
  ///
  /// E.g., for Visual Studio 2019 this returns "2019" rather than "16".
  String get minimumVersionDescription {
    return '2019';
116 117 118 119 120 121
  }

  /// The path to vcvars64.bat, or null if no Visual Studio installation has
  /// the components necessary to build.
  String get vcvarsPath {
    final Map<String, dynamic> details = _usableVisualStudioDetails;
122
    if (details.isEmpty) {
123 124
      return null;
    }
125
    return _fileSystem.path.join(
126
      _usableVisualStudioDetails[_installationPathKey] as String,
127 128 129 130 131 132 133
      'VC',
      'Auxiliary',
      'Build',
      'vcvars64.bat',
    );
  }

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

137 138 139 140 141 142
  /// 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.
143 144
  String get _vswherePath => _fileSystem.path.join(
    _platform.environment['PROGRAMFILES(X86)'],
145 146 147 148 149
    'Microsoft Visual Studio',
    'Installer',
    'vswhere.exe',
  );

150
  /// Components for use with vswhere requirements.
151 152 153
  ///
  /// Maps from component IDs to description in the installer UI.
  /// See https://docs.microsoft.com/en-us/visualstudio/install/workload-and-component-ids
154
  Map<String, String> _requiredComponents([int majorVersion]) {
155 156
    // The description of the C++ toolchain required by the template. The
    // component name is significantly different in different versions.
157 158 159 160 161 162 163 164 165
    // When a new major version of VS is supported, its toochain 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';
166
    }
167 168 169 170 171
    // 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.
172
    cppToolchainDescription += '\n   - If there are multiple build tool versions available, install the latest';
173 174 175 176 177 178 179 180 181 182 183
    return <String, String>{
      // The MSBuild tool and related command-line toolchain.
      'Microsoft.Component.MSBuild': 'MSBuild',
      // The C++ toolchain required by the template.
      'Microsoft.VisualStudio.Component.VC.Tools.x86.x64': cppToolchainDescription,
      // The Windows SDK version used by the template.
      'Microsoft.VisualStudio.Component.Windows10SDK.17763':
          'Windows 10 SDK (10.0.17763.0)',
    };
  }

184 185 186 187 188 189 190 191 192
  /// The minimum supported major version.
  static const int _minimumSupportedVersion = 16;  // '16' is VS 2019.

  /// vswhere argument to specificy the minimum version.
  static const String _vswhereMinVersionArgument = '-version';

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

193 194 195 196 197 198 199 200 201 202 203
  // 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';

204 205 206 207 208
  /// Keys for the status of the installation.
  static const String _isCompleteKey = 'isComplete';
  static const String _isLaunchableKey = 'isLaunchable';
  static const String _isRebootRequiredKey = 'isRebootRequired';

209 210 211
  /// The 'catalog' entry containing more details.
  static const String _catalogKey = 'catalog';

212 213 214
  /// The key for a pre-release version.
  static const String _isPrereleaseKey = 'isPrerelease';

215 216 217 218 219 220 221
  /// The user-friendly version.
  ///
  /// This key is under the 'catalog' entry.
  static const String _catalogDisplayVersionKey = 'productDisplayVersion';

  /// Returns the details dictionary for the newest version of Visual Studio
  /// that includes all of [requiredComponents], if there is one.
222 223
  Map<String, dynamic> _visualStudioDetails(
      {Iterable<String> requiredComponents, List<String> additionalArguments}) {
224 225 226 227
    final List<String> requirementArguments = requiredComponents == null
        ? <String>[]
        : <String>['-requires', ...requiredComponents];
    try {
228
      final List<String> defaultArguments = <String>[
229 230 231
        '-format', 'json',
        '-utf8',
        '-latest',
232
      ];
233
      final RunResult whereResult = _processUtils.runSync(<String>[
234 235 236
        _vswherePath,
        ...defaultArguments,
        ...?additionalArguments,
237 238 239
        ...?requirementArguments,
      ]);
      if (whereResult.exitCode == 0) {
240
        final List<Map<String, dynamic>> installations =
241
            (json.decode(whereResult.stdout) as List<dynamic>).cast<Map<String, dynamic>>();
242 243 244 245 246 247 248 249
        if (installations.isNotEmpty) {
          return installations[0];
        }
      }
    } on ArgumentError {
      // Thrown if vswhere doesnt' exist; ignore and return null below.
    } on ProcessException {
      // Ignored, return null below.
250 251
    } on FormatException {
      // may be thrown if invalid JSON is returned.
252 253 254 255
    }
    return null;
  }

256
  /// Checks if the given installation has issues that the user must resolve.
257 258 259
  ///
  /// Returns false if the required information is missing since older versions
  /// of Visual Studio might not include them.
260 261
  bool installationHasIssues(Map<String, dynamic>installationDetails) {
    assert(installationDetails != null);
262
    if (installationDetails[_isCompleteKey] != null && !(installationDetails[_isCompleteKey] as bool)) {
263 264 265
      return true;
    }

266
    if (installationDetails[_isLaunchableKey] != null && !(installationDetails[_isLaunchableKey] as bool)) {
267 268 269
      return true;
    }

270
    if (installationDetails[_isRebootRequiredKey] != null && installationDetails[_isRebootRequiredKey] as bool) {
271 272
      return true;
    }
273

274
    return false;
275 276
  }

277
  /// Returns the details dictionary for the latest version of Visual Studio
278 279
  /// that has all required components and is a supported version, or {} if
  /// there is no such installation.
280 281 282
  ///
  /// 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.
283 284
  Map<String, dynamic> _cachedUsableVisualStudioDetails;
  Map<String, dynamic> get _usableVisualStudioDetails {
285 286 287
    if (_cachedUsableVisualStudioDetails != null) {
      return _cachedUsableVisualStudioDetails;
    }
288 289 290 291 292 293 294
    final List<String> minimumVersionArguments = <String>[
      _vswhereMinVersionArgument,
      _minimumSupportedVersion.toString(),
    ];
    Map<String, dynamic> visualStudioDetails = _visualStudioDetails(
        requiredComponents: _requiredComponents(_minimumSupportedVersion).keys,
        additionalArguments: minimumVersionArguments);
295
    // If a stable version is not found, try searching for a pre-release version.
296
    visualStudioDetails ??= _visualStudioDetails(
297 298
        requiredComponents: _requiredComponents(_minimumSupportedVersion).keys,
        additionalArguments: <String>[...minimumVersionArguments, _vswherePrereleaseArgument]);
299 300 301 302 303 304

    if (visualStudioDetails != null) {
      if (installationHasIssues(visualStudioDetails)) {
        _cachedAnyVisualStudioDetails = visualStudioDetails;
      } else {
        _cachedUsableVisualStudioDetails = visualStudioDetails;
305 306
      }
    }
307
    _cachedUsableVisualStudioDetails ??= <String, dynamic>{};
308 309 310 311
    return _cachedUsableVisualStudioDetails;
  }

  /// Returns the details dictionary of the latest version of Visual Studio,
312 313
  /// regardless of components and version, or {} if no such installation is
  /// found.
314
  ///
315 316 317
  /// 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.
318 319
  Map<String, dynamic> _cachedAnyVisualStudioDetails;
  Map<String, dynamic> get _anyVisualStudioDetails {
320 321
    // Search for all types of installations.
    _cachedAnyVisualStudioDetails ??= _visualStudioDetails(
322
        additionalArguments: <String>[_vswherePrereleaseArgument, '-all']);
323 324
    // Add a sentinel empty value to avoid querying vswhere again.
    _cachedAnyVisualStudioDetails ??= <String, dynamic>{};
325 326 327 328
    return _cachedAnyVisualStudioDetails;
  }

  /// Returns the details dictionary of the best available version of Visual
329 330 331
  /// Studio.
  ///
  /// If there's a version that has all the required components, that
332
  /// will be returned, otherwise returns the latest installed version (if any).
333
  Map<String, dynamic> get _bestVisualStudioDetails {
334
    if (_usableVisualStudioDetails.isNotEmpty) {
335 336 337 338 339
      return _usableVisualStudioDetails;
    }
    return _anyVisualStudioDetails;
  }
}