diff --git a/TestAssets/TestPackages/dotnet-fallbackfoldertool/Program.cs b/TestAssets/TestPackages/dotnet-fallbackfoldertool/Program.cs new file mode 100644 index 000000000..c936da285 --- /dev/null +++ b/TestAssets/TestPackages/dotnet-fallbackfoldertool/Program.cs @@ -0,0 +1,15 @@ +// 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 ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Hello Fallback folder World!"); + } + } +} diff --git a/TestAssets/TestPackages/dotnet-fallbackfoldertool/dotnet-fallbackfoldertool.csproj b/TestAssets/TestPackages/dotnet-fallbackfoldertool/dotnet-fallbackfoldertool.csproj new file mode 100644 index 000000000..b7049a0ca --- /dev/null +++ b/TestAssets/TestPackages/dotnet-fallbackfoldertool/dotnet-fallbackfoldertool.csproj @@ -0,0 +1,17 @@ + + + + + netcoreapp2.0 + dotnet-fallbackfoldertool + Exe + + $(CLI_SharedFrameworkVersion) + + + + + $(ProjectRuntimeConfigFilePath) + + + diff --git a/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/AppWithFallbackFolderToolDependency.csproj b/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/AppWithFallbackFolderToolDependency.csproj new file mode 100755 index 000000000..706240e90 --- /dev/null +++ b/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/AppWithFallbackFolderToolDependency.csproj @@ -0,0 +1,14 @@ + + + + + netcoreapp2.0 + Exe + $(CLI_SharedFrameworkVersion) + netcoreapp2.0 + + + + + + diff --git a/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/NuGet.Config b/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/NuGet.Config new file mode 100644 index 000000000..b8e876fcb --- /dev/null +++ b/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + diff --git a/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/Program.cs b/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/Program.cs new file mode 100644 index 000000000..2130cd0a7 --- /dev/null +++ b/TestAssets/TestProjects/AppWithFallbackFolderToolDependency/Program.cs @@ -0,0 +1,15 @@ +// 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 ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/build/test/TestPackageProjects.targets b/build/test/TestPackageProjects.targets index 1d900b84a..88b80d3b4 100644 --- a/build/test/TestPackageProjects.targets +++ b/build/test/TestPackageProjects.targets @@ -127,6 +127,15 @@ True + + dotnet-fallbackfoldertool + dotnet-fallbackfoldertool.csproj + True + True + 1.0.0 + + True + dotnet-prefercliruntime dotnet-prefercliruntime.csproj diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs index f3f4b5eda..20f8a3302 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs @@ -18,5 +18,14 @@ namespace Microsoft.DotNet.Cli.Utils string depsFilePath, string runtimeConfigPath); + CommandSpec CreateCommandSpecFromLibrary( + LockFileTargetLibrary toolLibrary, + string commandName, + IEnumerable commandArguments, + IEnumerable allowedExtensions, + IEnumerable packageFolders, + CommandResolutionStrategy commandResolutionStrategy, + string depsFilePath, + string runtimeConfigPath); } } diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs index ca3ce324a..e7bd1b33c 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs @@ -31,6 +31,27 @@ namespace Microsoft.DotNet.Cli.Utils CommandResolutionStrategy commandResolutionStrategy, string depsFilePath, string runtimeConfigPath) + { + return CreateCommandSpecFromLibrary( + toolLibrary, + commandName, + commandArguments, + allowedExtensions, + new List { nugetPackagesRoot }, + commandResolutionStrategy, + depsFilePath, + runtimeConfigPath); + } + + public CommandSpec CreateCommandSpecFromLibrary( + LockFileTargetLibrary toolLibrary, + string commandName, + IEnumerable commandArguments, + IEnumerable allowedExtensions, + IEnumerable packageFolders, + CommandResolutionStrategy commandResolutionStrategy, + string depsFilePath, + string runtimeConfigPath) { Reporter.Verbose.WriteLine(string.Format( LocalizableStrings.AttemptingToFindCommand, @@ -51,7 +72,7 @@ namespace Microsoft.DotNet.Cli.Utils return null; } - var commandPath = GetCommandFilePath(nugetPackagesRoot, toolLibrary, toolAssembly); + var commandPath = GetCommandFilePath(packageFolders, toolLibrary, toolAssembly); if (!File.Exists(commandPath)) { @@ -68,17 +89,28 @@ namespace Microsoft.DotNet.Cli.Utils commandArguments, depsFilePath, commandResolutionStrategy, - nugetPackagesRoot, + packageFolders, runtimeConfigPath); } private string GetCommandFilePath( - string nugetPackagesRoot, + IEnumerable packageFolders, LockFileTargetLibrary toolLibrary, LockFileItem runtimeAssembly) { - var packageDirectory = new VersionFolderPathResolver(nugetPackagesRoot) - .GetInstallPath(toolLibrary.Name, toolLibrary.Version); + var packageFoldersCount = packageFolders.Count(); + var userPackageFolder = packageFoldersCount == 1 ? string.Empty : packageFolders.First(); + var fallbackPackageFolders = packageFoldersCount > 1 ? packageFolders.Skip(1) : packageFolders; + + var packageDirectory = new FallbackPackagePathResolver(userPackageFolder, fallbackPackageFolders) + .GetPackageDirectory(toolLibrary.Name, toolLibrary.Version); + + if (packageDirectory == null) + { + throw new GracefulException(string.Format( + LocalizableStrings.CommandAssembliesNotFound, + toolLibrary.Name)); + } var filePath = Path.Combine( packageDirectory, @@ -92,7 +124,7 @@ namespace Microsoft.DotNet.Cli.Utils IEnumerable commandArguments, string depsFilePath, CommandResolutionStrategy commandResolutionStrategy, - string nugetPackagesRoot, + IEnumerable packageFolders, string runtimeConfigPath) { var commandExtension = Path.GetExtension(commandPath); @@ -104,7 +136,7 @@ namespace Microsoft.DotNet.Cli.Utils commandArguments, depsFilePath, commandResolutionStrategy, - nugetPackagesRoot, + packageFolders, runtimeConfigPath); } @@ -116,7 +148,7 @@ namespace Microsoft.DotNet.Cli.Utils IEnumerable commandArguments, string depsFilePath, CommandResolutionStrategy commandResolutionStrategy, - string nugetPackagesRoot, + IEnumerable packageFolders, string runtimeConfigPath) { var host = string.Empty; @@ -144,8 +176,11 @@ namespace Microsoft.DotNet.Cli.Utils arguments.Add(depsFilePath); } - arguments.Add("--additionalprobingpath"); - arguments.Add(nugetPackagesRoot); + foreach (var packageFolder in packageFolders) + { + arguments.Add("--additionalprobingpath"); + arguments.Add(packageFolder); + } if(_addAdditionalArguments != null) { diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs index 995620e59..85f82bcd2 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs @@ -136,12 +136,10 @@ namespace Microsoft.DotNet.Cli.Utils var toolPackageFramework = project.DotnetCliToolTargetFramework; - string nugetPackagesRoot; var toolLockFile = GetToolLockFile( toolLibraryRange, toolPackageFramework, - possiblePackageRoots, - out nugetPackagesRoot); + possiblePackageRoots); if (toolLockFile == null) { @@ -174,7 +172,8 @@ namespace Microsoft.DotNet.Cli.Utils toolLockFile, depsFileRoot); - var normalizedNugetPackagesRoot = PathUtility.EnsureNoTrailingDirectorySeparator(nugetPackagesRoot); + var packageFolders = toolLockFile.PackageFolders.Select(p => + PathUtility.EnsureNoTrailingDirectorySeparator(p.Path)); Reporter.Verbose.WriteLine(string.Format( LocalizableStrings.AttemptingToCreateCommandSpec, @@ -185,7 +184,7 @@ namespace Microsoft.DotNet.Cli.Utils commandName, args, _allowedCommandExtensions, - normalizedNugetPackagesRoot, + packageFolders, s_commandResolutionStrategy, depsFilePath, null); @@ -215,19 +214,16 @@ namespace Microsoft.DotNet.Cli.Utils private LockFile GetToolLockFile( SingleProjectInfo toolLibrary, NuGetFramework framework, - IEnumerable possibleNugetPackagesRoot, - out string nugetPackagesRoot) + IEnumerable possibleNugetPackagesRoot) { foreach (var packagesRoot in possibleNugetPackagesRoot) { if (TryGetToolLockFile(toolLibrary, framework, packagesRoot, out LockFile lockFile)) { - nugetPackagesRoot = packagesRoot; return lockFile; } } - nugetPackagesRoot = null; return null; } diff --git a/src/Microsoft.DotNet.Cli.Utils/LocalizableStrings.cs b/src/Microsoft.DotNet.Cli.Utils/LocalizableStrings.cs index 6bbcac0ea..8a3e20014 100644 --- a/src/Microsoft.DotNet.Cli.Utils/LocalizableStrings.cs +++ b/src/Microsoft.DotNet.Cli.Utils/LocalizableStrings.cs @@ -71,6 +71,8 @@ namespace Microsoft.DotNet.Cli.Utils public const string NoExecutableFoundMatchingCommand = "No executable found matching command \"{0}\""; + public const string CommandAssembliesNotFound = "The command executable for \"{0}\" was not found. The project may not have been restored or restore failed - run `dotnet restore`"; + public const string WaitingForDebuggerToAttach = "Waiting for debugger to attach. Press ENTER to continue"; public const string ProcessId = "Process ID: {0}"; diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectToolsCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectToolsCommandResolver.cs index 5876cb5dd..9d9e00539 100644 --- a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectToolsCommandResolver.cs +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenAProjectToolsCommandResolver.cs @@ -335,6 +335,111 @@ namespace Microsoft.DotNet.Tests result.Args.Should().NotContain("--fx-version"); } + [Fact] + public void ItFindsToolsLocatedInTheNuGetFallbackFolder() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var testInstance = TestAssets.Get("AppWithFallbackFolderToolDependency") + .CreateInstance() + .WithSourceFiles(); + var testProjectDirectory = testInstance.Root.FullName; + var fallbackFolder = Path.Combine(testProjectDirectory, "fallbackFolder"); + + PopulateFallbackFolder(testProjectDirectory, fallbackFolder); + + var nugetConfig = UseNuGetConfigWithFallbackFolder(testInstance, fallbackFolder); + + new RestoreCommand() + .WithWorkingDirectory(testProjectDirectory) + .Execute($"--configfile {nugetConfig}") + .Should() + .Pass(); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-fallbackfoldertool", + CommandArguments = null, + ProjectDirectory = testProjectDirectory + }; + + var result = projectToolsCommandResolver.Resolve(commandResolverArguments); + + result.Should().NotBeNull(); + + var commandPath = result.Args.Trim('"'); + commandPath.Should().Contain(Path.Combine( + fallbackFolder, + "dotnet-fallbackfoldertool", + "1.0.0", + "lib", + "netcoreapp2.0", + "dotnet-fallbackfoldertool.dll")); + } + + [Fact] + public void ItXXXWhenTheToolDllIsNotFound() + { + var projectToolsCommandResolver = SetupProjectToolsCommandResolver(); + + var testInstance = TestAssets.Get("AppWithFallbackFolderToolDependency") + .CreateInstance() + .WithSourceFiles(); + var testProjectDirectory = testInstance.Root.FullName; + var fallbackFolder = Path.Combine(testProjectDirectory, "fallbackFolder"); + + PopulateFallbackFolder(testProjectDirectory, fallbackFolder); + + var nugetConfig = UseNuGetConfigWithFallbackFolder(testInstance, fallbackFolder); + + new RestoreCommand() + .WithWorkingDirectory(testProjectDirectory) + .Execute($"--configfile {nugetConfig}") + .Should() + .Pass(); + + Directory.Delete(Path.Combine(fallbackFolder, "dotnet-fallbackfoldertool"), true); + + var commandResolverArguments = new CommandResolverArguments() + { + CommandName = "dotnet-fallbackfoldertool", + CommandArguments = null, + ProjectDirectory = testProjectDirectory + }; + + Action action = () => projectToolsCommandResolver.Resolve(commandResolverArguments); + + action.ShouldThrow().WithMessage( + "The command executable for \"dotnet-fallbackfoldertool\" was not found. The project may not have been restored or restore failed - run `dotnet restore`"); + } + + private void PopulateFallbackFolder(string testProjectDirectory, string fallbackFolder) + { + new RestoreCommand() + .WithWorkingDirectory(testProjectDirectory) + .Execute($"--packages {fallbackFolder}") + .Should() + .Pass(); + + Directory.Delete(Path.Combine(fallbackFolder, ".tools"), true); + } + + private string UseNuGetConfigWithFallbackFolder(TestAssetInstance testInstance, string fallbackFolder) + { + var nugetConfig = testInstance.Root.GetFile("NuGet.Config").FullName; + File.WriteAllText( + nugetConfig, + $@" + + + + + + "); + + return nugetConfig; + } + private ProjectToolsCommandResolver SetupProjectToolsCommandResolver() { Environment.SetEnvironmentVariable(