diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs index 74f7168c0e1c..a22f8059a51a 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs @@ -35,6 +35,7 @@ public void ItBuildsDependencyContextsFromProjectLockFiles( SingleProjectInfo mainProject = SingleProjectInfo.Create( "/usr/Path", mainProjectName, + ".dll", mainProjectVersion, satelliteAssemblies ?? new ITaskItem[] { }); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs b/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs index 7be83715d5ad..a8fe76c5e667 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs @@ -20,9 +20,11 @@ public class DependencyContextBuilder private readonly VersionFolderPathResolver _versionFolderPathResolver; private readonly SingleProjectInfo _mainProjectInfo; private readonly ProjectContext _projectContext; + private IEnumerable _frameworkReferences; private Dictionary _referenceProjectInfos; private IEnumerable _privateAssetPackageIds; private CompilationOptions _compilationOptions; + private string _referenceAssembliesPath; public DependencyContextBuilder(SingleProjectInfo mainProjectInfo, ProjectContext projectContext) { @@ -33,6 +35,14 @@ public DependencyContextBuilder(SingleProjectInfo mainProjectInfo, ProjectContex _versionFolderPathResolver = new VersionFolderPathResolver(path: null); } + public DependencyContextBuilder WithFrameworkReferences(IEnumerable frameworkReferences) + { + // note: Framework libraries only export compile-time stuff + // since they assume the runtime library is present already + _frameworkReferences = frameworkReferences; + return this; + } + public DependencyContextBuilder WithReferenceProjectInfos(Dictionary referenceProjectInfos) { _referenceProjectInfos = referenceProjectInfos; @@ -51,6 +61,12 @@ public DependencyContextBuilder WithCompilationOptions(CompilationOptions compil return this; } + public DependencyContextBuilder WithReferenceAssembliesPath(string referenceAssembliesPath) + { + _referenceAssembliesPath = EnsureTrailingSlash(referenceAssembliesPath); + return this; + } + public DependencyContext Build() { bool includeCompilationLibraries = _compilationOptions != null; @@ -88,6 +104,7 @@ public DependencyContext Build() dependencyLookup); compilationLibraries = new[] { projectCompilationLibrary } + .Concat(GetFrameworkLibraries()) .Concat(GetLibraries(compilationExports, libraryLookup, dependencyLookup, runtime: false).Cast()); } else @@ -372,6 +389,34 @@ private IEnumerable GetCompileTimeAssemblies(LockFileTargetLibrary targe } } + private IEnumerable GetFrameworkLibraries() + { + return _frameworkReferences + ?.Select(r => new CompilationLibrary( + type: "referenceassembly", + name: r.Name, + version: r.Version, + hash: string.Empty, + assemblies: new[] { ResolveFrameworkReferencePath(r.FullPath) }, + dependencies: Enumerable.Empty(), + serviceable: false)) + ?? + Enumerable.Empty(); + } + + private string ResolveFrameworkReferencePath(string fullPath) + { + // If resolved path is under ReferenceAssembliesPath store it as a relative to it + // if not, save only assembly name and try to find it somehow later + if (!string.IsNullOrEmpty(_referenceAssembliesPath) && + fullPath?.StartsWith(_referenceAssembliesPath) == true) + { + return fullPath.Substring(_referenceAssembliesPath.Length); + } + + return Path.GetFileName(fullPath); + } + private static void EnsureProjectInfo(SingleProjectInfo referenceProjectInfo, string libraryName) { if (referenceProjectInfo == null) @@ -400,5 +445,26 @@ private SingleProjectInfo GetProjectInfo(LockFileLibrary library) return referenceProjectInfo; } + + private static string EnsureTrailingSlash(string path) + { + return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar); + } + + private static string EnsureTrailingCharacter(string path, char trailingCharacter) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // if the path is empty, we want to return the original string instead of a single trailing character. + if (path.Length == 0 || path[path.Length - 1] == trailingCharacter) + { + return path; + } + + return path + trailingCharacter; + } } } \ No newline at end of file diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs b/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs new file mode 100644 index 000000000000..aff4183608da --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs @@ -0,0 +1,52 @@ +// 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; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.Extensions.DependencyModel.Resolution; + +namespace Microsoft.NET.Build.Tasks +{ + internal static class FrameworkReferenceResolver + { + public static string GetDefaultReferenceAssembliesPath() + { + // Allow setting the reference assemblies path via an environment variable + var referenceAssembliesPath = DotNetReferenceAssembliesPathResolver.Resolve(); + + if (!string.IsNullOrEmpty(referenceAssembliesPath)) + { + return referenceAssembliesPath; + } + + if (RuntimeEnvironment.OperatingSystemPlatform != Platform.Windows) + { + // There is no reference assemblies path outside of windows + // The environment variable can be used to specify one + return null; + } + + // References assemblies are in %ProgramFiles(x86)% on + // 64 bit machines + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); + + if (string.IsNullOrEmpty(programFiles)) + { + // On 32 bit machines they are in %ProgramFiles% + programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + } + + if (string.IsNullOrEmpty(programFiles)) + { + // Reference assemblies aren't installed + return null; + } + + return Path.Combine( + programFiles, + "Reference Assemblies", "Microsoft", "Framework"); + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 03332724f6ad..88c5681e18e2 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -36,6 +36,9 @@ public class GenerateDepsFile : TaskBase [Required] public string AssemblyName { get; set; } + [Required] + public string AssemblyExtension { get; set; } + [Required] public string AssemblyVersion { get; set; } @@ -43,7 +46,7 @@ public class GenerateDepsFile : TaskBase public ITaskItem[] AssemblySatelliteAssemblies { get; set; } [Required] - public ITaskItem[] ProjectReferencePaths { get; set; } + public ITaskItem[] ReferencePaths { get; set; } [Required] public ITaskItem[] ProjectReferenceSatellitePaths { get; set; } @@ -57,9 +60,18 @@ protected override void ExecuteCore() LockFile lockFile = new LockFileCache(BuildEngine4).GetLockFile(AssetsFilePath); CompilationOptions compilationOptions = CompilationOptionsConverter.ConvertFrom(CompilerOptions); - SingleProjectInfo mainProject = SingleProjectInfo.Create(ProjectPath, AssemblyName, AssemblyVersion, AssemblySatelliteAssemblies); - Dictionary referenceProjects = SingleProjectInfo.CreateFromProjectReferences( - ProjectReferencePaths, + SingleProjectInfo mainProject = SingleProjectInfo.Create( + ProjectPath, + AssemblyName, + AssemblyExtension, + AssemblyVersion, + AssemblySatelliteAssemblies); + + IEnumerable frameworkReferences = + ReferenceInfo.CreateFrameworkReferenceInfos(ReferencePaths); + + Dictionary referenceProjects = SingleProjectInfo.CreateProjectReferenceInfos( + ReferencePaths, ProjectReferenceSatellitePaths); IEnumerable privateAssets = PackageReferenceConverter.GetPackageIds(PrivateAssetsPackageReferences); @@ -70,9 +82,11 @@ protected override void ExecuteCore() PlatformLibraryName); DependencyContext dependencyContext = new DependencyContextBuilder(mainProject, projectContext) + .WithFrameworkReferences(frameworkReferences) .WithReferenceProjectInfos(referenceProjects) .WithPrivateAssets(privateAssets) .WithCompilationOptions(compilationOptions) + .WithReferenceAssembliesPath(FrameworkReferenceResolver.GetDefaultReferenceAssembliesPath()) .Build(); var writer = new DependencyContextWriter(); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj b/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj index f5756cbdac39..3fcdebf61cac 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj @@ -36,7 +36,9 @@ + + diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ReferenceInfo.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ReferenceInfo.cs new file mode 100644 index 000000000000..27fbf26c7eae --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ReferenceInfo.cs @@ -0,0 +1,42 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; + +namespace Microsoft.NET.Build.Tasks +{ + public class ReferenceInfo + { + public string Name { get; } + public string Version { get; } + public string FullPath { get; } + + private ReferenceInfo(string name, string version, string fullPath) + { + Name = name; + Version = version; + FullPath = fullPath; + } + + public static IEnumerable CreateFrameworkReferenceInfos(IEnumerable referencePaths) + { + IEnumerable frameworkReferencePaths = referencePaths + .Where(r => r.GetBooleanMetadata("FrameworkFile") == true); + + List frameworkReferences = new List(); + foreach (ITaskItem frameworkReferencePath in frameworkReferencePaths) + { + string fullPath = frameworkReferencePath.ItemSpec; + string name = Path.GetFileNameWithoutExtension(fullPath); + string version = frameworkReferencePath.GetMetadata("Version"); + + frameworkReferences.Add(new ReferenceInfo(name, version, fullPath)); + } + + return frameworkReferences; + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/SingleProjectInfo.cs b/src/Tasks/Microsoft.NET.Build.Tasks/SingleProjectInfo.cs index c9ff4f4b9f1c..033823947786 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/SingleProjectInfo.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/SingleProjectInfo.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Collections.Generic; using Microsoft.Build.Framework; @@ -30,7 +31,7 @@ private SingleProjectInfo(string projectPath, string name, string version, strin _resourceAssemblies = resourceAssemblies; } - public static SingleProjectInfo Create(string projectPath, string name, string version, ITaskItem[] satelliteAssemblies) + public static SingleProjectInfo Create(string projectPath, string name, string fileExtension, string version, ITaskItem[] satelliteAssemblies) { List resourceAssemblies = new List(); @@ -42,15 +43,19 @@ public static SingleProjectInfo Create(string projectPath, string name, string v resourceAssemblies.Add(new ResourceAssemblyInfo(culture, relativePath)); } - return new SingleProjectInfo(projectPath, name, version, $"{name}.dll", resourceAssemblies); + string outputName = name + fileExtension; + return new SingleProjectInfo(projectPath, name, version, outputName, resourceAssemblies); } - public static Dictionary CreateFromProjectReferences( - ITaskItem[] projectReferencePaths, - ITaskItem[] projectReferenceSatellitePaths) + public static Dictionary CreateProjectReferenceInfos( + IEnumerable referencePaths, + IEnumerable projectReferenceSatellitePaths) { Dictionary projectReferences = new Dictionary(); + IEnumerable projectReferencePaths = referencePaths + .Where(r => string.Equals(r.GetMetadata("ReferenceSourceTarget"), "ProjectReference", StringComparison.OrdinalIgnoreCase)); + foreach (ITaskItem projectReferencePath in projectReferencePaths) { string sourceProjectFile = projectReferencePath.GetMetadata("MSBuildSourceProjectFile"); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets index ef9a3cbb8291..9114502e05ee 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets @@ -437,9 +437,10 @@ Copyright (c) .NET Foundation. All rights reserved. DepsFilePath="$(PublishDepsFilePath)" TargetFramework="$(TargetFrameworkMoniker)" AssemblyName="$(AssemblyName)" + AssemblyExtension="$(TargetExt)" AssemblyVersion="$(Version)" AssemblySatelliteAssemblies="@(IntermediateSatelliteAssembliesWithTargetPath)" - ProjectReferencePaths="@(ReferencePath->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))" + ReferencePaths="@(ReferencePath)" ProjectReferenceSatellitePaths="@(ReferenceSatellitePaths)" RuntimeIdentifier="$(RuntimeIdentifier)" PlatformLibraryName="$(MicrosoftNETPlatformLibrary)" diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets index fba56c74cae4..87b3163ac600 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets @@ -79,9 +79,10 @@ Copyright (c) .NET Foundation. All rights reserved. DepsFilePath="$(ProjectDepsFilePath)" TargetFramework="$(TargetFrameworkMoniker)" AssemblyName="$(AssemblyName)" + AssemblyExtension="$(TargetExt)" AssemblyVersion="$(Version)" AssemblySatelliteAssemblies="@(IntermediateSatelliteAssembliesWithTargetPath)" - ProjectReferencePaths="@(ReferencePath->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference'))" + ReferencePaths="@(ReferencePath)" ProjectReferenceSatellitePaths="@(ReferenceSatellitePaths)" RuntimeIdentifier="$(RuntimeIdentifier)" PlatformLibraryName="$(MicrosoftNETPlatformLibrary)" diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPreserveCompilationContext.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPreserveCompilationContext.cs index 4e2fe76e93c7..fbf6e05135fc 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPreserveCompilationContext.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPreserveCompilationContext.cs @@ -81,7 +81,7 @@ public void It_publishes_the_project_with_a_refs_folder_and_correct_deps_file() dependencyContext.CompilationOptions.EmitEntryPoint.Should().Be(true); dependencyContext.CompilationOptions.DebugType.Should().Be("portable"); - dependencyContext.CompileLibraries.Count.Should().Be(targetFramework == "net46" ? 51 : 116); + dependencyContext.CompileLibraries.Count.Should().Be(targetFramework == "net46" ? 53 : 116); // Ensure P2P references are specified correctly var testLibrary = dependencyContext @@ -90,6 +90,22 @@ public void It_publishes_the_project_with_a_refs_folder_and_correct_deps_file() testLibrary.Assemblies.Count.Should().Be(1); testLibrary.Assemblies[0].Should().Be("TestLibrary.dll"); + + // Ensure framework references are specified correctly + if (targetFramework == "net46") + { + var mscorlibLibrary = dependencyContext + .CompileLibraries + .FirstOrDefault(l => string.Equals(l.Name, "mscorlib", StringComparison.OrdinalIgnoreCase)); + mscorlibLibrary.Assemblies.Count.Should().Be(1); + mscorlibLibrary.Assemblies[0].Should().Be(".NETFramework/v4.6/mscorlib.dll"); + + var systemCoreLibrary = dependencyContext + .CompileLibraries + .FirstOrDefault(l => string.Equals(l.Name, "system.core", StringComparison.OrdinalIgnoreCase)); + systemCoreLibrary.Assemblies.Count.Should().Be(1); + systemCoreLibrary.Assemblies[0].Should().Be(".NETFramework/v4.6/System.Core.dll"); + } } } }