Skip to content

Support WPF and WindowsForms-specific FrameworkReferences via profiles #3259

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

Merged
merged 3 commits into from
May 25, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ protected override void ExecuteCore()
targetingPack.SetMetadata("TargetingPackFormat", knownFrameworkReference.TargetingPackFormat);
targetingPack.SetMetadata("TargetFramework", knownFrameworkReference.TargetFramework.GetShortFolderName());
targetingPack.SetMetadata("RuntimeFrameworkName", knownFrameworkReference.RuntimeFrameworkName);
if (!string.IsNullOrEmpty(knownFrameworkReference.Profile))
{
targetingPack.SetMetadata("Profile", knownFrameworkReference.Profile);
}

string targetingPackPath = null;
if (!string.IsNullOrEmpty(TargetingPackRoot))
Expand Down Expand Up @@ -386,6 +390,9 @@ public KnownFrameworkReference(ITaskItem item)
public string RuntimePackRuntimeIdentifiers => _item.GetMetadata("RuntimePackRuntimeIdentifiers");

public string IsTrimmable => _item.GetMetadata(MetadataKeys.IsTrimmable);

public string Profile => _item.GetMetadata("Profile");

public string LegacyFrameworkPackages
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ protected override void ExecuteCore()
}
}

HashSet<string> processedRuntimePackRoots = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var runtimePack in ResolvedRuntimePacks)
{
if (!frameworkReferenceNames.Contains(runtimePack.GetMetadata(MetadataKeys.FrameworkName)))
Expand All @@ -57,6 +59,13 @@ protected override void ExecuteCore()
runtimePack.GetMetadata(MetadataKeys.RuntimeIdentifier));
}

if (!processedRuntimePackRoots.Add(runtimePackRoot))
{
// We already added assets from this runtime pack (which can happen with FrameworkReferences to different
// profiles of the same shared framework)
continue;
}

string runtimeIdentifier = runtimePack.GetMetadata(MetadataKeys.RuntimeIdentifier);

// These hard-coded paths are temporary until we have "real" runtime packs, which will likely have a flattened
Expand Down
139 changes: 134 additions & 5 deletions src/Tasks/Microsoft.NET.Build.Tasks/ResolveTargetingPackAssets.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -32,7 +33,99 @@ public class ResolveTargetingPackAssets : TaskBase
public ITaskItem[] PackageConflictOverrides { get; set; }

[Output]
public ITaskItem[] RuntimeFrameworksToRemove { get; set; }
public ITaskItem[] UsedRuntimeFrameworks { get; set; }

private Dictionary<string, List<string>> _assemblyProfiles;

public ResolveTargetingPackAssets()
{
// Hard-code assembly profiles for WindowDesktop targeting pack here until
// they are added to its FrameworkList.xml: https://github.com/dotnet/core-setup/issues/6210
_assemblyProfiles = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

var bothProfiles = new List<string>() { "WindowsForms", "WPF" };

foreach (var assemblyName in new []
{
"Accessibility",
"Microsoft.Win32.Registry",
"Microsoft.Win32.SystemEvents",
"System.CodeDom",
"System.Configuration.ConfigurationManager",
"System.Diagnostics.EventLog",
"System.DirectoryServices",
"System.IO.FileSystem.AccessControl",
"System.Media.SoundPlayer",
"System.Security.AccessControl",
"System.Security.Cryptography.Cng",
"System.Security.Cryptography.Pkcs",
"System.Security.Cryptography.ProtectedData",
"System.Security.Cryptography.Xml",
"System.Security.Permissions",
"System.Security.Principal.Windows",
"System.Threading.AccessControl",
"System.Windows.Extensions",
})
{
_assemblyProfiles[assemblyName] = bothProfiles;
}

var wpfProfile = new List<string>() { "WPF" };

foreach (var assemblyName in new []
{
"DirectWriteForwarder",
"PenImc_cor3",
"PresentationCore-CommonResources",
"PresentationCore",
"PresentationFramework-SystemCore",
"PresentationFramework-SystemData",
"PresentationFramework-SystemDrawing",
"PresentationFramework-SystemXml",
"PresentationFramework-SystemXmlLinq",
"PresentationFramework.Aero",
"PresentationFramework.Aero2",
"PresentationFramework.AeroLite",
"PresentationFramework.Classic",
"PresentationFramework",
"PresentationFramework.Luna",
"PresentationFramework.Royale",
"PresentationNative_cor3",
"PresentationUI",
"ReachFramework",
"System.Printing",
"System.Windows.Controls.Ribbon",
"System.Windows.Input.Manipulations",
"System.Windows.Presentation",
"System.Xaml",
"UIAutomationClient",
"UIAutomationClientSideProviders",
"UIAutomationProvider",
"UIAutomationTypes",
"WindowsBase",
"WPFgfx_cor3",
})
{
_assemblyProfiles[assemblyName] = wpfProfile;
}

var windowsFormsProfile = new List<string>() { "WindowsForms" };

foreach (var assemblyName in new[]
{
"System.Design",
"System.Drawing.Common",
"System.Drawing.Design",
"System.Drawing.Design.Primitives",
"System.Windows.Forms.Design",
"System.Windows.Forms.Design.Editors",
"System.Windows.Forms",
})
{
_assemblyProfiles[assemblyName] = windowsFormsProfile;
}

}

protected override void ExecuteCore()
{
Expand Down Expand Up @@ -133,16 +226,34 @@ protected override void ExecuteCore()
}
}

// Remove RuntimeFramework items for shared frameworks which weren't referenced
// Calculate which RuntimeFramework items should actually be used based on framework references
HashSet<string> frameworkReferenceNames = new HashSet<string>(FrameworkReferences.Select(fr => fr.ItemSpec), StringComparer.OrdinalIgnoreCase);
RuntimeFrameworksToRemove = RuntimeFrameworks.Where(rf => !frameworkReferenceNames.Contains(rf.GetMetadata(MetadataKeys.FrameworkName)))
.ToArray();
UsedRuntimeFrameworks = RuntimeFrameworks.Where(rf => frameworkReferenceNames.Contains(rf.GetMetadata(MetadataKeys.FrameworkName)))
.ToArray();

// Filter out duplicate references (which can happen when referencing two different profiles that overlap)
List<TaskItem> deduplicatedReferences = DeduplicateItems(referencesToAdd);
ReferencesToAdd = deduplicatedReferences.Distinct() .ToArray();
Copy link
Contributor

Choose a reason for hiding this comment

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

Distinct is still there?


ReferencesToAdd = referencesToAdd.ToArray();
PlatformManifests = platformManifests.ToArray();
PackageConflictOverrides = packageConflictOverrides.ToArray();
}

// Get distinct items based on case-insensitive ItemSpec comparison
private static List<TaskItem> DeduplicateItems(List<TaskItem> items)
{
HashSet<string> seenItemSpecs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
List<TaskItem> deduplicatedItems = new List<TaskItem>(items.Count);
foreach (var item in items)
{
if (seenItemSpecs.Add(item.ItemSpec))
{
deduplicatedItems.Add(item);
}
}
return deduplicatedItems;
}

private TaskItem CreatePackageOverride(string runtimeFrameworkName, string packageOverridesPath)
{
TaskItem packageOverride = new TaskItem(runtimeFrameworkName);
Expand Down Expand Up @@ -173,9 +284,27 @@ private void AddReferencesFromFrameworkList(string frameworkListPath, string tar
{
XDocument frameworkListDoc = XDocument.Load(frameworkListPath);

string profile = targetingPack.GetMetadata("Profile");

foreach (var fileElement in frameworkListDoc.Root.Elements("File"))
{
string assemblyName = fileElement.Attribute("AssemblyName").Value;

if (!string.IsNullOrEmpty(profile))
{
_assemblyProfiles.TryGetValue(assemblyName, out var assemblyProfiles);
if (assemblyProfiles == null)
{
// If profile was specified but this assembly doesn't belong to any profiles, don't reference it
continue;
}
if (!assemblyProfiles.Contains(profile, StringComparer.OrdinalIgnoreCase))
{
// Assembly wasn't in profile specified, so don't reference it
continue;
}
}

var dllPath = Path.Combine(targetingPackDllFolder, assemblyName + ".dll");
var referenceItem = CreateReferenceItem(dllPath, targetingPack);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ Copyright (c) .NET Foundation. All rights reserved.

</ItemGroup>

<!-- We will add Microsoft.WindowsDesktop.App.WPF and Microsoft.WindowsDesktop.App.WindowsForms to the bundled versions.
Until we do, add them here. -->
<ItemGroup>
<_WPFKnownFrameworkReference Include="@(KnownFrameworkReference)"
Condition="'%(Identity)' == 'Microsoft.WindowsDesktop.App.WPF'"/>
</ItemGroup>
<ItemGroup Condition="'@(_WPFKnownFrameworkReference)' == ''">
<_WindowsDesktopKnownFrameworkReference Include="@(KnownFrameworkReference)"
Condition="'%(Identity)' == 'Microsoft.WindowsDesktop.App'"/>

<KnownFrameworkReference Include="@(_WindowsDesktopKnownFrameworkReference->'Microsoft.WindowsDesktop.App.WindowsForms')"
Profile="WindowsForms"/>
<KnownFrameworkReference Include="@(_WindowsDesktopKnownFrameworkReference->'Microsoft.WindowsDesktop.App.WPF')"
Profile="WPF"/>
</ItemGroup>

<PropertyGroup Condition="'$(EnableTargetingPackDownload)' == ''">
<EnableTargetingPackDownload>true</EnableTargetingPackDownload>
</PropertyGroup>
Expand Down Expand Up @@ -199,7 +215,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<Output TaskParameter="PlatformManifests" ItemName="PlatformManifestsFromTargetingPacks" />
<Output TaskParameter="PackageConflictPreferredPackages" PropertyName="PackageConflictPreferredPackages" />
<Output TaskParameter="PackageConflictOverrides" ItemName="PackageConflictOverrides" />
<Output TaskParameter="RuntimeFrameworksToRemove" ItemName="_RuntimeFrameworkToRemove" />
<Output TaskParameter="UsedRuntimeFrameworks" ItemName="_UsedRuntimeFramework" />

</ResolveTargetingPackAssets>

Expand All @@ -208,7 +224,8 @@ Copyright (c) .NET Foundation. All rights reserved.
</ItemGroup>

<ItemGroup>
<RuntimeFramework Remove="@(_RuntimeFrameworkToRemove)"/>
<RuntimeFramework Remove="@(RuntimeFramework)" />
<RuntimeFramework Include="@(_UsedRuntimeFramework)" />
</ItemGroup>

<GetPackageDirectory
Expand Down
112 changes: 112 additions & 0 deletions src/Tests/Microsoft.NET.Build.Tests/GivenFrameworkReferences.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -13,6 +14,7 @@
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;

namespace Microsoft.NET.Build.Tests
{
Expand Down Expand Up @@ -591,6 +593,116 @@ public void IsTrimmableCanBeSpecifiedOnFrameworkReference()
}
}

// TODO: convert to Theory with self-contained or not
[WindowsOnlyTheory]
[InlineData(true)]
[InlineData(false)]
public void WindowsFormsFrameworkReference(bool selfContained)
{
TestFrameworkReferenceProfiles(
frameworkReferences: new [] { "Microsoft.WindowsDesktop.App.WindowsForms" },
expectedReferenceNames: new[] { "Microsoft.Win32.Registry", "System.Windows.Forms" },
notExpectedReferenceNames: new[] { "System.Windows.Presentation", "WindowsFormsIntegration" },
selfContained);
}

[WindowsOnlyTheory]
[InlineData(true)]
[InlineData(false)]
public void WPFFrameworkReference(bool selfContained)
{
TestFrameworkReferenceProfiles(
frameworkReferences: new[] { "Microsoft.WindowsDesktop.App.WPF" },
expectedReferenceNames: new[] { "Microsoft.Win32.Registry", "System.Windows.Presentation" },
notExpectedReferenceNames: new[] { "System.Windows.Forms", "WindowsFormsIntegration" },
selfContained);
}

[WindowsOnlyTheory]
[InlineData(true)]
[InlineData(false)]
public void WindowsFormAndWPFFrameworkReference(bool selfContained)
{
TestFrameworkReferenceProfiles(
frameworkReferences: new[] { "Microsoft.WindowsDesktop.App.WindowsForms", "Microsoft.WindowsDesktop.App.WPF" },
expectedReferenceNames: new[] { "Microsoft.Win32.Registry", "System.Windows.Forms", "System.Windows.Presentation" },
notExpectedReferenceNames: new[] { "WindowsFormsIntegration" },
selfContained);
}

[WindowsOnlyTheory]
[InlineData(true)]
[InlineData(false)]
public void WindowsDesktopFrameworkReference(bool selfContained)
{
TestFrameworkReferenceProfiles(
frameworkReferences: new[] { "Microsoft.WindowsDesktop.App" },
expectedReferenceNames: new[] { "Microsoft.Win32.Registry", "System.Windows.Forms",
"System.Windows.Presentation", "WindowsFormsIntegration" },
notExpectedReferenceNames: Enumerable.Empty<string>(),
selfContained);
}

private void TestFrameworkReferenceProfiles(
IEnumerable<string> frameworkReferences,
IEnumerable<string> expectedReferenceNames,
IEnumerable<string> notExpectedReferenceNames,
bool selfContained,
[CallerMemberName] string callingMethod = "")
{
var testProject = new TestProject()
{
Name = "WindowsFormsFrameworkReference",
TargetFrameworks = "netcoreapp3.0",
IsSdkProject = true,
IsExe = true
};
testProject.FrameworkReferences.AddRange(frameworkReferences);

if (selfContained)
{
testProject.RuntimeIdentifier = EnvironmentInfo.GetCompatibleRid(testProject.TargetFrameworks);
}

string identifier = selfContained ? "_selfcontained" : string.Empty;

var testAsset = _testAssetsManager.CreateTestProject(testProject, callingMethod, identifier)
.Restore(Log, testProject.Name);

string projectFolder = Path.Combine(testAsset.TestRoot, testProject.Name);

var buildCommand = new BuildCommand(Log, projectFolder);

buildCommand
.Execute()
.Should()
.Pass();

var getValuesCommand = new GetValuesCommand(Log, projectFolder, testProject.TargetFrameworks, "Reference", GetValuesCommand.ValueType.Item);

getValuesCommand.Execute().Should().Pass();

var references = getValuesCommand.GetValues();
var referenceNames = references.Select(Path.GetFileNameWithoutExtension);

referenceNames.Should().Contain(expectedReferenceNames);

if (notExpectedReferenceNames.Any())
{
referenceNames.Should().NotContain(notExpectedReferenceNames);
}

if (selfContained)
{
var outputDirectory = buildCommand.GetOutputDirectory(testProject.TargetFrameworks, runtimeIdentifier: testProject.RuntimeIdentifier);

// The output directory should have the DLLs which are not referenced at compile time but are
// still part of the shared framework.
outputDirectory.Should().HaveFiles(expectedReferenceNames.Concat(notExpectedReferenceNames)
.Select(n => n + ".dll"));
}
}

private List<string> GetRuntimeFrameworks(string runtimeConfigPath)
{
string runtimeConfigContents = File.ReadAllText(runtimeConfigPath);
Expand Down