Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using FluentAssertions.Json;
using Microsoft.Build.Framework;
using Microsoft.Extensions.DependencyModel;
using Microsoft.NET.TestFramework;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NuGet.Frameworks;
Expand Down Expand Up @@ -229,7 +230,7 @@ private static DependencyContext BuildDependencyContextFromDependenciesWithResou
[]);
string mainProjectDirectory = Path.GetDirectoryName(mainProject.ProjectPath);


ITaskItem[] referencePaths = dllReference ? references.Select(reference =>
new MockTaskItem($"/usr/Path/{reference}.dll", new Dictionary<string, string> {
{ "CopyLocal", "false" },
Expand Down Expand Up @@ -529,5 +530,186 @@ void CheckRuntimeFallbacks(string runtimeIdentifier, int fallbackCount)
CheckRuntimeFallbacks("new_os-new_arch", 1);
CheckRuntimeFallbacks("unrelated_os-unknown_arch", 0);
}

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

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

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

string packageName = "Newtonsoft.Json";
string packageVersion = "9.0.1";

// Runtime assemblies
ResolvedFile runtime = new(
"Newtonsoft.Json.dll",
destinationSubDirectory: null,
new PackageIdentity(packageName, new NuGetVersion(packageVersion)),
AssetType.Runtime,
$"lib/{ToolsetInfo.CurrentTargetFramework}/Newtonsoft.Json.dll");
ResolvedFile runtimeWithCustomSubPath = new(
"CustomSubPath.dll",
"pkg/",
new PackageIdentity(packageName, new NuGetVersion(packageVersion)),
AssetType.Runtime,
$"lib/{ToolsetInfo.CurrentTargetFramework}/CustomSubPath.dll");

// Native libraries
ResolvedFile native = new(
"nativelib.dll",
"runtimes/win-x64/native/",
new PackageIdentity(packageName, new NuGetVersion(packageVersion)),
AssetType.Native,
"runtimes/win-x64/native/nativelib.dll");
ResolvedFile nativeWithCustomSubPath = new(
"nativecustomsubpath.dll",
"pkg/runtimes/win-x64/native/",
new PackageIdentity(packageName, new NuGetVersion(packageVersion)),
AssetType.Native,
"runtimes/win-x64/native/nativecustomsubpath.dll");

// Resource assemblies
MockTaskItem resourceTaskItem = new("de/Newtonsoft.Json.resources.dll",
new Dictionary<string, string>
{
[MetadataKeys.DestinationSubDirectory] = "de/",
[MetadataKeys.AssetType] = "resources",
[MetadataKeys.NuGetPackageId] = packageName,
[MetadataKeys.NuGetPackageVersion] = packageVersion,
[MetadataKeys.PathInPackage] = $"lib/{ToolsetInfo.CurrentTargetFramework}/de/Newtonsoft.Json.resources.dll",
[MetadataKeys.Culture] = "de",
});
MockTaskItem resourceWithCustomSubPathTaskItem = new("fr/Newtonsoft.Json.resources.dll",
new Dictionary<string, string>
{
[MetadataKeys.DestinationSubDirectory] = "pkg/fr/",
[MetadataKeys.AssetType] = "resources",
[MetadataKeys.NuGetPackageId] = packageName,
[MetadataKeys.NuGetPackageVersion] = packageVersion,
[MetadataKeys.PathInPackage] = $"lib/{ToolsetInfo.CurrentTargetFramework}/fr/Newtonsoft.Json.resources.dll",
[MetadataKeys.Culture] = "fr",
});
ResolvedFile resource = new(resourceTaskItem, false);
ResolvedFile resourceWithCustomSubPath = new(resourceWithCustomSubPathTaskItem, false);

DependencyContext dependencyContext = new DependencyContextBuilder(mainProject, includeRuntimeFileVersions: false, runtimeGraph: null, projectContext: projectContext, libraryLookup: lockFileLookup)
.WithResolvedNuGetFiles([runtime, runtimeWithCustomSubPath, native, nativeWithCustomSubPath, resource, resourceWithCustomSubPath])
.Build();

var library = dependencyContext.RuntimeLibraries.FirstOrDefault(l => l.Name == "Newtonsoft.Json");
library.Should().NotBeNull();

// Runtime assembly
library.RuntimeAssemblyGroups.Should().HaveCount(1);
IReadOnlyList<RuntimeFile> runtimeFiles = library.RuntimeAssemblyGroups[0].RuntimeFiles;
runtimeFiles.Should().HaveCount(2);
runtimeFiles.Should().Contain(
f => f.LocalPath == runtime.DestinationSubPath && f.Path == runtime.PathInPackage,
$"runtime assemblies should have item with LocalPath={runtime.DestinationSubPath} and Path matching {runtime.PathInPackage}");
runtimeFiles.Should().Contain(
f => f.LocalPath == runtimeWithCustomSubPath.DestinationSubPath && f.Path == runtimeWithCustomSubPath.PathInPackage,
$"runtime assemblies should have item with LocalPath={runtimeWithCustomSubPath.DestinationSubPath} and Path matching {runtimeWithCustomSubPath.PathInPackage}");

// Native library
library.NativeLibraryGroups.Should().HaveCount(1);
IReadOnlyList<RuntimeFile> nativeFiles = library.NativeLibraryGroups[0].RuntimeFiles;
nativeFiles.Should().HaveCount(2);
nativeFiles.Should().Contain(
f => f.LocalPath == native.DestinationSubPath && f.Path == native.PathInPackage,
$"native libraries should have item with LocalPath={native.PathInPackage} and Path={native.DestinationSubPath}");
nativeFiles.Should().Contain(
f => f.LocalPath == nativeWithCustomSubPath.DestinationSubPath && f.Path == nativeWithCustomSubPath.PathInPackage,
$"native libraries should have item with LocalPath={nativeWithCustomSubPath.PathInPackage} and Path={nativeWithCustomSubPath.DestinationSubPath}");

// Resource assembly
IReadOnlyList<ResourceAssembly> resourceAssemblies = library.ResourceAssemblies;
resourceAssemblies.Should().HaveCount(2);
resourceAssemblies.Should().Contain(
f => f.LocalPath == resource.DestinationSubPath && f.Path == resource.PathInPackage,
$"resource assemblies should have item with LocalPath={resource.PathInPackage} and Path={resource.DestinationSubPath}");
resourceAssemblies.Should().Contain(
f => f.LocalPath == resourceWithCustomSubPath.DestinationSubPath && f.Path == resourceWithCustomSubPath.PathInPackage,
$"resource assemblies should have item with LocalPath={resourceWithCustomSubPath.PathInPackage} and Path={resourceWithCustomSubPath.DestinationSubPath}");
}

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

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

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

MockTaskItem[] directReferenceTaskItems =
[
new MockTaskItem("DirectReference.dll", new Dictionary<string, string>
{
[MetadataKeys.DestinationSubDirectory] = "direct-ref/",
})
];
IEnumerable<ReferenceInfo> directReferences = ReferenceInfo.CreateDirectReferenceInfos(
directReferenceTaskItems,
[],
lockFileLookup: lockFileLookup,
i => true,
includeProjectsNotInAssetsFile: true);

MockTaskItem[] dependencyReferenceTaskItems =
[
new MockTaskItem("DependencyReference.dll", new Dictionary<string, string>
{
[MetadataKeys.DestinationSubDirectory] = "dependency-ref/",
})
];
IEnumerable<ReferenceInfo> dependencyReferences = ReferenceInfo.CreateDependencyReferenceInfos(
dependencyReferenceTaskItems,
[],
i => true);

DependencyContext dependencyContext = new DependencyContextBuilder(mainProject, includeRuntimeFileVersions: false, runtimeGraph: null, projectContext: projectContext, libraryLookup: lockFileLookup)
.WithDirectReferences(directReferences)
.WithDependencyReferences(dependencyReferences)
.Build();

ReferenceInfo[] expectedReferences = [.. directReferences, .. dependencyReferences];
foreach (ReferenceInfo referenceInfo in expectedReferences)
{
var lib = dependencyContext.RuntimeLibraries.FirstOrDefault(l => l.Name == referenceInfo.Name);
lib.Should().NotBeNull();
lib.RuntimeAssemblyGroups.Should().HaveCount(1);
lib.RuntimeAssemblyGroups[0].RuntimeFiles.Should().HaveCount(1);
lib.RuntimeAssemblyGroups[0].RuntimeFiles.Should().Contain(
f => f.LocalPath == referenceInfo.DestinationSubPath && f.Path == referenceInfo.FileName,
$"runtime assemblies should have item with LocalPath={referenceInfo.DestinationSubPath} and Path matching {referenceInfo.FileName}");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@
"System.Runtime.Serialization.Primitives": "4.1.1"
},
"runtime": {
"lib/netstandard1.0/Newtonsoft.Json.dll": {}
"lib/netstandard1.0/Newtonsoft.Json.dll": {
"localPath": "Newtonsoft.Json.dll"
}
}
},
"System.Collections.NonGeneric/4.0.1": {
"runtime": {
"lib/netstandard1.3/System.Collections.NonGeneric.dll": {}
"lib/netstandard1.3/System.Collections.NonGeneric.dll": {
"localPath": "System.Collections.NonGeneric.dll"
}
}
},
"System.Runtime.Serialization.Primitives/4.1.1": {
"runtime": {
"lib/netstandard1.3/System.Runtime.Serialization.Primitives.dll": {}
"lib/netstandard1.3/System.Runtime.Serialization.Primitives.dll": {
"localPath": "System.Runtime.Serialization.Primitives.dll"
}
}
}
}
Expand Down
28 changes: 13 additions & 15 deletions src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public DependencyContextBuilder WithPackagesThatWereFiltered(Dictionary<PackageI
return this;
}

public DependencyContext Build(string[] userRuntimeAssemblies = null)
public DependencyContext Build((string Path, string DestinationSubPath)[] userRuntimeAssemblies = null)
{
CalculateExcludedLibraries();

Expand Down Expand Up @@ -299,7 +299,7 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null)
name: GetReferenceLibraryName(directReference),
version: directReference.Version,
hash: string.Empty,
runtimeAssemblyGroups: [new RuntimeAssetGroup(string.Empty, [CreateRuntimeFile(directReference.FileName, directReference.FullPath)])],
runtimeAssemblyGroups: [new RuntimeAssetGroup(string.Empty, [CreateRuntimeFile(directReference.FileName, directReference.FullPath, directReference.DestinationSubPath)])],
nativeLibraryGroups: [],
resourceAssemblies: CreateResourceAssemblies(directReference.ResourceAssemblies),
dependencies: [],
Expand Down Expand Up @@ -623,7 +623,7 @@ private IEnumerable<ModifiableRuntimeLibrary> GetRuntimePackLibraries()
});
}

private ModifiableRuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, string[] userRuntimeAssemblies)
private ModifiableRuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, (string Path, string DestinationSubPath)[] userRuntimeAssemblies)
{
GetCommonLibraryProperties(library,
out string hash,
Expand All @@ -646,8 +646,10 @@ private ModifiableRuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, st
if (library.Type == "project" && !(referenceProjectInfo is UnreferencedProjectInfo))
{
var fileName = Path.GetFileNameWithoutExtension(library.Path);
var assemblyPath = userRuntimeAssemblies?.FirstOrDefault(p => Path.GetFileNameWithoutExtension(p).Equals(fileName));
var runtimeFile = !string.IsNullOrWhiteSpace(assemblyPath) && File.Exists(assemblyPath) ? CreateRuntimeFile(referenceProjectInfo.OutputName, assemblyPath) :
(string Path, string DestinationSubPath) assembly = userRuntimeAssemblies is not null
? userRuntimeAssemblies.FirstOrDefault(p => Path.GetFileNameWithoutExtension(p.Path).Equals(fileName))
: default;
var runtimeFile = !string.IsNullOrWhiteSpace(assembly.Path) && File.Exists(assembly.Path) ? CreateRuntimeFile(referenceProjectInfo.OutputName, assembly.Path, assembly.DestinationSubPath) :
!string.IsNullOrWhiteSpace(library.Path) && File.Exists(library.Path) ? CreateRuntimeFile(referenceProjectInfo.OutputName, library.Path) :
new RuntimeFile(referenceProjectInfo.OutputName, string.Empty, string.Empty);
runtimeAssemblyGroups.Add(new RuntimeAssetGroup(string.Empty, [runtimeFile]));
Expand All @@ -674,7 +676,7 @@ private ModifiableRuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, st
var resourceFiles = resolvedNuGetFiles.Where(f => f.Asset == AssetType.Resources &&
!f.IsRuntimeTarget);

resourceAssemblies.AddRange(resourceFiles.Select(f => new ResourceAssembly(f.PathInPackage, f.Culture)));
resourceAssemblies.AddRange(resourceFiles.Select(f => new ResourceAssembly(f.PathInPackage, f.Culture, f.DestinationSubPath)));

var runtimeTargets = resolvedNuGetFiles.Where(f => f.IsRuntimeTarget)
.GroupBy(f => f.RuntimeIdentifier);
Expand Down Expand Up @@ -813,25 +815,21 @@ private void GetCommonLibraryProperties(DependencyLibrary library,

private RuntimeFile CreateRuntimeFile(ResolvedFile resolvedFile)
{
string relativePath = resolvedFile.PathInPackage;
if (string.IsNullOrEmpty(relativePath))
{
relativePath = resolvedFile.DestinationSubPath;
}
return CreateRuntimeFile(relativePath, resolvedFile.SourcePath);
string relativePath = string.IsNullOrEmpty(resolvedFile.PathInPackage) ? resolvedFile.DestinationSubPath : resolvedFile.PathInPackage;
return CreateRuntimeFile(relativePath, resolvedFile.SourcePath, resolvedFile.DestinationSubPath);
}

private RuntimeFile CreateRuntimeFile(string path, string fullPath)
private RuntimeFile CreateRuntimeFile(string path, string fullPath, string localPath = null)
{
if (_includeRuntimeFileVersions)
{
string fileVersion = FileUtilities.GetFileVersion(fullPath).ToString();
string assemblyVersion = FileUtilities.TryGetAssemblyVersion(fullPath)?.ToString();
return new RuntimeFile(path, assemblyVersion, fileVersion);
return new RuntimeFile(path, assemblyVersion, fileVersion, localPath);
}
else
{
return new RuntimeFile(path, null, null);
return new RuntimeFile(path, null, null, localPath);
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public class GenerateDepsFile : TaskBase
// CopyLocal subset ot of @(ReferencePath), @(ReferenceDependencyPath)
// Used to filter out non-runtime assemblies from deps file. Only project and direct references in this
// set will be written to deps file as runtime dependencies.
public string[] UserRuntimeAssemblies { get; set; }
public ITaskItem[] UserRuntimeAssemblies { get; set; } = [];

Copy link

Copilot AI Aug 12, 2025

Choose a reason for hiding this comment

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

Changing UserRuntimeAssemblies from string[] to ITaskItem[] is a breaking change that could affect consumers of this task. Consider maintaining backward compatibility by providing an overload or conversion mechanism.

Suggested change
// Backward compatibility for string[] property
[Obsolete("Use UserRuntimeAssemblies as ITaskItem[] instead. This property is for backward compatibility.")]
public string[] UserRuntimeAssembliesAsString
{
get => UserRuntimeAssemblies?.Select(item => item.ItemSpec).ToArray();
set => UserRuntimeAssemblies = value?.Select(s => new TaskItem(s)).ToArray();
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't really know what the best thing to do here is. For usage via MSBuild targets, I think the passed strings convert to ITaskItem[] ("value1;value2" is two items with the itemspec set those values and no additional metadata).

Is external usage of this task via C# expected?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think so, this should be fine.

public bool IsSelfContained { get; set; }

Expand Down Expand Up @@ -159,7 +159,7 @@ private void WriteDepsFile(string depsFilePath)
AssemblyVersion,
AssemblySatelliteAssemblies);

var userRuntimeAssemblySet = new HashSet<string>(UserRuntimeAssemblies ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
var userRuntimeAssemblySet = new HashSet<string>(UserRuntimeAssemblies is not null ? UserRuntimeAssemblies.Select(i => i.ItemSpec) : Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
Func<ITaskItem, bool> isUserRuntimeAssembly = item => userRuntimeAssemblySet.Contains(item.ItemSpec);

IEnumerable<ReferenceInfo> referenceAssemblyInfos =
Expand Down Expand Up @@ -256,7 +256,13 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item)
.Concat(ResolvedRuntimeTargetsFiles.Select(f => new ResolvedFile(f, true)));
builder = builder.WithResolvedNuGetFiles(resolvedNuGetFiles);

DependencyContext dependencyContext = builder.Build(UserRuntimeAssemblies);
var userRuntimeAssemblies = UserRuntimeAssemblies.Select(i =>
{
string destinationSubDir = i.GetMetadata(MetadataKeys.DestinationSubDirectory);
string destinationSubPath = string.IsNullOrEmpty(destinationSubDir) ? null : Path.Combine(destinationSubDir, Path.GetFileName(i.ItemSpec));
return (i.ItemSpec, destinationSubPath);
}).ToArray();
DependencyContext dependencyContext = builder.Build(userRuntimeAssemblies);

var writer = new DependencyContextWriter();
using (var fileStream = File.Create(depsFilePath))
Expand Down
Loading
Loading