diff --git a/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/InteractiveEvaluator.cs b/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/InteractiveEvaluator.cs index bde8ae0eafa7f3bf0cbb93b4b0935d170e53c799..1e38de2a5850e15668eeddc645cc42ec78498435 100644 --- a/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/InteractiveEvaluator.cs +++ b/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/InteractiveEvaluator.cs @@ -2,7 +2,6 @@ extern alias WORKSPACES; - using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -29,7 +28,6 @@ using Roslyn.Utilities; using DesktopMetadataReferenceResolver = WORKSPACES::Microsoft.CodeAnalysis.Scripting.DesktopMetadataReferenceResolver; using GacFileResolver = WORKSPACES::Microsoft.CodeAnalysis.Scripting.GacFileResolver; -using NuGetPackageResolver = WORKSPACES::Microsoft.CodeAnalysis.Scripting.NuGetPackageResolver; namespace Microsoft.CodeAnalysis.Editor.Interactive { @@ -248,9 +246,13 @@ private Dispatcher Dispatcher private static MetadataFileReferenceResolver CreateFileResolver(ImmutableArray referencePaths, string baseDirectory) { + var userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var packagesDirectory = (userProfilePath == null) ? + null : + PathUtilities.CombineAbsoluteAndRelativePaths(userProfilePath, PathUtilities.CombinePossiblyRelativeAndRelativePaths(".nuget", "packages")); return new DesktopMetadataReferenceResolver( new RelativePathReferenceResolver(referencePaths, baseDirectory), - NuGetPackageResolver.Instance, + string.IsNullOrEmpty(packagesDirectory) ? null : new NuGetPackageResolverImpl(packagesDirectory), new GacFileResolver( architectures: GacFileResolver.Default.Architectures, // TODO (tomat) preferredCulture: System.Globalization.CultureInfo.CurrentCulture)); // TODO (tomat) diff --git a/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/NuGetPackageResolverImpl.cs b/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/NuGetPackageResolverImpl.cs new file mode 100644 index 0000000000000000000000000000000000000000..76ffc34df775a32a1efc880935b39704897d06de --- /dev/null +++ b/src/Interactive/EditorFeatures/Core/Extensibility/Interactive/NuGetPackageResolverImpl.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +extern alias WORKSPACES; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Roslyn.Utilities; +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Reflection; + +namespace Microsoft.CodeAnalysis.Editor.Interactive +{ + internal sealed class NuGetPackageResolverImpl : WORKSPACES::Microsoft.CodeAnalysis.Scripting.NuGetPackageResolver + { + private const string ProjectJsonFramework = "net46"; + private const string ProjectLockJsonFramework = ".NETFramework,Version=v4.6"; + + private readonly string _packagesDirectory; + private readonly Action _restore; + + internal NuGetPackageResolverImpl(string packagesDirectory, Action restore = null) + { + Debug.Assert(PathUtilities.IsAbsolute(packagesDirectory)); + _packagesDirectory = packagesDirectory; + _restore = restore ?? NuGetRestore; + } + + internal override ImmutableArray ResolveNuGetPackage(string reference) + { + string packageName; + string packageVersion; + if (!ParsePackageReference(reference, out packageName, out packageVersion)) + { + return default(ImmutableArray); + } + + try + { + var tempPath = PathUtilities.CombineAbsoluteAndRelativePaths(Path.GetTempPath(), Guid.NewGuid().ToString("D")); + var tempDir = Directory.CreateDirectory(tempPath); + try + { + // Create project.json. + var projectJson = PathUtilities.CombineAbsoluteAndRelativePaths(tempPath, "project.json"); + using (var stream = File.OpenWrite(projectJson)) + using (var writer = new StreamWriter(stream)) + { + WriteProjectJson(writer, packageName, packageVersion); + } + + // Run "nuget.exe restore project.json" to generate project.lock.json. + NuGetRestore(projectJson); + + // Read the references from project.lock.json. + var projectLockJson = PathUtilities.CombineAbsoluteAndRelativePaths(tempPath, "project.lock.json"); + using (var stream = File.OpenRead(projectLockJson)) + using (var reader = new StreamReader(stream)) + { + return ReadProjectLockJson(_packagesDirectory, reader); + } + } + finally + { + tempDir.Delete(recursive: true); + } + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + return default(ImmutableArray); + } + + /// + /// Syntax is "id/version", matching references in project.lock.json. + /// + internal static bool ParsePackageReference(string reference, out string name, out string version) + { + var parts = reference.Split('/'); + int n = reference.Length; + if ((parts.Length == 2) && + (parts[0].Length > 0) && + (parts[1].Length > 0)) + { + name = parts[0]; + version = parts[1]; + return true; + } + name = null; + version = null; + return false; + } + + /// + /// Generate a project.json file with the packages as "dependencies". + /// + internal static void WriteProjectJson(TextWriter writer, string packageName, string packageVersion) + { + using (var jsonWriter = new JsonTextWriter(writer)) + { + jsonWriter.Formatting = Newtonsoft.Json.Formatting.Indented; + jsonWriter.WriteStartObject(); + // "dependencies" : { "packageName" : "packageVersion" } + jsonWriter.WritePropertyName("dependencies"); + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName(packageName, escape: true); + jsonWriter.WriteValue(packageVersion); + jsonWriter.WriteEndObject(); + // "frameworks" : { "net46" : {} } + jsonWriter.WritePropertyName("frameworks"); + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName(ProjectJsonFramework, escape: true); + jsonWriter.WriteStartObject(); + jsonWriter.WriteEndObject(); + jsonWriter.WriteEndObject(); + jsonWriter.WriteEndObject(); + } + } + + /// + /// Read the references from the project.lock.json file. + /// + internal static ImmutableArray ReadProjectLockJson(string packagesDirectory, TextReader reader) + { + JObject obj; + using (var jsonReader = new JsonTextReader(reader)) + { + obj = JObject.Load(jsonReader); + } + var builder = ArrayBuilder.GetInstance(); + var targets = (JObject)GetPropertyValue(obj, "targets"); + foreach (var target in targets) + { + if (target.Key == ProjectLockJsonFramework) + { + foreach (var package in (JObject)target.Value) + { + var packageRoot = PathUtilities.CombineAbsoluteAndRelativePaths(packagesDirectory, package.Key); + var runtime = (JObject)GetPropertyValue((JObject)package.Value, "runtime"); + if (runtime == null) + { + continue; + } + foreach (var item in runtime) + { + var path = PathUtilities.CombinePossiblyRelativeAndRelativePaths(packageRoot, item.Key); + builder.Add(path); + } + } + break; + } + } + return builder.ToImmutableAndFree(); + } + + private static JToken GetPropertyValue(JObject obj, string propertyName) + { + JToken value; + obj.TryGetValue(propertyName, out value); + return value; + } + + private void NuGetRestore(string projectJsonPath) + { + // Load nuget.exe from same directory as current assembly. + var nugetExePath = PathUtilities.CombineAbsoluteAndRelativePaths( + PathUtilities.GetDirectoryName( + CorLightup.Desktop.GetAssemblyLocation(typeof(NuGetPackageResolverImpl).GetTypeInfo().Assembly)), + "nuget.exe"); + var startInfo = new ProcessStartInfo() + { + FileName = nugetExePath, + Arguments = $"restore \"{projectJsonPath}\" -PackagesDirectory \"{_packagesDirectory}\"", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + _restore(startInfo); + } + + private static void NuGetRestore(ProcessStartInfo startInfo) + { + var process = Process.Start(startInfo); + string line; + var reader = process.StandardOutput; + while ((line = reader.ReadLine()) != null) + { + // Should echo output to InteractiveWindow. + } + reader = process.StandardError; + while ((line = reader.ReadLine()) != null) + { + // Should echo errors to InteractiveWindow. + } + process.WaitForExit(); + } + } +} diff --git a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj index f6fe6ca591e9c4d1b6553bdaed9a707a83f0ca8a..b4f7f5effc7cc14bf3cb33b9ebf8ab8861ab9bfd 100644 --- a/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj +++ b/src/Interactive/EditorFeatures/Core/InteractiveEditorFeatures.csproj @@ -110,6 +110,7 @@ + @@ -130,6 +131,7 @@ + diff --git a/src/Interactive/EditorFeatures/Core/project.json b/src/Interactive/EditorFeatures/Core/project.json index dd069168d5c566a5b0436b45b58371fe2c0134eb..1ffabe7856b09c8cce98fab2eb0c10867913f1d0 100644 --- a/src/Interactive/EditorFeatures/Core/project.json +++ b/src/Interactive/EditorFeatures/Core/project.json @@ -1,6 +1,7 @@ { "dependencies": { "Microsoft.Composition": "1.0.27", + "Newtonsoft.Json": "6.0.4", "System.Collections": "4.0.10", "System.Diagnostics.Debug": "4.0.10", "System.Globalization": "4.0.0", diff --git a/src/Interactive/HostTest/InteractiveHostTest.csproj b/src/Interactive/HostTest/InteractiveHostTest.csproj index 72f6cbe04353b9684c970daedbed746004ab1776..3c772e9b856513fd6160ba018569a16536077914 100644 --- a/src/Interactive/HostTest/InteractiveHostTest.csproj +++ b/src/Interactive/HostTest/InteractiveHostTest.csproj @@ -45,6 +45,10 @@ {76C6F005-C89D-4348-BB4A-391898DBEB52} TestUtilities.Desktop + + {2e87fa96-50bb-4607-8676-46521599f998} + Workspaces.Desktop + {5F8D2414-064A-4B3A-9B42-8E2A04246BE5} Workspaces @@ -53,6 +57,10 @@ {B0CE9307-FFDB-4838-A5EC-CE1F7CDC4AC2} CSharpEditorFeatures + + {92412d1a-0f23-45b5-b196-58839c524917} + InteractiveEditorFeatures + {FE2CBEA6-D121-4FAA-AA8B-FC9900BF8C83} CSharpInteractiveEditorFeatures @@ -107,6 +115,7 @@ + diff --git a/src/Interactive/HostTest/NuGetPackageResolverTests.cs b/src/Interactive/HostTest/NuGetPackageResolverTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..44b936969a9a378cb3292a0b8292deeee9877dff --- /dev/null +++ b/src/Interactive/HostTest/NuGetPackageResolverTests.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Editor.Interactive; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Roslyn.Utilities; +using System.Collections.Immutable; +using System.IO; +using System.Text; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.Interactive +{ + public class NuGetPackageResolverTests : TestBase + { + [ConditionalFact(typeof(WindowsOnly))] + public void ResolveReference() + { + var expectedProjectJson = +@"{ + ""dependencies"": { + ""A.B.C"": ""1.2"" + }, + ""frameworks"": { + ""net46"": {} + } +}"; + var actualProjectLockJson = +@"{ + ""locked"": false, + ""version"": 1, + ""targets"": { + "".NETFramework,Version=v4.5"": { }, + "".NETFramework,Version=v4.6"": { + ""System.Collections/4.0.10"": { + ""dependencies"": { + ""System.Runtime"": """" + }, + ""compile"": { + ""ref/dotnet/System.Runtime.dll"": {} + }, + ""runtime"": { + ""ref/dotnet/System.Collections.dll"": {} + } + }, + ""System.Diagnostics.Debug/4.0.10"": { + ""dependencies"": { + ""System.Runtime"": """" + }, + }, + ""System.IO/4.0.10"": { + ""dependencies"": {}, + ""runtime"": { + ""ref/dotnet/System.Runtime.dll"": {}, + ""ref/dotnet/System.IO.dll"": {} + } + } + } + } +}"; + using (var directory = new DisposableDirectory(Temp)) + { + var packagesDirectory = directory.Path; + var resolver = new NuGetPackageResolverImpl( + packagesDirectory, + startInfo => + { + var arguments = startInfo.Arguments.Split('"'); + Assert.Equal(5, arguments.Length); + Assert.Equal("restore ", arguments[0]); + Assert.Equal("project.json", PathUtilities.GetFileName(arguments[1])); + Assert.Equal(" -PackagesDirectory ", arguments[2]); + Assert.Equal(packagesDirectory, arguments[3]); + Assert.Equal("", arguments[4]); + var projectJsonPath = arguments[1]; + var actualProjectJson = File.ReadAllText(projectJsonPath); + Assert.Equal(expectedProjectJson, actualProjectJson); + var projectLockJsonPath = PathUtilities.CombineAbsoluteAndRelativePaths(PathUtilities.GetDirectoryName(projectJsonPath), "project.lock.json"); + using (var writer = new StreamWriter(projectLockJsonPath)) + { + writer.Write(actualProjectLockJson); + } + }); + var actualPaths = resolver.ResolveNuGetPackage("A.B.C/1.2"); + AssertEx.SetEqual(actualPaths, + PathUtilities.CombineAbsoluteAndRelativePaths(packagesDirectory, PathUtilities.CombinePossiblyRelativeAndRelativePaths("System.Collections/4.0.10", "ref/dotnet/System.Collections.dll")), + PathUtilities.CombineAbsoluteAndRelativePaths(packagesDirectory, PathUtilities.CombinePossiblyRelativeAndRelativePaths("System.IO/4.0.10", "ref/dotnet/System.Runtime.dll")), + PathUtilities.CombineAbsoluteAndRelativePaths(packagesDirectory, PathUtilities.CombinePossiblyRelativeAndRelativePaths("System.IO/4.0.10", "ref/dotnet/System.IO.dll"))); + } + } + + [ConditionalFact(typeof(WindowsOnly))] + public void ParsePackageNameAndVersion() + { + ParseInvalidPackageReference("A"); + ParseInvalidPackageReference("A.B"); + ParseInvalidPackageReference("A/"); + ParseInvalidPackageReference("A//1.0"); + ParseInvalidPackageReference("/1.0.0"); + ParseInvalidPackageReference("A/B/2.0.0"); + + ParseValidPackageReference("A/1", "A", "1"); + ParseValidPackageReference("A.B/1.0.0", "A.B", "1.0.0"); + ParseValidPackageReference("A/B.C", "A", "B.C"); + ParseValidPackageReference(" /1", " ", "1"); + ParseValidPackageReference("A\t/\n1.0\r ", "A\t", "\n1.0\r "); + } + + private static void ParseValidPackageReference(string reference, string expectedName, string expectedVersion) + { + string name; + string version; + Assert.True(NuGetPackageResolverImpl.ParsePackageReference(reference, out name, out version)); + Assert.Equal(expectedName, name); + Assert.Equal(expectedVersion, version); + } + + private static void ParseInvalidPackageReference(string reference) + { + string name; + string version; + Assert.False(NuGetPackageResolverImpl.ParsePackageReference(reference, out name, out version)); + Assert.Null(name); + Assert.Null(version); + } + + [ConditionalFact(typeof(WindowsOnly))] + public void WriteProjectJson() + { + WriteProjectJsonPackageReference("A.B", "4.0.1", +@"{ + ""dependencies"": { + ""A.B"": ""4.0.1"" + }, + ""frameworks"": { + ""net46"": {} + } +}"); + WriteProjectJsonPackageReference("\n\t", "\"'", +@"{ + ""dependencies"": { + ""\n\t"": ""\""'"" + }, + ""frameworks"": { + ""net46"": {} + } +}"); + } + + private static void WriteProjectJsonPackageReference(string packageName, string packageVersion, string expectedJson) + { + var builder = new StringBuilder(); + using (var writer = new StringWriter(builder)) + { + NuGetPackageResolverImpl.WriteProjectJson(writer, packageName, packageVersion); + } + var actualJson = builder.ToString(); + Assert.Equal(expectedJson, actualJson); + } + } +} diff --git a/src/Scripting/Core/DesktopMetadataReferenceResolver.cs b/src/Scripting/Core/DesktopMetadataReferenceResolver.cs index 5631f116721fbfaab550874c5cd09d6215077a3f..e80c8e293d2e34ce622c831a14c7df9e944ccc7e 100644 --- a/src/Scripting/Core/DesktopMetadataReferenceResolver.cs +++ b/src/Scripting/Core/DesktopMetadataReferenceResolver.cs @@ -50,11 +50,8 @@ public override string ResolveReference(string reference, string baseFilePath) if (_packageResolver != null) { - string path = _packageResolver.ResolveNuGetPackage(reference); - if (path != null && PortableShim.File.Exists(path)) - { - return path; - } + // TODO: Call _packageResolver.ResolveNuGetPackage when + // this method supports returning a collection of results. } if (_gacFileResolver != null) diff --git a/src/Scripting/Core/NuGetPackageResolver.cs b/src/Scripting/Core/NuGetPackageResolver.cs index 3cdb2ccd54adf23e3a8457dcaf8a6d312c2f3b71..86ebc194d0929729189824f81f8edd2ceef4e4ac 100644 --- a/src/Scripting/Core/NuGetPackageResolver.cs +++ b/src/Scripting/Core/NuGetPackageResolver.cs @@ -1,64 +1,11 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Linq; -using Roslyn.Utilities; +using System.Collections.Immutable; namespace Microsoft.CodeAnalysis.Scripting { - internal sealed class NuGetPackageResolver + internal abstract class NuGetPackageResolver { - internal static readonly NuGetPackageResolver Instance = new NuGetPackageResolver(); - - private NuGetPackageResolver() - { - } - - internal string ResolveNuGetPackage(string reference) - { - if (PathUtilities.IsFilePath(reference)) - { - return null; - } - - var assemblyName = GetPackageAssemblyName(reference); - if (assemblyName == null) - { - return null; - } - - // Expecting {package}{version}\lib\{arch}\{package}.dll. - var resolvedPath = PathUtilities.CombineAbsoluteAndRelativePaths(reference, "lib"); - if (!PortableShim.Directory.Exists(resolvedPath)) - { - return null; - } - - // We're not validating the architecture currently - // so fail if there's not exactly one architecture. - resolvedPath = PortableShim.Directory.EnumerateDirectories(resolvedPath, "*", PortableShim.SearchOption.TopDirectoryOnly).SingleOrDefault(); - if (resolvedPath == null) - { - return null; - } - - return PathUtilities.CombineAbsoluteAndRelativePaths(resolvedPath, assemblyName); - } - - private static string GetPackageAssemblyName(string reference) - { - // Assembly name is in .nuspec file. - // For now, simply strip off any version #, etc. - var name = PathUtilities.GetFileName(reference); - int offset = 0; - while ((offset = name.IndexOf('.', offset)) >= 0) - { - if ((offset < name.Length - 1) && char.IsDigit(name[offset + 1])) - { - return name.Substring(0, offset) + ".dll"; - } - offset += 1; - } - return null; - } + internal abstract ImmutableArray ResolveNuGetPackage(string reference); } } diff --git a/src/Test/Utilities/Runtime.FX46/project.json b/src/Test/Utilities/Runtime.FX46/project.json index dbb643438706b0419a17ba5985fed68907aaada9..9af7e25f61cccc7ef14a6beb94f03dff3223492e 100644 --- a/src/Test/Utilities/Runtime.FX46/project.json +++ b/src/Test/Utilities/Runtime.FX46/project.json @@ -1,6 +1,7 @@ { "dependencies": { "Microsoft.NETCore.Platforms": "1.0.0", + "Newtonsoft.Json": "6.0.4", "System.Collections": "4.0.10", "System.Collections.Immutable": "1.1.36", "System.Console": "4.0.0-beta-23123",