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()); + } + } +}