Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

In-product acquisition updates #9552

Merged
merged 11 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/design-time-builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ CollectPackageDownloads | NuGet | Returns `PackageDo
CollectPackageReferences | NuGet | Returns `PackageReference` items. Supports package restore.
CollectResolvedCompilationReferencesDesignTime | DNPS |
CollectResolvedSDKReferencesDesignTime | SDK |
CollectSuggestedWorkloads | DNPS |
CollectSuggestedVisualStudioComponentIds | DNPS | Supports in-product acquisition (IPA).
CollectUpToDateCheckBuiltDesignTime | DNPS | Supports the Fast Up-to-date Check.
CollectUpToDateCheckInputDesignTime | DNPS | Supports the Fast Up-to-date Check.
CollectUpToDateCheckOutputDesignTime | DNPS | Supports the Fast Up-to-date Check.
Expand Down
22 changes: 17 additions & 5 deletions docs/in-product-acquisition.md
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document is more of a spec for the IPA feature and doesn't reflect what was actually built. It should probably be trimmed heavily, and only content relevant to this repo kept.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# In-product acquisition (IPA) for projects loaded by CPS
# In-product acquisition (IPA)

Visual Studio supports in-product acquisition (IPA) when it attempts to load a project that requires components or workloads that you are missing from your installation.

Expand Down Expand Up @@ -76,9 +76,9 @@ In the future, ASP.NET Core may be factored into a .NET SDK Workload that will b

### No xamarin workload installed, need xamarin dependencies

When loading a pre-net5 Xamarin project or set of projects, IPA should trigger and offer a resolution for all relevant projects to install the .NET Mobole development VS workload.
When loading a pre-net5 Xamarin project or set of projects, IPA should trigger and offer a resolution for all relevant projects to install the .NET Mobile development VS workload.

If the workload is installed but optional Components that are needed by the project or set of projects are missing (such as the HAXM eumulator Component), then IPA should trigger and offer a resolution for all reelvant projects to install the necessary components. **Note:** use of these components may not be present in MSBuild evaluation data. It may not be possible to fully handle this sub-scenario.
If the workload is installed but optional Components that are needed by the project or set of projects are missing (such as the HAXM emulator Component), then IPA should trigger and offer a resolution for all relevant projects to install the necessary components. **Note:** use of these components may not be present in MSBuild evaluation data. It may not be possible to fully handle this sub-scenario.

A `net5` Xamarin project is designated by its TFM: `net5-ios`, `net5-android`, etc. _REDACTED_ will have another designation. This could be multi-TFM. In any case, whenever the user tries to load a project or set of projects and is missing the Xamarin workload, IPA should trigger and offer a resolution for all relevant projects to install the .NET Mobile development VS workload.

Expand Down Expand Up @@ -130,7 +130,7 @@ If a `global.json` is present and it specifies an SDK version that the user does

**Note:** the SDK resolver likely needs to be involved here. For example, a user could specify `rollForward": "major"` which would most likely allow the SDK installed with VS to work with their codebase.

**Open question:** What about nested `global.json`? Is one compatible with the other (thus making their specification of the nested `global.json` unecessary)? etc.
**Open question:** What about nested `global.json`? Is one compatible with the other (thus making their specification of the nested `global.json` unnecessary)? etc.

## UX

Expand Down Expand Up @@ -172,8 +172,20 @@ TODO - this needs a mockup to be more clear

There may be N projects that need a particular resolution applied to them (e.g., 50 .NET Framework projects might need a specific targeting pack). In such a scenario, we should group each project affected by a resolution together underneath that resolution.

It is possible to have M resolutions applied to N projects, where M > N. This means that grouping projects under resolutions could lead to a seemingly unecessary UI. For example, a single project could require 10 different things. This would imply a UI where there are 10 different "resolution groups" with only a single project listed underneath it. This is probably fine since it's probably more likely that N > M.
It is possible to have M resolutions applied to N projects, where M > N. This means that grouping projects under resolutions could lead to a seemingly unnecessary UI. For example, a single project could require 10 different things. This would imply a UI where there are 10 different "resolution groups" with only a single project listed underneath it. This is probably fine since it's probably more likely that N > M.

### Global resolutions

Some resolutions may be global and not a per-project thing. For example, resolving a missing .NET SDK from a `global.json` is not a per-project action.

## Debugging IPA

To see the set of Visual Studio setup components that a project requires, add the `DiagnoseVisualStudioComponents` project capability to your project. To do this in MSBuild, add this to your project:

```xml
<ItemGroup>
<ProjectCapability Include="DiagnoseVisualStudioComponents" />
</ItemGroup>
```

This will cause Visual Studio to show the set of components the project requires in Solution Explorer, which is useful for debugging.
Original file line number Diff line number Diff line change
@@ -1,72 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE.md file in the project root for more information.

using Microsoft.VisualStudio.ProjectSystem.Properties;
using Microsoft.VisualStudio.Text;

namespace Microsoft.VisualStudio.ProjectSystem.VS.Setup;

/// <summary>
/// Immutable snapshot of the components required by a configured project.
/// Immutable snapshot of the VS setup components required by a configured project.
/// </summary>
internal sealed class ConfiguredSetupComponentSnapshot
{
public static ConfiguredSetupComponentSnapshot Empty { get; } = new();
public static ConfiguredSetupComponentSnapshot Empty { get; } = new(requiresWebComponent: false, componentIds: ImmutableStringHashSet.EmptyVisualStudioSetupComponentIds);

private readonly bool _requiresWebComponent;
private readonly string? _netCoreTargetFrameworkVersion;
private readonly ImmutableHashSet<string> _suggestedWorkloadComponentIds;

public ImmutableHashSet<string> ComponentIds { get; }

public bool IsEmpty { get; }

private ConfiguredSetupComponentSnapshot()
private ConfiguredSetupComponentSnapshot(bool requiresWebComponent, ImmutableHashSet<string> componentIds)
{
_suggestedWorkloadComponentIds = ComponentIds = ImmutableStringHashSet.EmptyVisualStudioSetupComponentIds;
IsEmpty = true;
_requiresWebComponent = requiresWebComponent;
ComponentIds = componentIds;
}

private ConfiguredSetupComponentSnapshot(bool requiresWebComponent, string? netCoreTargetFrameworkVersion, ImmutableHashSet<string> suggestedWorkloadComponentIds)

/// <summary>
/// Applies changes to the current (immutable) snapshot, producing a new snapshot.
/// </summary>
/// <returns>The updated snapshot, or the same instance if no changes were required.</returns>
public ConfiguredSetupComponentSnapshot Update(IProjectSubscriptionUpdate buildUpdate, IProjectCapabilitiesSnapshot capabilities)
{
_requiresWebComponent = requiresWebComponent;
_netCoreTargetFrameworkVersion = netCoreTargetFrameworkVersion;
_suggestedWorkloadComponentIds = suggestedWorkloadComponentIds;
ImmutableHashSet<string> componentIds;

ImmutableHashSet<string> componentIds = suggestedWorkloadComponentIds;
bool requiresWebComponent;

if (requiresWebComponent)
{
componentIds = componentIds.Add(SetupComponentReferenceData.WebComponentId);
}
ProcessCapabilities();
ProcessBuildUpdate();

if (netCoreTargetFrameworkVersion is not null && SetupComponentReferenceData.TryGetComponentIdByNetCoreTargetFrameworkVersion(netCoreTargetFrameworkVersion, out string? runtimeComponentId))
if (ReferenceEquals(componentIds, ComponentIds))
{
componentIds = componentIds.Add(runtimeComponentId);
return this;
}

ComponentIds = componentIds;
}
return new(requiresWebComponent, componentIds);

public ConfiguredSetupComponentSnapshot Update(
IProjectSubscriptionUpdate evaluationUpdate,
IProjectSubscriptionUpdate buildUpdate,
IProjectCapabilitiesSnapshot capabilities)
{
// We use bitwise | here instead of logical || to prevent short circuiting.
if (IsEmpty |
ProcessCapabilities(out bool requiresWebComponent) |
ProcessEvaluationUpdate(out string? netCoreTargetFrameworkVersion) |
ProcessBuildUpdate(out ImmutableHashSet<string> suggestedWorkloadComponentIds))
void ProcessCapabilities()
{
return new(requiresWebComponent, netCoreTargetFrameworkVersion, suggestedWorkloadComponentIds);
}

return this;
const string webComponentId = "Microsoft.VisualStudio.Component.Web";

bool ProcessCapabilities(out bool requiresWebComponent)
{
requiresWebComponent = RequiresWebComponent();
return requiresWebComponent != _requiresWebComponent;

componentIds = (requiresWebComponent, _requiresWebComponent) switch
{
(true, false) => ComponentIds.Add(webComponentId),
(false, true) => ComponentIds.Remove(webComponentId),
_ => ComponentIds
};

bool RequiresWebComponent()
{
Expand All @@ -79,62 +63,34 @@ bool RequiresWebComponent()
}
}

bool ProcessEvaluationUpdate(out string? netCoreTargetFrameworkVersion)
void ProcessBuildUpdate()
{
IProjectChangeDescription change = evaluationUpdate.ProjectChanges[ConfigurationGeneral.SchemaName];
IProjectChangeDescription change = buildUpdate.ProjectChanges[SuggestedVisualStudioComponentId.SchemaName];

if (change.Difference.ChangedProperties.Count == 0)
if (!change.Difference.AnyChanges)
{
netCoreTargetFrameworkVersion = _netCoreTargetFrameworkVersion;
return false;
return;
}

IImmutableDictionary<string, string> properties = change.After.Properties;

string? targetFrameworkIdentifier = properties.GetStringProperty(ConfigurationGeneral.TargetFrameworkIdentifierProperty);

netCoreTargetFrameworkVersion = StringComparers.FrameworkIdentifiers.Equals(targetFrameworkIdentifier, TargetFrameworkIdentifiers.NetCoreApp)
? properties.GetStringProperty(ConfigurationGeneral.TargetFrameworkVersionProperty)
: null;
return netCoreTargetFrameworkVersion != _netCoreTargetFrameworkVersion;
}

bool ProcessBuildUpdate(out ImmutableHashSet<string> suggestedWorkloadComponentIds)
{
IProjectChangeDescription change = buildUpdate.ProjectChanges[SuggestedWorkload.SchemaName];
var builder = ComponentIds.ToBuilder();

if (!change.Difference.AnyChanges)
foreach (string addedItem in change.Difference.AddedItems)
{
suggestedWorkloadComponentIds = _suggestedWorkloadComponentIds;
return false;
builder.Add(addedItem);
}

IImmutableDictionary<string, IImmutableDictionary<string, string>> suggestedWorkloads = change.After.Items;

if (suggestedWorkloads.Count == 0)
foreach (string removedItem in change.Difference.RemovedItems)
{
suggestedWorkloadComponentIds = ImmutableHashSet<string>.Empty;
return false;
builder.Remove(removedItem);
}

ImmutableHashSet<string>.Builder? componentIds = null;

foreach ((string workloadName, IImmutableDictionary<string, string> metadata) in suggestedWorkloads)
foreach ((string before, string after) in change.Difference.RenamedItems)
{
if (metadata.GetStringProperty(SuggestedWorkload.VisualStudioComponentIdsProperty) is string ids)
{
componentIds ??= ImmutableStringHashSet.EmptyVisualStudioSetupComponentIds.ToBuilder();
componentIds.AddRange(new LazyStringSplit(ids, ';').Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()));
}
else if (metadata.GetStringProperty(SuggestedWorkload.VisualStudioComponentIdProperty) is string id)
{
componentIds ??= ImmutableStringHashSet.EmptyVisualStudioSetupComponentIds.ToBuilder();
componentIds.Add(id.Trim());
}
builder.Remove(before);
builder.Add(after);
}

suggestedWorkloadComponentIds = componentIds?.ToImmutable() ?? ImmutableHashSet<string>.Empty;
return true;
componentIds = builder.ToImmutable();
}
}
}
Loading
Loading