diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs index 5a849a3042e7..272bdf095551 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs @@ -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; @@ -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 { { "CopyLocal", "false" }, @@ -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 + { + [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 + { + [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 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 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 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 + { + [MetadataKeys.DestinationSubDirectory] = "direct-ref/", + }) + ]; + IEnumerable directReferences = ReferenceInfo.CreateDirectReferenceInfos( + directReferenceTaskItems, + [], + lockFileLookup: lockFileLookup, + i => true, + includeProjectsNotInAssetsFile: true); + + MockTaskItem[] dependencyReferenceTaskItems = + [ + new MockTaskItem("DependencyReference.dll", new Dictionary + { + [MetadataKeys.DestinationSubDirectory] = "dependency-ref/", + }) + ]; + IEnumerable 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}"); + } + } } } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/simple.dependencies.deps.json b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/simple.dependencies.deps.json index 29254fdefd0a..c49ffbdc4e8a 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/simple.dependencies.deps.json +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/simple.dependencies.deps.json @@ -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" + } } } } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs b/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs index 5b4cecbcfd88..10e60d0e3958 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs @@ -263,7 +263,7 @@ public DependencyContextBuilder WithPackagesThatWereFiltered(Dictionary 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 + * 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 */ @@ -483,8 +483,8 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null) runtimeSignature: string.Empty, _isPortable); - // Compute the runtime fallback graph - // + // Compute the runtime fallback graph + // // If the input RuntimeGraph is empty, or we're not compiling // for a specific RID, then an runtime fallback graph is empty // @@ -623,7 +623,7 @@ private IEnumerable GetRuntimePackLibraries() }); } - private ModifiableRuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, string[] userRuntimeAssemblies) + private ModifiableRuntimeLibrary GetRuntimeLibrary(DependencyLibrary library, (string Path, string DestinationSubPath)[] userRuntimeAssemblies) { GetCommonLibraryProperties(library, out string hash, @@ -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])); @@ -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); @@ -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 = resolvedFile.PathInPackage ?? resolvedFile.DestinationSubPath; + 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); } } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 8b89525c9fd1..91217161737d 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -85,7 +85,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; } = []; public bool IsSelfContained { get; set; } @@ -154,7 +154,7 @@ private void WriteDepsFile(string depsFilePath) AssemblyVersion, AssemblySatelliteAssemblies); - var userRuntimeAssemblySet = new HashSet(UserRuntimeAssemblies ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase); + var userRuntimeAssemblySet = new HashSet(UserRuntimeAssemblies is not null ? UserRuntimeAssemblies.Select(i => i.ItemSpec) : Enumerable.Empty(), StringComparer.OrdinalIgnoreCase); Func isUserRuntimeAssembly = item => userRuntimeAssemblySet.Contains(item.ItemSpec); IEnumerable referenceAssemblyInfos = @@ -251,7 +251,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)) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ReferenceInfo.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ReferenceInfo.cs index 1e09b638dda0..61afc34304e1 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ReferenceInfo.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ReferenceInfo.cs @@ -14,6 +14,7 @@ internal class ReferenceInfo public string Version { get; } public string FullPath { get; } public string FileName => Path.GetFileName(FullPath); + public string DestinationSubPath { get; } public string PackageName { get; } public string PackageVersion { get; } @@ -26,7 +27,7 @@ public IEnumerable ResourceAssemblies } private ReferenceInfo(string name, string version, string fullPath, - string packageName, string packageVersion, string pathInPackage) + string packageName, string packageVersion, string pathInPackage, string destinationSubPath = null) { Name = name; Version = version; @@ -34,6 +35,7 @@ private ReferenceInfo(string name, string version, string fullPath, PackageName = packageName; PackageVersion = packageVersion; PathInPackage = pathInPackage; + DestinationSubPath = destinationSubPath; _resourceAssemblies = new List(); } @@ -163,8 +165,11 @@ internal static ReferenceInfo CreateReferenceInfo(ITaskItem referencePath) var pathInPackage = referencePath.GetMetadata(MetadataKeys.PathInPackage); + var destinationSubDirectory = referencePath.GetMetadata(MetadataKeys.DestinationSubDirectory); + string destinationSubPath = string.IsNullOrEmpty(destinationSubDirectory) ? null : Path.Combine(destinationSubDirectory, Path.GetFileName(fullPath)); + return new ReferenceInfo(name, version, fullPath, - packageName, packageVersion, pathInPackage); + packageName, packageVersion, pathInPackage, destinationSubPath); } private static string GetVersion(ITaskItem referencePath) diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs index 8bd78958485f..8daf14997a91 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs @@ -4,7 +4,7 @@ #nullable disable using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyModel; using Microsoft.NET.Build.Tasks; using Newtonsoft.Json.Linq; @@ -1131,5 +1131,267 @@ static void Main(string[] args) .Execute() .Should().HaveStdOut(expectedOutput); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void It_includes_local_path_for_package_dependencies(bool useCustomSubdirectory) + { + string targetFramework = ToolsetInfo.CurrentTargetFramework; + + // Create a test project that references the package + TestProject testProject = new() + { + Name = "TestProjWithPackageDependencies", + TargetFrameworks = targetFramework, + IsExe = true + }; + + string culture = "fr"; + testProject.PackageReferences.Add(new TestPackageReference("Newtonsoft.Json", ToolsetInfo.GetNewtonsoftJsonPackageVersion())); + testProject.PackageReferences.Add(new TestPackageReference("Libuv", "1.10.0")); + testProject.PackageReferences.Add(new TestPackageReference($"Humanizer.Core.{culture}", "2.14.1")); + testProject.AdditionalProperties["RestorePackagesPath"] = @"$(MSBuildProjectDirectory)\packages"; + + // Add source that uses all the packages + testProject.SourceFiles[$"{testProject.Name}.cs"] = + $$"""" + using System; + using System.Runtime.InteropServices; + using Humanizer; + class Program + { + private static void UseNewtonsoftJson() + { + var jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject("{}"); + Console.WriteLine($"Used Newtonsoft.Json - deserialized: {jsonObject}"); + } + private static void UseHumanizer() + { + string humanized = DateTime.Now.AddDays(-1).Humanize(culture: new System.Globalization.CultureInfo("{{culture}}")); + Console.WriteLine($"Used Humanizer.Core.{{culture}} - yesterday humanized: {humanized}"); + } + private static void UseLibuv() + { + uint libuvVersion = uv_version(); + Console.WriteLine($"Used Libuv - version: {libuvVersion}"); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + static extern uint uv_version(); + } + static void Main(string[] args) + { + try + { + UseNewtonsoftJson(); + } + catch (Exception ex) { Console.Error.WriteLine($"Failure using Newtonsoft.Json: {ex}"); } + + try + { + UseHumanizer(); + } + catch (Exception ex) { Console.Error.WriteLine($"Failure using Humanizer.Core.{{culture}}: {ex}"); } + + try + { + UseLibuv(); + } + catch (Exception ex) { Console.Error.WriteLine($"Failure using Libuv: {ex}"); } + } + } + """"; + + string subdirectory = string.Empty; + if (useCustomSubdirectory) + { + // Put the resolved package assets in a subdirectory + subdirectory = "pkg-ref/"; + testProject.ProjectChanges.Add(xml => + { + xml.Root.Add(XElement.Parse( + $""" + + + + + + + + """)); + }); + } + + var buildCommand = new BuildCommand(_testAssetsManager.CreateTestProject(testProject, identifier: $"{nameof(useCustomSubdirectory)}={useCustomSubdirectory}")); + buildCommand.Execute().Should().Pass(); + + // Expected package and local output paths for package assets + // Path for runtime and resource assets is a regex to match for the expected path in the .deps.json + (string Name, (string, string)[] Runtime, (string, string)[] Native, (string, string)[] Resource)[] expectedPackages = [ + ("Newtonsoft.Json", + Runtime: [ + ("lib/.*/Newtonsoft.Json.dll", $"{subdirectory}Newtonsoft.Json.dll") ], + Native: [], + Resource: []), + ("Libuv", + Runtime: [], + Native: [ + ("runtimes/linux-x64/native/libuv.so", $"{subdirectory}runtimes/linux-x64/native/libuv.so"), + ("runtimes/win-x64/native/libuv.dll", $"{subdirectory}runtimes/win-x64/native/libuv.dll") ], + Resource: []), + ($"Humanizer.Core", + Runtime: [ + ("lib/.*/Humanizer.dll", $"{subdirectory}Humanizer.dll") ], + Native: [], + Resource: []), + ($"Humanizer.Core.{culture}", + Runtime: [], + Native: [], + Resource: [ + ($"lib/.*/{culture}/Humanizer.resources.dll", $"{subdirectory}{culture}/Humanizer.resources.dll") ]) + ]; + + string outputDirectory = buildCommand.GetOutputDirectory(testProject.TargetFrameworks).FullName; + string depsFile = Path.Combine(outputDirectory, $"{testProject.Name}.deps.json"); + using (FileStream stream = File.OpenRead(depsFile)) + { + DependencyContext dependencyContext = new DependencyContextJsonReader().Read(stream); + foreach (var expected in expectedPackages) + { + // Validate package assets are in the deps file with the expected path and local paths + RuntimeLibrary lib = dependencyContext.RuntimeLibraries.FirstOrDefault(lib => lib.Name == expected.Name); + Assert.NotNull(lib); + + foreach ((string packagePath, string localPath) in expected.Runtime) + { + lib.RuntimeAssemblyGroups.Should().Contain( + g => g.RuntimeFiles.Any(f => f.LocalPath == localPath && Regex.IsMatch(f.Path, packagePath)), + $"runtime assemblies should have item with LocalPath={localPath} and Path matching {packagePath}"); + } + + foreach ((string packagePath, string localPath) in expected.Native) + { + lib.NativeLibraryGroups.Should().Contain( + g => g.RuntimeFiles.Any(f => f.LocalPath == localPath && f.Path == packagePath), + $"native libraries should have item with LocalPath={localPath} and Path={packagePath}"); + } + + foreach ((string packagePath, string localPath) in expected.Resource) + { + lib.ResourceAssemblies.Should().Contain( + a => a.LocalPath == localPath && Regex.IsMatch(a.Path, packagePath), + $"resource assemblies should have item with LocalPath={localPath} and Path matching {packagePath}"); + } + } + } + + string app = Path.Join(outputDirectory, $"{testProject.Name}.dll"); + var result = new DotnetCommand(Log, app).Execute(); + result.Should().Pass(); + + if (useCustomSubdirectory) + { + // TODO: This should not have errors once we have a runtime that supports localPath + result.Should() + .HaveStdErrContaining("System.IO.FileNotFoundException") + .And.HaveStdErrContaining("System.DllNotFoundException"); + foreach (var pkg in testProject.PackageReferences) + { + result.Should().HaveStdErrContaining($"Failure using {pkg.ID}"); + } + } + else + { + result.Should().NotHaveStdErr(); + foreach (var pkg in testProject.PackageReferences) + { + result.Should().HaveStdOutContaining($"Used {pkg.ID}"); + } + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void It_includes_local_path_for_project_references(bool useCustomSubdirectory) + { + string targetFramework = ToolsetInfo.CurrentTargetFramework; + + // Create a referenced library project + TestProject referencedProject = new() + { + Name = "ReferencedLibrary", + TargetFrameworks = targetFramework, + IsExe = false + }; + + // Create test project that references the library + TestProject testProject = new() + { + Name = "TestProjWithProjectReference", + TargetFrameworks = targetFramework, + IsExe = true + }; + testProject.ReferencedProjects.Add(referencedProject); + + string subdirectory = string.Empty; + if (useCustomSubdirectory) + { + // Put the project reference in a subdirectory + subdirectory = "proj-ref/"; + testProject.ProjectChanges.Add(xml => + { + xml.Root.Add(XElement.Parse( + $""" + + + + <_ToUpdate Include="@(ReferencePath)" Condition="'%(ReferencePath.CopyLocal)' == 'true'" /> + + + + + """)); + }); + } + + var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: $"{nameof(useCustomSubdirectory)}={useCustomSubdirectory}"); + var buildCommand = new BuildCommand(testAsset); + buildCommand.Execute().Should().Pass(); + + string outputDirectory = buildCommand.GetOutputDirectory(testProject.TargetFrameworks).FullName; + string depsFile = Path.Combine(outputDirectory, $"{testProject.Name}.deps.json"); + using (FileStream stream = File.OpenRead(depsFile)) + { + DependencyContext dependencyContext = new DependencyContextJsonReader().Read(stream); + + // Find the referenced project in runtime libraries + RuntimeLibrary lib = dependencyContext.RuntimeLibraries.FirstOrDefault(lib => lib.Name.Contains(referencedProject.Name)); + Assert.NotNull(lib); + + // Validate project reference is the deps file with the expected path and local paths + string expectedPath = $"{referencedProject.Name}.dll"; + string expectedLocalPath = useCustomSubdirectory ? $"{subdirectory}{referencedProject.Name}.dll" : null; + lib.RuntimeAssemblyGroups.Should().Contain( + g => g.RuntimeFiles.Any(f => f.LocalPath == expectedLocalPath && f.Path == expectedPath), + $"project reference should have LocalPath={expectedLocalPath} and Path={expectedPath}"); + } + + string app = Path.Join(outputDirectory, $"{testProject.Name}.dll"); + var result = new DotnetCommand(Log, app).Execute(); + + if (useCustomSubdirectory) + { + // TODO: This should pass once we have a runtime that supports localPath + result.Should().Fail() + .And.HaveStdErrContaining("System.IO.FileNotFoundException"); + } + else + { + result.Should().Pass() + .And.HaveStdOutContaining(referencedProject.Name); + } + } } }