From b4b89c78497f0cde1f4b05e75671e66a3d1bf0e4 Mon Sep 17 00:00:00 2001 From: Livar Cunha Date: Mon, 3 Oct 2016 15:36:14 -0700 Subject: [PATCH] Switch to code to generate deps file for tools using the lock file. --- .../ProjectToolsCommandResolver.cs | 27 +- .../DependencyContextBuilder.cs | 315 ++++++++++++++++++ .../LockFileTargetExtensions.cs | 105 ++++++ .../DependencyContextBuilding/NuGetUtils.cs | 25 ++ .../ResourceAssemblyInfo.cs | 17 + .../SingleProjectInfo.cs | 27 ++ 6 files changed, 500 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/DependencyContextBuilder.cs create mode 100644 src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/LockFileTargetExtensions.cs create mode 100644 src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/NuGetUtils.cs create mode 100644 src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/ResourceAssemblyInfo.cs create mode 100644 src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/SingleProjectInfo.cs diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs index 4509ed995..bf81b1510 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs @@ -181,42 +181,37 @@ namespace Microsoft.DotNet.Cli.Utils depsPathRoot, toolLibrary.Name + FileNameSuffixes.DepsJson); - EnsureToolJsonDepsFileExists(toolLockFile, depsJsonPath); + EnsureToolJsonDepsFileExists(toolLockFile, depsJsonPath, toolLibrary); return depsJsonPath; } private void EnsureToolJsonDepsFileExists( LockFile toolLockFile, - string depsPath) + string depsPath, + LockFileTargetLibrary toolLibrary) { if (!File.Exists(depsPath)) { - GenerateDepsJsonFile(toolLockFile, depsPath); + GenerateDepsJsonFile(toolLockFile, depsPath, toolLibrary); } } // Need to unit test this, so public public void GenerateDepsJsonFile( LockFile toolLockFile, - string depsPath) + string depsPath, + LockFileTargetLibrary toolLibrary) { Reporter.Verbose.WriteLine($"Generating deps.json at: {depsPath}"); - var projectContext = new ProjectContextBuilder() - .WithLockFile(toolLockFile) - .WithTargetFramework(s_toolPackageFramework.ToString()) - .Build(); - - var exporter = projectContext.CreateExporter(Constants.DefaultConfiguration); + var singleProjectInfo = new SingleProjectInfo( + toolLibrary.Name, + toolLibrary.Version.ToFullString(), + Enumerable.Empty()); var dependencyContext = new DependencyContextBuilder() - .Build(null, - null, - exporter.GetAllExports(), - true, - s_toolPackageFramework, - string.Empty); + .Build(singleProjectInfo, null, toolLockFile, s_toolPackageFramework, null); var tempDepsFile = Path.GetTempFileName(); using (var fileStream = File.Open(tempDepsFile, FileMode.Open, FileAccess.Write)) diff --git a/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/DependencyContextBuilder.cs b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/DependencyContextBuilder.cs new file mode 100644 index 000000000..c6ee4223c --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/DependencyContextBuilder.cs @@ -0,0 +1,315 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.DependencyModel; +using NuGet.Frameworks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.ProjectModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal class DependencyContextBuilder + { + private readonly VersionFolderPathResolver _versionFolderPathResolver; + + public DependencyContextBuilder() + { + // This resolver is only used for building file names, so that base path is not required. + _versionFolderPathResolver = new VersionFolderPathResolver(path: null); + } + + public DependencyContext Build( + SingleProjectInfo mainProjectInfo, + CompilationOptions compilationOptions, + LockFile lockFile, + NuGetFramework framework, + string runtime) + { + bool includeCompilationLibraries = compilationOptions != null; + + LockFileTarget lockFileTarget = lockFile.GetTarget(framework, runtime); + + IEnumerable runtimeExports = lockFileTarget.GetRuntimeLibraries(); + IEnumerable compilationExports = + includeCompilationLibraries ? + lockFileTarget.GetCompileLibraries() : + Enumerable.Empty(); + + var dependencyLookup = compilationExports + .Concat(runtimeExports) + .Distinct() + .Select(library => new Dependency(library.Name, library.Version.ToString())) + .ToDictionary(dependency => dependency.Name, StringComparer.OrdinalIgnoreCase); + + var libraryLookup = lockFile.Libraries.ToDictionary(l => l.Name, StringComparer.OrdinalIgnoreCase); + + var runtimeSignature = GenerateRuntimeSignature(runtimeExports); + + IEnumerable runtimeLibraries = + GetLibraries(runtimeExports, libraryLookup, dependencyLookup, runtime: true).Cast(); + + IEnumerable compilationLibraries; + if (includeCompilationLibraries) + { + CompilationLibrary projectCompilationLibrary = GetProjectCompilationLibrary( + mainProjectInfo, + lockFile, + lockFileTarget, + dependencyLookup); + compilationLibraries = new[] { projectCompilationLibrary } + .Concat( + GetLibraries(compilationExports, libraryLookup, dependencyLookup, runtime: false) + .Cast()); + } + else + { + compilationLibraries = Enumerable.Empty(); + } + + return new DependencyContext( + new TargetInfo(framework.DotNetFrameworkName, runtime, runtimeSignature, lockFileTarget.IsPortable()), + compilationOptions ?? CompilationOptions.Default, + compilationLibraries, + runtimeLibraries, + new RuntimeFallbacks[] { }); + } + + private static string GenerateRuntimeSignature(IEnumerable runtimeExports) + { + var sha1 = SHA1.Create(); + var builder = new StringBuilder(); + var packages = runtimeExports + .Where(libraryExport => libraryExport.Type == "package"); + var separator = "|"; + foreach (var libraryExport in packages) + { + builder.Append(libraryExport.Name); + builder.Append(separator); + builder.Append(libraryExport.Version.ToString()); + builder.Append(separator); + } + var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())); + + builder.Clear(); + foreach (var hashByte in hash) + { + builder.AppendFormat("{0:x2}", hashByte); + } + return builder.ToString(); + } + + private List GetProjectDependencies( + LockFile lockFile, + LockFileTarget lockFileTarget, + Dictionary dependencyLookup) + { + + List dependencies = new List(); + + IEnumerable projectFileDependencies = lockFile + .ProjectFileDependencyGroups + .Where(dg => dg.FrameworkName == string.Empty || + dg.FrameworkName == lockFileTarget.TargetFramework.DotNetFrameworkName); + + foreach (string projectFileDependency in projectFileDependencies.SelectMany(dg => dg.Dependencies)) + { + int separatorIndex = projectFileDependency.IndexOf(' '); + string dependencyName = separatorIndex > 0 ? + projectFileDependency.Substring(0, separatorIndex) : + projectFileDependency; + + Dependency dependency; + if (dependencyLookup.TryGetValue(dependencyName, out dependency)) + { + dependencies.Add(dependency); + } + } + + return dependencies; + } + + private RuntimeLibrary GetProjectRuntimeLibrary( + SingleProjectInfo projectInfo, + LockFile lockFile, + LockFileTarget lockFileTarget, + Dictionary dependencyLookup) + { + + RuntimeAssetGroup[] runtimeAssemblyGroups = new[] { new RuntimeAssetGroup(string.Empty, projectInfo.GetOutputName()) }; + + List dependencies = GetProjectDependencies(lockFile, lockFileTarget, dependencyLookup); + + ResourceAssembly[] resourceAssemblies = projectInfo + .ResourceAssemblies + .Select(r => new ResourceAssembly(r.RelativePath, r.Culture)) + .ToArray(); + + return new RuntimeLibrary( + type: "project", + name: projectInfo.Name, + version: projectInfo.Version, + hash: string.Empty, + runtimeAssemblyGroups: runtimeAssemblyGroups, + nativeLibraryGroups: new RuntimeAssetGroup[] { }, + resourceAssemblies: resourceAssemblies, + dependencies: dependencies.ToArray(), + serviceable: false); + } + + private CompilationLibrary GetProjectCompilationLibrary( + SingleProjectInfo projectInfo, + LockFile lockFile, + LockFileTarget lockFileTarget, + Dictionary dependencyLookup) + { + List dependencies = GetProjectDependencies(lockFile, lockFileTarget, dependencyLookup); + + return new CompilationLibrary( + type: "project", + name: projectInfo.Name, + version: projectInfo.Version, + hash: string.Empty, + assemblies: new[] { projectInfo.GetOutputName() }, + dependencies: dependencies.ToArray(), + serviceable: false); + } + + private IEnumerable GetLibraries( + IEnumerable exports, + IDictionary libraryLookup, + IDictionary dependencyLookup, + bool runtime) + { + return exports.Select(export => GetLibrary(export, libraryLookup, dependencyLookup, runtime)); + } + + private Library GetLibrary( + LockFileTargetLibrary export, + IDictionary libraryLookup, + IDictionary dependencyLookup, + bool runtime) + { + var type = export.Type; + + // TEMPORARY: All packages are serviceable in RC2 + // See https://github.com/dotnet/cli/issues/2569 + var serviceable = export.Type == "package"; + var libraryDependencies = new HashSet(); + + foreach (PackageDependency libraryDependency in export.Dependencies) + { + Dependency dependency; + if (dependencyLookup.TryGetValue(libraryDependency.Id, out dependency)) + { + libraryDependencies.Add(dependency); + } + } + + string hash = string.Empty; + string path = null; + string hashPath = null; + LockFileLibrary library; + if (libraryLookup.TryGetValue(export.Name, out library)) + { + if (!string.IsNullOrEmpty(library.Sha512)) + { + hash = "sha512-" + library.Sha512; + hashPath = _versionFolderPathResolver.GetHashFileName(export.Name, export.Version); + } + + path = library.Path; + } + + if (runtime) + { + return new RuntimeLibrary( + type.ToLowerInvariant(), + export.Name, + export.Version.ToString(), + hash, + CreateRuntimeAssemblyGroups(export), + CreateNativeLibraryGroups(export), + export.ResourceAssemblies.FilterPlaceHolderFiles().Select(CreateResourceAssembly), + libraryDependencies, + serviceable, + path, + hashPath); + } + else + { + IEnumerable assemblies = export + .CompileTimeAssemblies + .FilterPlaceHolderFiles() + .Select(libraryAsset => libraryAsset.Path); + + return new CompilationLibrary( + type.ToString().ToLowerInvariant(), + export.Name, + export.Version.ToString(), + hash, + assemblies, + libraryDependencies, + serviceable, + path, + hashPath); + } + } + + private IReadOnlyList CreateRuntimeAssemblyGroups(LockFileTargetLibrary export) + { + List assemblyGroups = new List(); + + assemblyGroups.Add( + new RuntimeAssetGroup( + string.Empty, + export.RuntimeAssemblies.FilterPlaceHolderFiles().Select(a => a.Path))); + + foreach (var runtimeTargetsGroup in export.GetRuntimeTargetsGroups("runtime")) + { + assemblyGroups.Add( + new RuntimeAssetGroup( + runtimeTargetsGroup.Key, + runtimeTargetsGroup.Select(t => t.Path))); + } + + return assemblyGroups; + } + + private IReadOnlyList CreateNativeLibraryGroups(LockFileTargetLibrary export) + { + List nativeGroups = new List(); + + nativeGroups.Add( + new RuntimeAssetGroup( + string.Empty, + export.NativeLibraries.FilterPlaceHolderFiles().Select(a => a.Path))); + + foreach (var runtimeTargetsGroup in export.GetRuntimeTargetsGroups("native")) + { + nativeGroups.Add( + new RuntimeAssetGroup( + runtimeTargetsGroup.Key, + runtimeTargetsGroup.Select(t => t.Path))); + } + + return nativeGroups; + } + + private ResourceAssembly CreateResourceAssembly(LockFileItem resourceAssembly) + { + string locale; + if (!resourceAssembly.Properties.TryGetValue("locale", out locale)) + { + locale = null; + } + + return new ResourceAssembly(resourceAssembly.Path, locale); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/LockFileTargetExtensions.cs b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/LockFileTargetExtensions.cs new file mode 100644 index 000000000..52c4d2aed --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/LockFileTargetExtensions.cs @@ -0,0 +1,105 @@ +// 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.Collections.Generic; +using System.Linq; +using NuGet.Packaging.Core; +using NuGet.ProjectModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal static class LockFileTargetExtensions + { + public static bool IsPortable(this LockFileTarget lockFileTarget) + { + return string.IsNullOrEmpty(lockFileTarget.RuntimeIdentifier) && + lockFileTarget.GetPlatformLibrary() != null; + } + + public static LockFileTargetLibrary GetPlatformLibrary(this LockFileTarget lockFileTarget) + { + // TODO: https://github.com/dotnet/sdk/issues/17 get this from the lock file + var platformPackageName = "Microsoft.NETCore.App"; + var platformLibrary = lockFileTarget + .Libraries + .FirstOrDefault(e => e.Name.Equals(platformPackageName, StringComparison.OrdinalIgnoreCase)); + + return platformLibrary; + } + + public static HashSet GetPlatformExclusionList( + this LockFileTarget lockFileTarget, + IDictionary libraryLookup) + { + var platformLibrary = lockFileTarget.GetPlatformLibrary(); + var exclusionList = new HashSet(); + + exclusionList.Add(platformLibrary.Name); + CollectDependencies(libraryLookup, platformLibrary.Dependencies, exclusionList); + + return exclusionList; + } + + public static IEnumerable GetRuntimeLibraries(this LockFileTarget lockFileTarget) + { + IEnumerable runtimeLibraries = lockFileTarget.Libraries; + Dictionary libraryLookup = + runtimeLibraries.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase); + + HashSet allExclusionList = new HashSet(); + + if (lockFileTarget.IsPortable()) + { + allExclusionList.UnionWith(lockFileTarget.GetPlatformExclusionList(libraryLookup)); + } + + // TODO: exclude "type: build" dependencies during publish - https://github.com/dotnet/sdk/issues/42 + + return runtimeLibraries.Filter(allExclusionList).ToArray(); + } + + public static IEnumerable GetCompileLibraries(this LockFileTarget lockFileTarget) + { + // TODO: exclude "type: build" dependencies during publish - https://github.com/dotnet/sdk/issues/42 + + return lockFileTarget.Libraries; + } + + public static IEnumerable Filter( + this IEnumerable libraries, + HashSet exclusionList) + { + return libraries.Where(e => !exclusionList.Contains(e.Name)); + } + + public static IEnumerable> GetRuntimeTargetsGroups( + this LockFileTargetLibrary library, + string assetType) + { + return library.RuntimeTargets + .FilterPlaceHolderFiles() + .Cast() + .Where(t => string.Equals(t.AssetType, assetType, StringComparison.OrdinalIgnoreCase)) + .GroupBy(t => t.Runtime); + } + + private static void CollectDependencies( + IDictionary libraryLookup, + IEnumerable dependencies, + HashSet exclusionList) + { + foreach (PackageDependency dependency in dependencies) + { + LockFileTargetLibrary library = libraryLookup[dependency.Id]; + if (library.Version.Equals(dependency.VersionRange.MinVersion)) + { + if (exclusionList.Add(library.Name)) + { + CollectDependencies(libraryLookup, library.Dependencies, exclusionList); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/NuGetUtils.cs b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/NuGetUtils.cs new file mode 100644 index 000000000..2c569a8ab --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/NuGetUtils.cs @@ -0,0 +1,25 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using NuGet.Packaging.Core; +using NuGet.ProjectModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal static class NuGetUtils + { + public static bool IsPlaceholderFile(string path) + { + return string.Equals(Path.GetFileName(path), PackagingCoreConstants.EmptyFolder, StringComparison.Ordinal); + } + + public static IEnumerable FilterPlaceHolderFiles(this IEnumerable files) + { + return files.Where(f => !IsPlaceholderFile(f.Path)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/ResourceAssemblyInfo.cs b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/ResourceAssemblyInfo.cs new file mode 100644 index 000000000..80346aae2 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/ResourceAssemblyInfo.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. + +namespace Microsoft.DotNet.Cli.Utils +{ + internal class ResourceAssemblyInfo + { + public string Culture { get; } + public string RelativePath { get; } + + public ResourceAssemblyInfo(string culture, string relativePath) + { + Culture = culture; + RelativePath = relativePath; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/SingleProjectInfo.cs b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/SingleProjectInfo.cs new file mode 100644 index 000000000..fa15442d6 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/DependencyContextBuilding/SingleProjectInfo.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal class SingleProjectInfo + { + public string Name { get; } + public string Version { get; } + + public IEnumerable ResourceAssemblies { get; } + + public SingleProjectInfo(string name, string version, IEnumerable resourceAssemblies) + { + Name = name; + Version = version; + ResourceAssemblies = resourceAssemblies; + } + + public string GetOutputName() + { + return $"{Name}.dll"; + } + } +} \ No newline at end of file