Skip to content

Commit

Permalink
Merge pull request #9552 from drewnoakes/ipa-roslyn-components-targets
Browse files Browse the repository at this point in the history
In-product acquisition updates
  • Loading branch information
drewnoakes authored Oct 17, 2024
2 parents 01de8ef + 8d229af commit c7bde6d
Show file tree
Hide file tree
Showing 15 changed files with 474 additions and 304 deletions.
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
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

0 comments on commit c7bde6d

Please sign in to comment.