Skip to main content

Add support to detect and install VSIX packages into VS2017

User stories

  • As a developer I can detect if VS2017 (VS15) is installed and, if needed, where it is installed.
  • As a developer I can declare required workloads that must be installed to install my VSIX package.
  • As a developer I can install my extension into multiple instances of VS2017.

Background

VS2017 deployment was redesigned from the ground up to be lighter and allow for multiple instances to be installed. The default installation - providing faster start times, syntax highlighting of many languages, and more - is evidence of this.

However, to achieve this a number of features were changed, and use of Windows Installer packages and even registry entries were drastically reduced. As such, no longer will simple registry, component, or file system searches work. The WixVSExtension will need to be updated to work with VS2017.

Proposal

A fast COM API was created to enumerate products installed by the new deployment engine. This does mean that a custom action will need to be executed to set properties. The in ability to create an instance of the COM class - when CoCreateInstance returns REGDB_E_CLASSNOTREG - means that no instances are installed in a supported manner.

Because VSIX extensions can be installed to different instances with different workloads and/or components installed, more of the process to install extensions will move to install time.

The existing VS15.wxs will be removed since it did not work with any public pre-releases of VS2017, and will be replaced by a new VS2017.wxs file. Packages using VS15.wxs will fail to link and will need to update references to VS2017.wxs.

Properties

Given how the existing VS-related properties from the WixVSExtension are used and limitations in how Windows Installer works with properties, the properties defined for VS2017 can only point to one location. For most end users this is expected to be fine. However, there is currently no concept of a "default instance". Instead, the highest constrained version (e.g. the highest 15.0 for VS2017) and the latest if multiple instances of the same version (i.e. most recently installed) will be used.

To get the root VS2017_ROOT_FOLDER, the custom action will enumerate instances searching for one or more of the following product IDs.

  • Microsoft.VisualStudio.Product.Enterprise
  • Microsoft.VisualStudio.Product.Professional
  • Microsoft.VisualStudio.Product.Community

The custom action will be scheduled before AppSearch so that existing DirectorySearch elements can be used to find sub-folders for properties:

  • VS2017_EXTENSIONS_DIR
  • VS2017_PROJECTTEMPLATES_DIR
  • VS2017_SCHEMAS_DIR
  • VS2017_ITEMTEPLATES_DIR
  • VS2017_BOOTSTRAPPER_PACKAGE_FOLDER
  • VS2017DEVENV

With the VS2017DEVENV property set, the following custom action definitions should continue to work as defined.

  • VS2017Setup
  • VS2017InstallVSTemplates

Project system properties will be supported by detecting the related component IDs.

PropertyComponent ID
VS2017_IDE_VC_PROJECTSYSTEM_INSTALLEDMicrosoft.VisualStudio.Component.VC.CoreIde
VS2017_IDE_VCSHARP_PROJECTSYSTEM_INSTALLEDMicrosoft.VisualStudio.Component.Roslyn.LanguageServices
VS2017_IDE_VB_PROJECTSYSTEM_INSTALLEDMicrosoft.VisualStudio.Component.Roslyn.LanguageServices
VS2017_IDE_FSHARP_PROJECTSYSTEM_INSTALLEDMicrosoft.VisualStudio.Component.FSharp
VS2017_IDE_MODELING_PROJECTSYSTEM_INSTALLEDMicrosoft.VisualStudio.PackageGroup.DslRuntime
VS2017_IDE_VSTS_TESTSYSTEM_INSTALLEDMicrosoft.VisualStudio.Component.TestTools.Core
VS2017_IDE_VWD_PROJECTSYSTEM_INSTALLEDMicrosoft.VisualStudio.Component.Web

Note there is no separate component for the modeling tools (DSL). This ID could change in the future.

A new property VS_INSTANCES_ID will be supported and represents a comma-delimited list of instance IDs. End users via the command line or BAs could discover instance IDs ahead of time to determine this list and pass this property to the MSI. Because this is an explicit action, it is assumed that the client is taking responsibility for selection and this value will override all ActionProperty properties as described below. This will be defined at build time as a secure custom property, but will not be defined in the Property table specifically.

Because the ActionProperty properties will also be secure custom properties, clients can also pass different instance IDs to each property individually if required and instead of VS_INSTANCE_IDS. While supported, this is not assumed to be atypical.

Elements

The VsixPackage table will be updated to support authoring of an optional Id attribute. If not specified, this will be generated using the GenerateIdentifier method accepting "vsx" as the prefix, and the component ID and file ID as input. This Id attribute is used to relate the package to workload and component package IDs on which the package depends. This information could also be harvested.

The existing Target and TargetVersion attributes can still be specified but are ignored when the /instanceIds switch is passed to VSIXInstaller.exe. This will maintain backward compatibility for the custom actions with older versions of VS if VS2017 or newer is not installed.

The existing Vital attribute will also be stored in a new VsixPackage table as described below to keep track of which packages require instances. Packages that are not vital will not cause the installation to err if no compatible instances are found. This is analogous to existing behavior with older versions of VS if a package failed to install, which could be because it is not compatible with an edition and was not authored appropriately.

A new, optional ActionProperty attribute is added. If not specified, this will be the uppercase version of the Id attribute which is a valid property ID. This property will be added to the secure custom properties list because it can be passed to the client and must be passed through to the server.

The VsixPackage element will also accept one or more child elements named VsixDependency like in the following example.

<vs:VsixPackage Id="Optional ID; otherwise, generated" ActionProperty="Optional property; otherwise, uppercase Id" ...>
<vs:Requires Id="Workload Or Component ID" Version="Optional version or version range" Chip="Optional chip" Language="Optional language" Branch="Optional branch" />
</vs:VsixPackage>

The Id attribute is required, but all other attributes are optional. The Version can be a single version number like "1.2.3.4" or a range using the same syntax as VSIX and NuGet packages like "[1.0,2.0)". Because the attributes are treated as opaque values, the Language attribute should be in the format language_Locale as with web standards.

Tables

Until now the VsixPackage element was compiled directly into custom actions; however, to fail the installation if a compatible instance is not found for a package where VsixPackage/@Vital is yes (default), we need to remember that. To be robust, this will be tracked in an Attributes column. This will allow for other binary attributes to be stored without table schema changes. All rows will be processed without considering the parent component state because the custom action will enumerate instances only once as an optimization.

The VsixPackage table would have the following schema.

NameTypeDescription
VsixPackages72The row primary key. This is generated if not authored as described above.
Component_s72Required parent component ID. Not currently used but could be, and table schemas can only grow. Could be useful for static analysis.
PackageIds255Required package ID used for reporting.
ActionPropertys72Required per-package property will be set to comma-delimited list of compatible instance IDs.
AttributesI4Optional attributes including Vital (1).

If any VsixDependency elements are authored, the related VsixPackage/@Id is associated with the specified package references in a table with the schema as follows. The column values are treated as opaque values processed by the custom action directly and compared as documented.

NameTypeDescription
VsixPackage_s72Foreign key reference to related VsixPackage row.
Ids255Required workload or component ID.
VersionS50Optional version or version range.
ChipS10Optional chip such as "x86" or "x64".
LanguageS10Optional language identifier like "en-US" or "ja-JP".
BranchS20Optional branch identifier. Not typical but supported by the engine.

Custom actions

Two sets of custom actions are used to discover compatible instances, and to execute VSIXInstaller.exe to install extensions into those instances of VS2017 or newer, or versions and editions of previous VS releases.

Immediate custom action

An immediate custom action will both set properties and enumerate instances, if required. Initially, this custom action will only set VS2017_ROOT_FOLDER as described above but should be updated as future versions are released. It will run before AppSearch so that derivative properties can use the root folder to search for subdirectories. The related DirectorySearch and FileSearch elements will be authored into VSIX2017.wxs similar to previous releases' authoring.

It will also process the VsixPackage table - if it exists - and find compatible instances if the VS_INSTANCE_IDS property is not set using related rows from the VsixDependency table that relates a VsixPackage row to a collection of workload and component IDs.

The custom action will call ISetupConfiguration2::EnumAllInstances to query all instances regardless of the ISetupInstance::GetState result. While enumerating, the version is considered. The version is parsed using ISetupHelper::ParseVersion, where ISetupHelper is QI'd from ISetupConfiguration. For VS2017, the highest version of 15.0 will be kept as well as the latest installed from ISetupInstance::GetInstallDate. The latest install of the highest version wil be used to set the VS2017_ROOT_FOLDER (for the highest 15.0 version), and the VS2017_VSIX_INSTALLER_PATH using combining the result of ISetupInstance2::GetEnginePath of "VSIXInstaller.exe". VsixPackage.wxs will be updated to add another SetProperty element to set VS_VSIX_INSTALLER_PATH to VS2017_VSIX_INSTALLER_PATH, if set.

For each instance, the CA will check for each workload and component ID in the VsixDependency table and keep track of which instances contain each ID. After enumeration is complete, instances that contain all workload and component IDs for a package are set in the associated row's ActionProperty as a comma-separated list of instance IDs. The deferred custom action below uses this property in its condition whether to run, as well as the command line as described above. If the package is attributed as Vital and its ActionProperty is not set, the custom action will log a custom error message and exit with an error. This is necessary because the deferred custom actions are conditioned on that property as described below and would not run if the property is not set.

Error message

The WixVSExtension will define an error message in src\libs\wcautil\custommsierrors.h in the range 27101 to 27200.

IDMessageDescription
msierrVSNoInstancesNo compatible instances found for package "[2]".[2] is the PackageId column value.

Deferred custom action

The existing custom actions generated at compile time will be used. These are generated per-package in a declarative manner. To support installing extensions to specified or discovered compatible instances, associated VsixPackage table rows' ActionProperty will be used in the command line like so: "{/instanceIds[ActionProperty]}". This means if the ActionProperty is defined, the /instanceIds command line switch is passed to VSIXInstaller.exe which takes precedence over the existing /skuName and /skuVersion switches. The ActionProperty will also be used in the custom action's Condition column in the InstallExecuteSequence table so that if no instances were found, the custom action will not run.

The scheduling of these custom actions will not change.

Considerations

The following sections are from discussions within Microsoft for issues we have considered but seek feedback from the community. These could be done incrementally after the base feature work proposed above.

Harvest workload and component IDs from VSIX packages

Because installing VSIX packages without required workloads and instances will install them to all installed instances (at least for compatible package IDs authored into the package manifest itself). This will install any dependent workloads and/or components. So it's best to harvest these IDs from VSIX packages if they are available.

VSIX packages are based on the Open Packaging Convention (OPC), which use a ZIP container format. In the root of the package is a manifest.json file that contains a dependencies object property. The keys are dependent package IDs that contain all the same values as the columns in the new VsixDependency table and can be harvested directly, like in the following example.

{
"dependencies": {
"a": "",
"b": "[1.0,2.0)",
"c": {
"version": "[1.0,2.0)",
"chip": "x86",
"language": "en-US",
"branch": "develop"
}
}
}

All three examples are supported, where package "a" specifies only an ID, "b" declares a dependent version range, and "c" declares all supported package reference attributes. These would map to rows like in the following example.

<vs:VsixPackage PackageId="Example">
<vs:Requires Id="a" />
<vs:Requires Id="b" Version="[1.0,2.0)" />
<vs:Requires Id="c" Version="[1.0,2.0)" Chip="x86" Language="en-US" Branch="develop" />
</vs:VsixPackage>

Nested MSI sessions

When an MSI invokes VSIXInstaller.exe targeting VS2017, if any workloads or components are installed that also install MSIs, the session will fail. Microsoft is considering a change to VSIXInstaller.exe to accept a command line parameter when run from within an MSI when VSIXInstaller.exe was found by the CAs documented herein, and when passed will determine if any MSIs will be installed / repaired and exit immediately with an error code we would map back to to a custom message like the following example.

IDMessageDescription
msierrNestedInstallCannot install "[2]" because it will trigger an unsupported install.[2] is the path to the VSIX package.

An installer could also use the value of field 2 to ShellExecute it or allow the user to do it. VSIXInstaller.exe will prompt into which versions, editions, and instances of all installed Visual Studio products to install.

Additionally, if bundle authoring were expanded to allow custom package authoring elements, we could create compiler support for a VsixPackage element that compiles to an ExePackage but would drop an EXE (perhaps embedded in WixVSExtension.dll if a single-file extension is required) that calls the query APIs to find VSIXInstaller.exe and forwards the command line. This would be a simple shortcut for package developers and would move invocation of the VSIXInstaller.exe out of MSIs without additional support required in Burn.

MSI messages

To support external UI handlers for single MSI installs, the custom actions could send messages for all instances - mapping to the VSIX_INSTANCE_IDS property - and/or each individual instance using package rows' ActionProperty properties. The InstanceId, DisplayName, Nickname (optional property), and InstallationPath properties would be packaged into message data to be displayed by the external UI handler.

The DisplayName would be selected by passing the ProductLanguage MSI property to ISetupConfiguration::GetDisplayName as the lcid parameter. If the language is not supported, the English (US) (1033) DisplayName is returned.

BA functions

Because it's most likely a Burn bundle will install multiple MSIs, it would only make sense to define a BA function that enumerates instances and can provide the UI with the InstanceId, DisplayName, Nickname (optional property), and InstallationPath so that the UI can present display information to the user and pass back selected the InstanceIds via either the global VS_INSTANCE_IDS or individual ActionProperty properties.

The DisplayName would be selected by passing in the locale used by the BA to ISetupConfiguration::GetDisplayName as the lcid parameter. If the language is not supported, the English (US) (1033) DisplayName is returned.

See Also