diff --git a/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs b/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs index 8b6efbeac..867d4eab1 100644 --- a/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs +++ b/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs @@ -16,5 +16,10 @@ namespace Microsoft.Extensions.EnvironmentAbstractions { return File.ReadAllText(path); } + + public Stream OpenRead(string path) + { + return File.OpenRead(path); + } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.InternalAbstractions/IFile.cs b/src/Microsoft.DotNet.InternalAbstractions/IFile.cs index 26d029eec..5b0328de9 100644 --- a/src/Microsoft.DotNet.InternalAbstractions/IFile.cs +++ b/src/Microsoft.DotNet.InternalAbstractions/IFile.cs @@ -1,6 +1,8 @@ // 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.IO; + namespace Microsoft.Extensions.EnvironmentAbstractions { internal interface IFile @@ -8,5 +10,7 @@ namespace Microsoft.Extensions.EnvironmentAbstractions bool Exists(string path); string ReadAllText(string path); + + Stream OpenRead(string path); } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.ProjectModel/DependencyContextBuilder.cs b/src/Microsoft.DotNet.ProjectModel/DependencyContextBuilder.cs index fcfec2cc4..d36ac6f82 100644 --- a/src/Microsoft.DotNet.ProjectModel/DependencyContextBuilder.cs +++ b/src/Microsoft.DotNet.ProjectModel/DependencyContextBuilder.cs @@ -55,7 +55,7 @@ namespace Microsoft.Extensions.DependencyModel compilationOptions, GetLibraries(compilationExports, dependencyLookup, runtime: false).Cast(), GetLibraries(runtimeExports, dependencyLookup, runtime: true).Cast(), - new KeyValuePair[0]); + new RuntimeFallbacks[] {}); } private static CompilationOptions GetCompilationOptions(CommonCompilerOptions compilerOptions) diff --git a/src/Microsoft.Extensions.DependencyModel/DependencyContext.cs b/src/Microsoft.Extensions.DependencyModel/DependencyContext.cs index c31ea8dc6..f4f3e34ba 100644 --- a/src/Microsoft.Extensions.DependencyModel/DependencyContext.cs +++ b/src/Microsoft.Extensions.DependencyModel/DependencyContext.cs @@ -11,9 +11,6 @@ namespace Microsoft.Extensions.DependencyModel { public class DependencyContext { - private const string DepsJsonExtension = ".deps.json"; - private const string DepsFileExtension = ".deps"; - private static readonly Lazy _defaultContext = new Lazy(LoadDefault); public DependencyContext(string targetFramework, @@ -22,7 +19,7 @@ namespace Microsoft.Extensions.DependencyModel CompilationOptions compilationOptions, IEnumerable compileLibraries, IEnumerable runtimeLibraries, - IEnumerable> runtimeGraph) + IEnumerable runtimeGraph) { if (targetFramework == null) { @@ -32,6 +29,10 @@ namespace Microsoft.Extensions.DependencyModel { throw new ArgumentNullException(nameof(runtime)); } + if (compilationOptions == null) + { + throw new ArgumentNullException(nameof(compilationOptions)); + } if (compileLibraries == null) { throw new ArgumentNullException(nameof(compileLibraries)); @@ -68,48 +69,47 @@ namespace Microsoft.Extensions.DependencyModel public IReadOnlyList RuntimeLibraries { get; } - public IReadOnlyList> RuntimeGraph { get; } + public IReadOnlyList RuntimeGraph { get; } + + public DependencyContext Merge(DependencyContext other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + return new DependencyContext( + TargetFramework, + Runtime, + IsPortable, + CompilationOptions, + CompileLibraries.Union(other.CompileLibraries, new LibraryMergeEqualityComparer()), + RuntimeLibraries.Union(other.RuntimeLibraries, new LibraryMergeEqualityComparer()), + RuntimeGraph.Union(other.RuntimeGraph) + ); + } private static DependencyContext LoadDefault() { - var entryAssembly = Assembly.GetEntryAssembly(); - return Load(entryAssembly); + return DependencyContextLoader.Default.Load(Assembly.GetEntryAssembly()); } public static DependencyContext Load(Assembly assembly) { - if (assembly == null) + return DependencyContextLoader.Default.Load(assembly); + } + + private class LibraryMergeEqualityComparer: IEqualityComparer where T:Library + { + public bool Equals(T x, T y) { - throw new ArgumentNullException(nameof(assembly)); + return string.Equals(x.Name, y.Name, StringComparison.Ordinal); } - using (var stream = assembly.GetManifestResourceStream(assembly.GetName().Name + DepsJsonExtension)) + public int GetHashCode(T obj) { - if (stream != null) - { - return new DependencyContextJsonReader().Read(stream); - } + return obj.Name.GetHashCode(); } - - var depsJsonFile = Path.ChangeExtension(assembly.Location, DepsJsonExtension); - if (File.Exists(depsJsonFile)) - { - using (var stream = File.OpenRead(depsJsonFile)) - { - return new DependencyContextJsonReader().Read(stream); - } - } - - var depsFile = Path.ChangeExtension(assembly.Location, DepsFileExtension); - if (File.Exists(depsFile)) - { - using (var stream = File.OpenRead(depsFile)) - { - return new DependencyContextCsvReader().Read(stream); - } - } - - return null; } } } diff --git a/src/Microsoft.Extensions.DependencyModel/DependencyContextCsvReader.cs b/src/Microsoft.Extensions.DependencyModel/DependencyContextCsvReader.cs index ebab91f2e..0e1e5e54a 100644 --- a/src/Microsoft.Extensions.DependencyModel/DependencyContextCsvReader.cs +++ b/src/Microsoft.Extensions.DependencyModel/DependencyContextCsvReader.cs @@ -9,7 +9,7 @@ using System.Text; namespace Microsoft.Extensions.DependencyModel { - public class DependencyContextCsvReader + public class DependencyContextCsvReader: IDependencyContextReader { public DependencyContext Read(Stream stream) { @@ -62,7 +62,7 @@ namespace Microsoft.Extensions.DependencyModel compilationOptions: CompilationOptions.Default, compileLibraries: Enumerable.Empty(), runtimeLibraries: runtimeLibraries.ToArray(), - runtimeGraph: Enumerable.Empty>()); + runtimeGraph: Enumerable.Empty()); } private Tuple PackageIdentity(DepsFileLine line) diff --git a/src/Microsoft.Extensions.DependencyModel/DependencyContextJsonReader.cs b/src/Microsoft.Extensions.DependencyModel/DependencyContextJsonReader.cs index ca0d90916..12019b06f 100644 --- a/src/Microsoft.Extensions.DependencyModel/DependencyContextJsonReader.cs +++ b/src/Microsoft.Extensions.DependencyModel/DependencyContextJsonReader.cs @@ -10,7 +10,7 @@ using Newtonsoft.Json.Linq; namespace Microsoft.Extensions.DependencyModel { - public class DependencyContextJsonReader + public class DependencyContextJsonReader: IDependencyContextReader { public DependencyContext Read(Stream stream) { @@ -79,7 +79,7 @@ namespace Microsoft.Extensions.DependencyModel ); } - private IEnumerable> ReadRuntimeGraph(JObject runtimes) + private IEnumerable ReadRuntimeGraph(JObject runtimes) { if (runtimes == null) { @@ -90,7 +90,7 @@ namespace Microsoft.Extensions.DependencyModel var runtime = (JProperty)targets.Single(); foreach (var pair in (JObject)runtime.Value) { - yield return new KeyValuePair(pair.Key, pair.Value.Values().ToArray()); + yield return new RuntimeFallbacks(pair.Key, pair.Value.Values().ToArray()); } } diff --git a/src/Microsoft.Extensions.DependencyModel/DependencyContextLoader.cs b/src/Microsoft.Extensions.DependencyModel/DependencyContextLoader.cs new file mode 100644 index 000000000..4a4256bb1 --- /dev/null +++ b/src/Microsoft.Extensions.DependencyModel/DependencyContextLoader.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.Extensions.DependencyModel +{ + public class DependencyContextLoader + { + private static Lazy _depsFiles = new Lazy(GetHostDepsList); + + private const string DepsJsonExtension = ".deps.json"; + private const string DepsExtension = ".deps"; + + private readonly string _entryPointDepsLocation; + private readonly string _runtimeDepsLocation; + private readonly IFileSystem _fileSystem; + private readonly IDependencyContextReader _jsonReader; + private readonly IDependencyContextReader _csvReader; + + public DependencyContextLoader() : this( + GetDefaultEntrypointDepsLocation(), + GetDefaultRuntimeDepsLocation(), + FileSystemWrapper.Default, + new DependencyContextJsonReader(), + new DependencyContextCsvReader()) + { + } + + internal DependencyContextLoader( + string entryPointDepsLocation, + string runtimeDepsLocation, + IFileSystem fileSystem, + IDependencyContextReader jsonReader, + IDependencyContextReader csvReader) + { + _entryPointDepsLocation = entryPointDepsLocation; + _runtimeDepsLocation = runtimeDepsLocation; + _fileSystem = fileSystem; + _jsonReader = jsonReader; + _csvReader = csvReader; + } + + public static DependencyContextLoader Default { get; } = new DependencyContextLoader(); + + internal virtual bool IsEntryAssembly(Assembly assembly) + { + return assembly.GetName() == Assembly.GetEntryAssembly().GetName(); + } + + internal virtual Stream GetResourceStream(Assembly assembly, string name) + { + return assembly.GetManifestResourceStream(name); + } + + public DependencyContext Load(Assembly assembly) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + DependencyContext context = null; + + if (IsEntryAssembly(assembly)) + { + context = LoadEntryAssemblyContext(); + } + + if (context == null) + { + context = LoadAssemblyContext(assembly); + } + + if (context?.IsPortable == true) + { + var runtimeContext = LoadRuntimeContext(); + if (runtimeContext != null) + { + context = context.Merge(runtimeContext); + } + } + return context; + } + + private DependencyContext LoadEntryAssemblyContext() + { + if (!string.IsNullOrEmpty(_entryPointDepsLocation)) + { + Debug.Assert(File.Exists(_entryPointDepsLocation)); + using (var stream = _fileSystem.File.OpenRead(_entryPointDepsLocation)) + { + return _jsonReader.Read(stream); + } + } + return null; + } + + private DependencyContext LoadRuntimeContext() + { + if (!string.IsNullOrEmpty(_runtimeDepsLocation)) + { + Debug.Assert(File.Exists(_runtimeDepsLocation)); + using (var stream = _fileSystem.File.OpenRead(_runtimeDepsLocation)) + { + return _jsonReader.Read(stream); + } + } + return null; + } + + private DependencyContext LoadAssemblyContext(Assembly assembly) + { + using (var stream = GetResourceStream(assembly, assembly.GetName().Name + DepsJsonExtension)) + { + if (stream != null) + { + return _jsonReader.Read(stream); + } + } + + var depsJsonFile = Path.ChangeExtension(assembly.Location, DepsJsonExtension); + if (_fileSystem.File.Exists(depsJsonFile)) + { + using (var stream = _fileSystem.File.OpenRead(depsJsonFile)) + { + return _jsonReader.Read(stream); + } + } + + var depsFile = Path.ChangeExtension(assembly.Location, DepsExtension); + if (_fileSystem.File.Exists(depsFile)) + { + using (var stream = _fileSystem.File.OpenRead(depsFile)) + { + return _csvReader.Read(stream); + } + } + + return null; + } + + private static string GetDefaultRuntimeDepsLocation() + { + var deps = _depsFiles.Value; + if (deps != null && deps.Length > 1) + { + return deps[1]; + } + return null; + } + + private static string GetDefaultEntrypointDepsLocation() + { + var deps = _depsFiles.Value; + if (deps != null && deps.Length > 0) + { + return deps[0]; + } + return null; + } + + private static string[] GetHostDepsList() + { + // TODO: Were going to replace this with AppContext.GetData + var appDomainType = typeof(object).GetTypeInfo().Assembly?.GetType("System.AppDomain"); + var currentDomain = appDomainType?.GetProperty("CurrentDomain")?.GetValue(null); + var deps = appDomainType?.GetMethod("GetData")?.Invoke(currentDomain, new[] { "APP_CONTEXT_DEPS_FILES" }); + + return (deps as string)?.Split(new [] { ';' }, StringSplitOptions.RemoveEmptyEntries); + } + + } +} diff --git a/src/Microsoft.Extensions.DependencyModel/DependencyContextWriter.cs b/src/Microsoft.Extensions.DependencyModel/DependencyContextWriter.cs index 1a6235345..ecc06a77b 100644 --- a/src/Microsoft.Extensions.DependencyModel/DependencyContextWriter.cs +++ b/src/Microsoft.Extensions.DependencyModel/DependencyContextWriter.cs @@ -50,7 +50,7 @@ namespace Microsoft.Extensions.DependencyModel private JObject WriteRuntimeGraph(DependencyContext context) { return new JObject( - context.RuntimeGraph.Select(g => new JProperty(g.Key, new JArray(g.Value))) + context.RuntimeGraph.Select(g => new JProperty(g.Runtime, new JArray(g.Fallbacks))) ); } @@ -79,7 +79,7 @@ namespace Microsoft.Extensions.DependencyModel { if (value != null) { - o[name] = value.ToString(); + o.Add(new JProperty(name, value)); } } @@ -168,7 +168,7 @@ namespace Microsoft.Extensions.DependencyModel { return; } - libraryObject.Add( + libraryObject.AddFirst( new JProperty(DependencyContextStrings.DependenciesPropertyName, new JObject( dependencies.Select(dependency => new JProperty(dependency.Name, dependency.Version)))) @@ -232,8 +232,8 @@ namespace Microsoft.Extensions.DependencyModel new JObject(runtimeLibrary.RuntimeTargets.SelectMany(WriteRuntimeTarget))) ); } - AddResourceAssemblies(libraryObject, runtimeLibrary.ResourceAssemblies); AddRuntimeAssemblies(libraryObject, runtimeLibrary.Assemblies); + AddResourceAssemblies(libraryObject, runtimeLibrary.ResourceAssemblies); libraryObject.Add(DependencyContextStrings.NativeLibrariesKey, WriteAssetList(runtimeLibrary.NativeLibraries)); dependencies.UnionWith(runtimeLibrary.Dependencies); diff --git a/src/Microsoft.Extensions.DependencyModel/IDependencyContextReader.cs b/src/Microsoft.Extensions.DependencyModel/IDependencyContextReader.cs new file mode 100644 index 000000000..57b2a442e --- /dev/null +++ b/src/Microsoft.Extensions.DependencyModel/IDependencyContextReader.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace Microsoft.Extensions.DependencyModel +{ + public interface IDependencyContextReader + { + DependencyContext Read(Stream stream); + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.DependencyModel/RuntimeFallbacks.cs b/src/Microsoft.Extensions.DependencyModel/RuntimeFallbacks.cs new file mode 100644 index 000000000..d759b1942 --- /dev/null +++ b/src/Microsoft.Extensions.DependencyModel/RuntimeFallbacks.cs @@ -0,0 +1,19 @@ +// 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.Extensions.DependencyModel +{ + public class RuntimeFallbacks + { + public string Runtime { get; set; } + public IEnumerable Fallbacks { get; set; } + + public RuntimeFallbacks(string runtime, IEnumerable fallbacks) + { + Runtime = runtime; + Fallbacks = fallbacks; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs index 40ac2aa37..b5bfd5a56 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using Microsoft.Extensions.EnvironmentAbstractions; namespace Microsoft.Extensions.DependencyModel.Tests @@ -74,6 +75,11 @@ namespace Microsoft.Extensions.DependencyModel.Tests } return text; } + + public Stream OpenRead(string path) + { + return new MemoryStream(Encoding.UTF8.GetBytes(ReadAllText(path))); + } } private class DirectoryMock : IDirectory diff --git a/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonReaderTest.cs b/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonReaderTest.cs index b48d812e5..719559d7a 100644 --- a/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonReaderTest.cs +++ b/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonReaderTest.cs @@ -96,14 +96,14 @@ namespace Microsoft.Extensions.DependencyModel.Tests } } }"); - context.RuntimeGraph.Should().Contain(p => p.Key == "osx.10.10-x64").Which - .Value.Should().BeEquivalentTo(); + context.RuntimeGraph.Should().Contain(p => p.Runtime == "osx.10.10-x64").Which + .Fallbacks.Should().BeEquivalentTo(); - context.RuntimeGraph.Should().Contain(p => p.Key == "osx.10.11-x64").Which - .Value.Should().BeEquivalentTo("osx"); + context.RuntimeGraph.Should().Contain(p => p.Runtime == "osx.10.11-x64").Which + .Fallbacks.Should().BeEquivalentTo("osx"); - context.RuntimeGraph.Should().Contain(p => p.Key == "rhel.7-x64").Which - .Value.Should().BeEquivalentTo("linux-x64", "unix"); + context.RuntimeGraph.Should().Contain(p => p.Runtime == "rhel.7-x64").Which + .Fallbacks.Should().BeEquivalentTo("linux-x64", "unix"); } [Fact] diff --git a/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonWriterTests.cs b/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonWriterTests.cs index 2091b157b..f4d4ad721 100644 --- a/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonWriterTests.cs +++ b/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextJsonWriterTests.cs @@ -37,7 +37,7 @@ namespace Microsoft.Extensions.DependencyModel.Tests CompilationOptions compilationOptions = null, CompilationLibrary[] compileLibraries = null, RuntimeLibrary[] runtimeLibraries = null, - IReadOnlyList> runtimeGraph = null) + IReadOnlyList runtimeGraph = null) { return new DependencyContext( target ?? string.Empty, @@ -46,7 +46,7 @@ namespace Microsoft.Extensions.DependencyModel.Tests compilationOptions ?? CompilationOptions.Default, compileLibraries ?? new CompilationLibrary[0], runtimeLibraries ?? new RuntimeLibrary[0], - runtimeGraph ?? new KeyValuePair[0] + runtimeGraph ?? new RuntimeFallbacks[0] ); } @@ -58,8 +58,8 @@ namespace Microsoft.Extensions.DependencyModel.Tests "Target/runtime", runtimeGraph: new[] { - new KeyValuePair("win7-x64", new [] { "win6", "win5"}), - new KeyValuePair("win8-x64", new [] { "win7-x64"}), + new RuntimeFallbacks("win7-x64", new [] { "win6", "win5"}), + new RuntimeFallbacks("win8-x64", new [] { "win7-x64"}), })); var rids = result.Should().HaveProperty("runtimes") diff --git a/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextLoaderTests.cs b/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextLoaderTests.cs new file mode 100644 index 000000000..3b6e88fea --- /dev/null +++ b/test/Microsoft.Extensions.DependencyModel.Tests/DependencyContextLoaderTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyModel; +using FluentAssertions; +using Xunit; + +namespace Microsoft.Extensions.DependencyModel.Tests +{ + public class DependencyContextTests + { + [Fact] + public void MergeMergesLibraries() + { + var compilationLibraries = new[] + { + CreateCompilation("PackageA"), + CreateCompilation("PackageB"), + }; + + var runtimeLibraries = new[] + { + CreateRuntime("PackageA"), + CreateRuntime("PackageB"), + }; + + var compilationLibrariesRedist = new[] + { + CreateCompilation("PackageB"), + CreateCompilation("PackageC"), + }; + + var runtimeLibrariesRedist = new[] + { + CreateRuntime("PackageB"), + CreateRuntime("PackageC"), + }; + + var context = new DependencyContext( + "Framework", + "runtime", + true, + CompilationOptions.Default, + compilationLibraries, + runtimeLibraries, + new RuntimeFallbacks[] { }); + + var contextRedist = new DependencyContext( + "Framework", + "runtime", + true, + CompilationOptions.Default, + compilationLibrariesRedist, + runtimeLibrariesRedist, + new RuntimeFallbacks[] { }); + + var result = context.Merge(contextRedist); + + result.CompileLibraries.Should().BeEquivalentTo(new[] + { + compilationLibraries[0], + compilationLibraries[1], + compilationLibrariesRedist[1], + }); + + result.RuntimeLibraries.Should().BeEquivalentTo(new[] + { + runtimeLibraries[0], + runtimeLibraries[1], + runtimeLibrariesRedist[1], + }); + } + + public void MergeMergesRuntimeGraph() + { + var context = new DependencyContext( + "Framework", + "runtime", + true, + CompilationOptions.Default, + Enumerable.Empty(), + Enumerable.Empty(), + new RuntimeFallbacks[] + { + new RuntimeFallbacks("win8-x64", new [] { "win8" }), + }); + + var contextRedist = new DependencyContext( + "Framework", + "runtime", + true, + CompilationOptions.Default, + Enumerable.Empty(), + Enumerable.Empty(), + new RuntimeFallbacks[] + { + new RuntimeFallbacks("win8", new [] { "win7-x64", "win7-x86" }), + }); + + var result = context.Merge(contextRedist); + result.RuntimeGraph.Should().Contain(g => g.Runtime == "win8-x64"). + Subject.Fallbacks.Should().BeEquivalentTo("win8"); + result.RuntimeGraph.Should().Contain(g => g.Runtime == "win8"). + Subject.Fallbacks.Should().BeEquivalentTo("win7-x64", "win7-x86"); + } + + private CompilationLibrary CreateCompilation(string name) + { + return new CompilationLibrary( + "project", + name, + "1.1.1", + "HASH", + new string[] { }, + new Dependency[] { }, + false); + } + + private RuntimeLibrary CreateRuntime(string name) + { + return new RuntimeLibrary( + "project", + name, + "1.1.1", + "HASH", + new RuntimeAssembly[] { }, + new string[] { }, + new ResourceAssembly[] { }, + new RuntimeTarget[] { }, + new Dependency[] {}, + false); + } + } +}