diff --git a/TestAssets/TestProjects/AppWithTransitiveProjectRefs/AuxLibrary/AuxLibrary.csproj b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/AuxLibrary/AuxLibrary.csproj
new file mode 100644
index 000000000000..547065f1c5f5
--- /dev/null
+++ b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/AuxLibrary/AuxLibrary.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Library
+ netstandard1.4
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TestAssets/TestProjects/AppWithTransitiveProjectRefs/AuxLibrary/Helper.cs b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/AuxLibrary/Helper.cs
new file mode 100644
index 000000000000..361b9a6c197d
--- /dev/null
+++ b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/AuxLibrary/Helper.cs
@@ -0,0 +1,13 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace AuxLibrary
+{
+ public static class Helper
+ {
+ public static string GetMessage()
+ {
+ return "This string came from AuxLibrary!";
+ }
+ }
+}
diff --git a/TestAssets/TestProjects/AppWithTransitiveProjectRefs/MainLibrary/Helper.cs b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/MainLibrary/Helper.cs
new file mode 100644
index 000000000000..6db7745e638b
--- /dev/null
+++ b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/MainLibrary/Helper.cs
@@ -0,0 +1,13 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace MainLibrary
+{
+ public static class Helper
+ {
+ public static string GetMessage()
+ {
+ return "This string came from MainLibrary!";
+ }
+ }
+}
diff --git a/TestAssets/TestProjects/AppWithTransitiveProjectRefs/MainLibrary/MainLibrary.csproj b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/MainLibrary/MainLibrary.csproj
new file mode 100644
index 000000000000..93b7085b020e
--- /dev/null
+++ b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/MainLibrary/MainLibrary.csproj
@@ -0,0 +1,16 @@
+
+
+ Library
+ netstandard1.4
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TestAssets/TestProjects/AppWithTransitiveProjectRefs/TestApp/Program.cs b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/TestApp/Program.cs
new file mode 100644
index 000000000000..207df350813f
--- /dev/null
+++ b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/TestApp/Program.cs
@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace TestApp
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ Console.WriteLine("TestApp --depends on--> MainLibrary --depends on--> AuxLibrary");
+ Console.WriteLine(MainLibrary.Helper.GetMessage());
+ Console.WriteLine(AuxLibrary.Helper.GetMessage());
+ }
+ }
+}
diff --git a/TestAssets/TestProjects/AppWithTransitiveProjectRefs/TestApp/TestApp.csproj b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/TestApp/TestApp.csproj
new file mode 100644
index 000000000000..42fab9f54f2a
--- /dev/null
+++ b/TestAssets/TestProjects/AppWithTransitiveProjectRefs/TestApp/TestApp.csproj
@@ -0,0 +1,16 @@
+
+
+ Exe
+ netcoreapp1.0
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolvePackageDependenciesTask.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolvePackageDependenciesTask.cs
index ce35caacd587..0cfe35e26c3d 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolvePackageDependenciesTask.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolvePackageDependenciesTask.cs
@@ -693,6 +693,76 @@ public void ItUsesResolvedPackageVersionFromSameTarget()
.First().Should().Be("Dep.Lib.Chi/4.1.0");
}
+ [Fact]
+ public void ItMarksTransitiveProjectReferences()
+ {
+ // --------------------------------------------------------------------------
+ // Given the following layout, only ProjC and ProjE are transitive references
+ // (ProjB and ProjD are direct references, and ProjF is declared private in ProjC):
+ //
+ // TestProject (i.e. current project assets file)
+ // -> ProjB
+ // -> ProjC
+ // -> ProjD
+ // -> ProjE
+ // -> ProjF (PrivateAssets=Compile)
+ // -> ProjD
+ // --------------------------------------------------------------------------
+
+ var target = CreateTarget(".NETCoreApp,Version=v1.0",
+
+ CreateTargetLibrary("ProjB/1.0.0", "project",
+ dependencies: new string[] { "\"ProjC\": \"1.0.0\"" }),
+
+ CreateTargetLibrary("ProjC/1.0.0", "project",
+ dependencies: new string[] {
+ "\"ProjD\": \"1.0.0\"", "\"ProjE\": \"1.0.0\"", "\"ProjF\": \"1.0.0\""
+ }),
+
+ CreateTargetLibrary("ProjD/1.0.0", "project"),
+
+ CreateTargetLibrary("ProjE/1.0.0", "project"),
+
+ CreateTargetLibrary("ProjF/1.0.0", "project",
+ compile: new string[] { CreateFileItem("bin/Debug/_._") })
+ );
+
+ var libraries = new string[]
+ {
+ "ProjB", "ProjC", "ProjD", "ProjE", "ProjF"
+ }
+ .Select(
+ proj => CreateProjectLibrary($"{proj}/1.0.0",
+ path: $"../{proj}/{proj}.csproj",
+ msbuildProject: $"../{proj}/{proj}.csproj"))
+ .ToArray();
+
+ string lockFileContent = CreateLockFileSnippet(
+ targets: new string[] { target },
+ libraries: libraries,
+ projectFileDependencyGroups: new string[]
+ {
+ CreateProjectFileDependencyGroup(".NETCoreApp,Version=v1.0", "ProjB", "ProjD")
+ }
+ );
+
+ var task = GetExecutedTaskFromContents(lockFileContent, out var lockFile);
+
+ task.PackageDependencies.Count().Should().Be(6);
+
+ var transitivePkgs = task.PackageDependencies
+ .Where(t => t.GetMetadata(MetadataKeys.TransitiveProjectReference) == "true");
+ transitivePkgs.Count().Should().Be(2);
+ transitivePkgs.Select(t => t.ItemSpec)
+ .Should().Contain(new string[] { "ProjC/1.0.0", "ProjE/1.0.0" });
+
+ var others = task.PackageDependencies.Except(transitivePkgs);
+ others.Count().Should().Be(4);
+ others.Where(t => t.ItemSpec == "ProjB/1.0.0").Count().Should().Be(1);
+ others.Where(t => t.ItemSpec == "ProjD/1.0.0").Count().Should().Be(2);
+ others.Where(t => t.ItemSpec == "ProjF/1.0.0").Count().Should().Be(1);
+ }
+
private ResolvePackageDependencies GetExecutedTaskFromPrefix(string lockFilePrefix)
{
LockFile lockFile;
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/MetadataKeys.cs b/src/Tasks/Microsoft.NET.Build.Tasks/MetadataKeys.cs
index a4c7151a0a41..54948d300233 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/MetadataKeys.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/MetadataKeys.cs
@@ -27,5 +27,6 @@ public static class MetadataKeys
// Tags
public const string Analyzer = "Analyzer";
public const string AnalyzerLanguage = "AnalyzerLanguage";
+ public const string TransitiveProjectReference = "TransitiveProjectReference";
}
}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageDependencies.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageDependencies.cs
index ca45a3e410b1..a7ed9e177278 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageDependencies.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageDependencies.cs
@@ -18,6 +18,7 @@ namespace Microsoft.NET.Build.Tasks
///
public sealed class ResolvePackageDependencies : TaskBase
{
+ private const string ProjectTypeKey = "project";
private readonly Dictionary _fileTypes = new Dictionary(StringComparer.OrdinalIgnoreCase);
private readonly HashSet _projectFileDependencies = new HashSet(StringComparer.OrdinalIgnoreCase);
private IPackageResolver _packageResolver;
@@ -288,6 +289,12 @@ private void GetPackageAndFileDependencies(LockFileTarget target)
var resolvedPackageVersions = target.Libraries
.ToDictionary(pkg => pkg.Name, pkg => pkg.Version.ToNormalizedString(), StringComparer.OrdinalIgnoreCase);
+ var transitiveProjectRefs = new HashSet(
+ target.Libraries
+ .Where(lib => IsTransitiveProjectReference(lib))
+ .Select(pkg => pkg.Name),
+ StringComparer.OrdinalIgnoreCase);
+
TaskItem item;
foreach (var package in target.Libraries)
{
@@ -303,17 +310,25 @@ private void GetPackageAndFileDependencies(LockFileTarget target)
}
// get sub package dependencies
- GetPackageDependencies(package, target.Name, resolvedPackageVersions);
+ GetPackageDependencies(package, target.Name, resolvedPackageVersions, transitiveProjectRefs);
// get file dependencies on this package
GetFileDependencies(package, target.Name);
}
}
+ // A package is a TransitiveProjectReference if it is a project, is not directly referenced,
+ // and does not contain a placeholder compile time assembly
+ private bool IsTransitiveProjectReference(LockFileTargetLibrary package) =>
+ string.Equals(package.Type, ProjectTypeKey, StringComparison.OrdinalIgnoreCase) &&
+ !_projectFileDependencies.Contains(package.Name) &&
+ package.CompileTimeAssemblies.FirstOrDefault(f => NuGetUtils.IsPlaceholderFile(f.Path)) == null;
+
private void GetPackageDependencies(
LockFileTargetLibrary package,
string targetName,
- Dictionary resolvedPackageVersions)
+ Dictionary resolvedPackageVersions,
+ HashSet transitiveProjectRefs)
{
string packageId = $"{package.Name}/{package.Version.ToNormalizedString()}";
TaskItem item;
@@ -332,6 +347,11 @@ private void GetPackageDependencies(
item.SetMetadata(MetadataKeys.ParentTarget, targetName); // Foreign Key
item.SetMetadata(MetadataKeys.ParentPackage, packageId); // Foreign Key
+ if (transitiveProjectRefs.Contains(deps.Id))
+ {
+ item.SetMetadata(MetadataKeys.TransitiveProjectReference, "true");
+ }
+
_packageDependencies.Add(item);
}
}
@@ -339,7 +359,6 @@ private void GetPackageDependencies(
private void GetFileDependencies(LockFileTargetLibrary package, string targetName)
{
string packageId = $"{package.Name}/{package.Version.ToNormalizedString()}";
- TaskItem item;
// for each type of file group
foreach (var fileGroup in (FileGroup[])Enum.GetValues(typeof(FileGroup)))
@@ -356,7 +375,7 @@ private void GetFileDependencies(LockFileTargetLibrary package, string targetNam
}
var fileKey = $"{packageId}/{filePath}";
- item = new TaskItem(fileKey);
+ var item = new TaskItem(fileKey);
item.SetMetadata(MetadataKeys.FileGroup, fileGroup.ToString());
item.SetMetadata(MetadataKeys.ParentTarget, targetName); // Foreign Key
item.SetMetadata(MetadataKeys.ParentPackage, packageId); // Foreign Key
@@ -401,7 +420,7 @@ private void SaveFileKeyType(string fileKey, FileGroup fileGroup)
private string ResolvePackagePath(LockFileLibrary package)
{
- if (package.Type == "project")
+ if (string.Equals(package.Type, ProjectTypeKey, StringComparison.OrdinalIgnoreCase))
{
var relativeMSBuildProjectPath = package.MSBuildProject;
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets
index 738a14d33688..11e6a4ecf9bd 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets
@@ -21,10 +21,6 @@ Copyright (c) .NET Foundation. All rights reserved.
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
-
-
- true
@@ -104,7 +100,9 @@ Copyright (c) .NET Foundation. All rights reserved.
============================================================
ResolvePackageDependenciesForBuild
- Populate items for build...
+ Populate items for build. This is triggered before target
+ "AssignProjectConfiguration" to ensure ProjectReference items
+ are populated before ResolveProjectReferences is run.
============================================================
-->
@@ -112,10 +110,12 @@ Copyright (c) .NET Foundation. All rights reserved.
ResolveLockFileReferences;
ResolveLockFileAnalyzers;
ResolveLockFileCopyLocalProjectDeps;
+ IncludeTransitiveProjectReferences;
+
+
+ <_ActiveTFMPackageDependencies Include="@(PackageDependencies->WithMetadataValue('ParentTarget', '$(_NugetTargetMonikerAndRID)'))" />
+
+
+
@@ -290,6 +298,39 @@ Copyright (c) .NET Foundation. All rights reserved.
+
+
+
+
+ <_TransitiveProjectDependencies Include="@(_ActiveTFMPackageDependencies->WithMetadataValue('TransitiveProjectReference', 'true'))" />
+
+
+ <__TransitiveProjectDefinitions Include="@(PackageDefinitions)" Exclude="@(_TransitiveProjectDependencies)" />
+ <_TransitiveProjectDefinitions Include="@(PackageDefinitions)" Exclude="@(__TransitiveProjectDefinitions)" />
+
+ <_TransitiveProjectReferences Include="%(_TransitiveProjectDefinitions.Path)">
+ %(_TransitiveProjectDefinitions.ResolvedPath)
+
+
+
+
+
+
+
+
+
+
+
MainLibrary --depends on--> AuxLibrary
+ // (TestApp transitively depends on AuxLibrary)
+
+ var testAsset = _testAssetsManager
+ .CopyTestAsset("AppWithTransitiveProjectRefs")
+ .WithSource();
+
+ testAsset.Restore("TestApp");
+ testAsset.Restore("MainLibrary");
+ testAsset.Restore("AuxLibrary");
+
+ VerifyAppBuilds(testAsset);
+ }
+
+ void VerifyAppBuilds(TestAsset testAsset)
+ {
+ var appProjectDirectory = Path.Combine(testAsset.TestRoot, "TestApp");
+
+ var buildCommand = new BuildCommand(Stage0MSBuild, appProjectDirectory);
+ var outputDirectory = buildCommand.GetOutputDirectory("netcoreapp1.0");
+
+ buildCommand
+ .Execute()
+ .Should()
+ .Pass();
+
+ outputDirectory.Should().OnlyHaveFiles(new[] {
+ "TestApp.dll",
+ "TestApp.pdb",
+ "TestApp.deps.json",
+ "TestApp.runtimeconfig.json",
+ "TestApp.runtimeconfig.dev.json",
+ "MainLibrary.dll",
+ "MainLibrary.pdb",
+ "AuxLibrary.dll",
+ "AuxLibrary.pdb",
+ });
+
+ Command.Create(RepoInfo.DotNetHostPath, new[] { Path.Combine(outputDirectory.FullName, "TestApp.dll") })
+ .CaptureStdOut()
+ .Execute()
+ .Should()
+ .Pass()
+ .And
+ .HaveStdOutContaining("This string came from MainLibrary!")
+ .And
+ .HaveStdOutContaining("This string came from AuxLibrary!");
+ }
+
+ [Fact]
+ public void The_clean_target_removes_all_files_from_the_output_folder()
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ var testAsset = _testAssetsManager
+ .CopyTestAsset("AppWithTransitiveProjectRefs")
+ .WithSource()
+ .Restore("TestApp");
+
+ var appProjectDirectory = Path.Combine(testAsset.TestRoot, "TestApp");
+
+ var buildCommand = new BuildCommand(Stage0MSBuild, appProjectDirectory);
+
+ buildCommand
+ .Execute()
+ .Should()
+ .Pass();
+
+ var outputDirectory = buildCommand.GetOutputDirectory("netcoreapp1.0");
+
+ outputDirectory.Should().OnlyHaveFiles(new[] {
+ "TestApp.dll",
+ "TestApp.pdb",
+ "TestApp.deps.json",
+ "TestApp.runtimeconfig.dev.json",
+ "TestApp.runtimeconfig.json",
+ "MainLibrary.dll",
+ "MainLibrary.pdb",
+ "AuxLibrary.dll",
+ "AuxLibrary.pdb"
+ });
+
+ var cleanCommand = Stage0MSBuild.CreateCommandForTarget("Clean", buildCommand.FullPathProjectFile);
+
+ cleanCommand
+ .Execute()
+ .Should()
+ .Pass();
+
+ outputDirectory.Should().OnlyHaveFiles(Array.Empty());
+ }
+ }
+}