Skip to content

Include only PackageReferences included at runtime in deps.json #46218

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 20 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from 11 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 @@ -195,6 +195,15 @@ public void ItDoesntCreateReferenceAssembliesWhenNoCompilationOptions()
.NotContain(d => d.Name == "System.Collections.NonGeneric.Reference");
}

[Fact]
public void ItDoesntCreateKeepUnneededRuntimeReferences()
{
DependencyContext dependencyContext = BuildDependencyContextWithReferenceAssemblies(useCompilationOptions: false);

dependencyContext.RuntimeLibraries.Count.Should().Be(1);
dependencyContext.RuntimeLibraries[0].Name.Should().Be("simple.dependencies"); // This is the entrypoint
}

[Fact]
public void ItHandlesReferenceAndPackageReferenceNameCollisions()
{
Expand All @@ -211,6 +220,80 @@ public void ItHandlesReferenceAndPackageReferenceNameCollisions()
.Contain(c => c.Name == "System.Collections.NonGeneric.Reference.Reference" && c.Type == "referenceassembly");
}

[Fact]
public void ItHandlesReferencesThatCannotBeRemovedProperly()
{
string mainProjectName = "simple.dependencies";
LockFile lockFile = TestLockFiles.GetLockFile(mainProjectName);

SingleProjectInfo mainProject = SingleProjectInfo.Create(
"/usr/Path",
mainProjectName,
".dll",
"1.0.0",
[]);

ITaskItem[] referencePaths = new ITaskItem[]
{
new MockTaskItem(
"/usr/Path/System.NotConflicting.dll",
new Dictionary<string, string>
{
{ "CopyLocal", "false" },
{ "FusionName", "System.NotConflicting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" },
{ "Version", "" },
}),
new MockTaskItem(
"/usr/Path/System.Collections.NonGeneric.dll",
new Dictionary<string, string>
{
{ "CopyLocal", "false" },
{ "FusionName", "System.Collections.NonGeneric, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" },
{ "Version", "" },
}),
new MockTaskItem(
"/usr/Path/System.Collections.NonGeneric.Reference.dll",
new Dictionary<string, string>
{
{ "CopyLocal", "false" },
{ "FusionName", "System.Collections.NonGeneric.Reference, Version=4.0.0.0, Culture=neutral, PublicKeyToken=null" },
{ "Version", "" },
}),
};

ProjectContext projectContext = lockFile.CreateProjectContext(
FrameworkConstants.CommonFrameworks.NetCoreApp10.GetShortFolderName(),
runtime: null,
platformLibraryName: Constants.DefaultPlatformLibrary,
runtimeFrameworks: null,
isSelfContained: false);

CompilationOptions compilationOptions = CreateCompilationOptions();

IEnumerable<ReferenceInfo> directReferences =
ReferenceInfo.CreateDirectReferenceInfos(
referencePaths,
[],
lockFileLookup: new LockFileLookup(lockFile),
i => true,
true);

DependencyContext dependencyContext = new DependencyContextBuilder(mainProject, includeRuntimeFileVersions: false, runtimeGraph: null, projectContext: projectContext, libraryLookup: new LockFileLookup(lockFile))
.WithReferenceAssemblies(ReferenceInfo.CreateReferenceInfos(referencePaths))
.WithCompilationOptions(compilationOptions)
.WithDirectReferences(directReferences)
.Build();

// ensure the DependencyContext can be written out successfully - it has no duplicate dependency names
Save(dependencyContext);

dependencyContext.RuntimeLibraries.Count.Should().Be(4);
dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("simple.dependencies"));
dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.NotConflicting"));
dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.Collections.NonGeneric.Reference"));
dependencyContext.RuntimeLibraries.Should().Contain(x => x.Name.Equals("System.Collections.NonGeneric.Reference.Reference"));
}

private DependencyContext BuildDependencyContextWithReferenceAssemblies(bool useCompilationOptions)
{
string mainProjectName = "simple.dependencies";
Expand Down
115 changes: 93 additions & 22 deletions src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null)
{
CalculateExcludedLibraries();

List<RuntimeLibrary> runtimeLibraries = new();
List<ModifiableRuntimeLibrary> runtimeLibraries = new();

if (_includeMainProjectInDepsFile)
{
Expand All @@ -285,23 +285,83 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null)

foreach (var directReference in directAndDependencyReferences)
{
var runtimeLibrary = new RuntimeLibrary(
var runtimeLibrary = new ModifiableRuntimeLibrary(new RuntimeLibrary(
type: "reference",
name: GetReferenceLibraryName(directReference),
version: directReference.Version,
hash: string.Empty,
runtimeAssemblyGroups: new[] { new RuntimeAssetGroup(string.Empty, new[] { CreateRuntimeFile(directReference.FileName, directReference.FullPath) }) },
nativeLibraryGroups: new RuntimeAssetGroup[] { },
runtimeAssemblyGroups: [new RuntimeAssetGroup(string.Empty, [CreateRuntimeFile(directReference.FileName, directReference.FullPath)])],
nativeLibraryGroups: [],
resourceAssemblies: CreateResourceAssemblies(directReference.ResourceAssemblies),
dependencies: Enumerable.Empty<Dependency>(),
dependencies: [],
path: null,
hashPath: null,
runtimeStoreManifestName: null,
serviceable: false);
serviceable: false));

runtimeLibraries.Add(runtimeLibrary);
}

/*
* We now need to modify runtimeLibraries to eliminate those that don't have any runtime assets. We follow the following steps:
* 0. Construct a reverse dependencies list: all runtimeLibraries that depend on this one
* 1. If runtimeAssemblyGroups, nativeLibraryGroups, dependencies, and resourceAssemblies are all empty, remove this runtimeLibrary as well as any dependencies on it.
* 2. Add all runtimeLibraries to a list of to-be-processed libraries called libraryCandidatesForRemoval
* 3. libraryCandidatesForRemoval.Pop() --> if there are no runtimeAssemblyGroups, nativeLibraryGroups, or resourceAssemblies, and either dependencies is empty or all
* dependencies have something else that depends on them, remove it (and from libraryCandidatesForRemoval), adding everything that depends on this to
* libraryCandidatesForRemoval if it isn't already there
* Repeat 3 until libraryCandidatesForRemoval is empty
*/
var references = runtimeLibraries.ToDictionary(lib => lib.Library.Name, lib => lib);
foreach (var reference in runtimeLibraries)
Copy link
Member

Choose a reason for hiding this comment

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

"Reference" seems to mean something slightly different than "library". Is there a reason not to name these libraries and library instead of references and reference?

{
foreach (var dependency in reference.Library.Dependencies)
{
if (references.TryGetValue(dependency.Name, out var dep))
{
dep.Dependents.Add(reference.Library.Name);
}
}
}

var unprocessedReferences = runtimeLibraries.ToHashSet();
HashSet<ModifiableRuntimeLibrary> temp = new();
Copy link
Member

Choose a reason for hiding this comment

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

What's the reason for the separate temp HashSet? Originally I thought it was because you couldn't add to the unprocessedReference set while iterating over it, but now I see that you're not actually iterating over it, just popping an item out each time through the loop. Given that, could you remove the separate temp list?

If not, it would probably be a good idea to rename temp to something more descriptive and add a comment about why it's used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. That's exactly how I'd been thinking about it, but you're right that it wasn't actually being iterated over, so there's no need for a temp HashSet. Deleted it

while (unprocessedReferences.Any())
{
var lib = unprocessedReferences.First();
unprocessedReferences.Remove(lib);

if (lib.Library.RuntimeAssemblyGroups.Count == 0 && lib.Library.NativeLibraryGroups.Count == 0 && lib.Library.ResourceAssemblies.Count == 0)
{
if (lib.Library.Dependencies.All(d => !references.TryGetValue(d.Name, out var dependency) || dependency.Dependents.Count > 1))
{
runtimeLibraries.Remove(lib);
references.Remove(lib.Library.Name);
foreach (var dependency in lib.Library.Dependencies)
{
if (references.TryGetValue(dependency.Name, out ModifiableRuntimeLibrary? value))
{
value.Dependents.Remove(lib.Library.Name);
}
}

foreach (var dependent in lib.Dependents)
{
if (references.TryGetValue(dependent, out var dep))
{
temp.Add(dep);
}
}
}
}

if (!unprocessedReferences.Any())
{
unprocessedReferences = temp;
temp = new();
}
}

List<CompilationLibrary> compilationLibraries = new();
if (IncludeCompilationLibraries)
{
Expand Down Expand Up @@ -386,7 +446,6 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null)
//
// Otherwise, it is the set of all runtimes compatible with (inheriting)
// the target runtime-identifier.

var runtimeFallbackGraph =
(_runtimeGraph == null || _runtimeIdentifier == null) ?
new RuntimeFallbacks[] { } :
Expand All @@ -399,11 +458,11 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null)
targetInfo,
_compilationOptions ?? CompilationOptions.Default,
compilationLibraries,
runtimeLibraries,
runtimeLibraries.Select(library => library.Library),
Copy link
Member

Choose a reason for hiding this comment

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

Since the library and its dependencies are actually immutable, I think you need to recreate each library after the graph walk so that any dependencies that were removed don't show up as dangling links in the deps file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not very confident about what the deps.json is supposed to look at, but it looks ok to me. This is a runtime change, which means it keeps them as compile-time dependencies. Here's an example from the test:
System.NotConflicting has both runtime can compile-time assets and looks like this:

"System.NotConflicting/4.0.0.0": {
  "runtime": {
    "System.NotConflicting.dll": {}
  },
  "compile": {
    "System.NotConflicting.dll": {}
  }
}

Meanwhile, System.NotConflicting.Reference doesn't and looks like this:

"System.NotConflicting.Reference/4.0.0.0": {
  "compile": {
    "System.NotConflicting.dll": {}
  },
  "compileOnly": true
}

(They should be more complicated if they were real.)

In other words, I think keeping the dependencies list as it was before is correct; it removes the runtime references while retaining the note that they're connected.

runtimeFallbackGraph);
}

private RuntimeLibrary GetProjectRuntimeLibrary()
private ModifiableRuntimeLibrary GetProjectRuntimeLibrary()
{
RuntimeAssetGroup[] runtimeAssemblyGroups = new[] { new RuntimeAssetGroup(string.Empty, _mainProjectInfo.OutputName) };

Expand All @@ -418,7 +477,7 @@ private RuntimeLibrary GetProjectRuntimeLibrary()
}
}

return new RuntimeLibrary(
return new ModifiableRuntimeLibrary(new RuntimeLibrary(
type: "project",
name: _mainProjectInfo.Name,
version: _mainProjectInfo.Version,
Expand All @@ -430,7 +489,7 @@ private RuntimeLibrary GetProjectRuntimeLibrary()
path: null,
hashPath: null,
runtimeStoreManifestName: GetRuntimeStoreManifestName(_mainProjectInfo.Name, _mainProjectInfo.Version),
serviceable: false);
serviceable: false));
}

private List<Dependency> GetProjectDependencies()
Expand Down Expand Up @@ -477,11 +536,11 @@ private List<Dependency> GetProjectDependencies()
return dependencies;
}

private IEnumerable<RuntimeLibrary> GetRuntimePackLibraries()
private IEnumerable<ModifiableRuntimeLibrary> GetRuntimePackLibraries()
{
if (_runtimePackAssets == null)
{
return Enumerable.Empty<RuntimeLibrary>();
return [];
}
return _runtimePackAssets.Select(runtimePack =>
{
Expand All @@ -493,20 +552,20 @@ private IEnumerable<RuntimeLibrary> GetRuntimePackLibraries()
runtimePack.Value.Where(asset => asset.AssetType == AssetType.Native)
.Select(asset => CreateRuntimeFile(asset.DestinationSubPath, asset.SourcePath)));

return new RuntimeLibrary(
return new ModifiableRuntimeLibrary(new RuntimeLibrary(
type: "runtimepack",
name: runtimePack.Key,
version: runtimePack.Value.First().PackageVersion,
hash: string.Empty,
runtimeAssemblyGroups: new[] { runtimeAssemblyGroup },
nativeLibraryGroups: new[] { nativeLibraryGroup },
resourceAssemblies: Enumerable.Empty<ResourceAssembly>(),
dependencies: Enumerable.Empty<Dependency>(),
serviceable: false);
runtimeAssemblyGroups: [runtimeAssemblyGroup],
nativeLibraryGroups: [nativeLibraryGroup],
resourceAssemblies: [],
dependencies: [],
serviceable: false));
});
}

private RuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, string[] userRuntimeAssemblies)
private ModifiableRuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, string[] userRuntimeAssemblies)
{
GetCommonLibraryProperties(library,
out string hash,
Expand Down Expand Up @@ -582,7 +641,7 @@ private RuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, string[] use

}

var runtimeLibrary = new RuntimeLibrary(
var runtimeLibrary = new ModifiableRuntimeLibrary(new RuntimeLibrary(
type: library.Type,
name: library.Name,
version: library.Version.ToString(),
Expand All @@ -594,7 +653,7 @@ private RuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, string[] use
path: path,
hashPath: hashPath,
runtimeStoreManifestName: GetRuntimeStoreManifestName(library.Name, library.Version.ToString()),
serviceable: serviceable);
serviceable: serviceable));

return runtimeLibrary;
}
Expand Down Expand Up @@ -885,5 +944,17 @@ private struct LibraryDependency
public string Name { get; set; }
public NuGetVersion MinVersion { get; set; }
}

private class ModifiableRuntimeLibrary
{
public RuntimeLibrary Library { get; set; }
public HashSet<string> Dependents { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

I think it's easy to get confused about what "dependent" means (libraries that depend on this one, ie the opposite of a dependency), so it would be good ta have a clarifying comment here.


public ModifiableRuntimeLibrary(RuntimeLibrary library)
{
this.Library = library;
this.Dependents = new();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Microsoft.NET.Build.Tasks
{
internal static class PackageReferenceConverter
{
public static IEnumerable<string> GetPackageIds(ITaskItem[] packageReferences)
public static IEnumerable<string> GetPackageIds(IEnumerable<ITaskItem> packageReferences)
{
if (packageReferences == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,34 @@ public void Method1()

}

[Theory]
[InlineData("RazorSimpleMvc22", "netcoreapp2.2", "SimpleMvc22")]
[InlineData("DesktopReferencingNetStandardLibrary", "net46", "Library")]
public void PackageReferences_with_private_assets_do_not_appear_in_deps_file(string asset, string targetFramework, string exeName)
{
var testAsset = _testAssetsManager
.CopyTestAsset(asset)
.WithSource();

var buildCommand = new BuildCommand(testAsset);
buildCommand.Execute().Should().Pass();

using (var depsJsonFileStream = File.OpenRead(Path.Combine(buildCommand.GetOutputDirectory(targetFramework).FullName, exeName + ".deps.json")))
{
var dependencyContext = new DependencyContextJsonReader().Read(depsJsonFileStream);
if (asset.Equals("DesktopReferencingNetStandardLibrary"))
{
dependencyContext.CompileLibraries.Any(l => l.Name.Equals("Library")).Should().BeTrue();
dependencyContext.RuntimeLibraries.Any(l => l.Name.Equals("Library")).Should().BeTrue();
}
else
{
dependencyContext.CompileLibraries.Any(l => l.Name.Equals("Nerdbank.GitVersioning")).Should().BeTrue();
dependencyContext.RuntimeLibraries.Any(l => l.Name.Equals("Nerdbank.GitVersioning")).Should().BeFalse();
}
}
}

[WindowsOnlyFact]
public void It_can_reference_a_netstandard2_library_and_exchange_types()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.146"/>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.6" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
</ItemGroup>
Expand Down
Loading