438 lines
16 KiB
C#
438 lines
16 KiB
C#
// 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 System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.DotNet.Tools.Common;
|
|
using Microsoft.Extensions.DependencyModel;
|
|
using NuGet.Configuration;
|
|
using NuGet.Frameworks;
|
|
using NuGet.ProjectModel;
|
|
using NuGet.Versioning;
|
|
using ConcurrencyUtilities = NuGet.Common.ConcurrencyUtilities;
|
|
|
|
namespace Microsoft.DotNet.Cli.Utils
|
|
{
|
|
public class ProjectToolsCommandResolver : ICommandResolver
|
|
{
|
|
private const string ProjectToolsCommandResolverName = "projecttoolscommandresolver";
|
|
|
|
private static readonly CommandResolutionStrategy s_commandResolutionStrategy =
|
|
CommandResolutionStrategy.ProjectToolsPackage;
|
|
|
|
private List<string> _allowedCommandExtensions;
|
|
private IPackagedCommandSpecFactory _packagedCommandSpecFactory;
|
|
|
|
private IEnvironmentProvider _environment;
|
|
|
|
public ProjectToolsCommandResolver(
|
|
IPackagedCommandSpecFactory packagedCommandSpecFactory,
|
|
IEnvironmentProvider environment)
|
|
{
|
|
_packagedCommandSpecFactory = packagedCommandSpecFactory;
|
|
_environment = environment;
|
|
|
|
_allowedCommandExtensions = new List<string>()
|
|
{
|
|
FileNameSuffixes.DotNet.DynamicLib
|
|
};
|
|
}
|
|
|
|
public CommandSpec Resolve(CommandResolverArguments commandResolverArguments)
|
|
{
|
|
if (commandResolverArguments.CommandName == null
|
|
|| commandResolverArguments.ProjectDirectory == null)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.InvalidCommandResolverArguments,
|
|
ProjectToolsCommandResolverName));
|
|
|
|
return null;
|
|
}
|
|
|
|
return ResolveFromProjectTools(commandResolverArguments);
|
|
}
|
|
|
|
private CommandSpec ResolveFromProjectTools(CommandResolverArguments commandResolverArguments)
|
|
{
|
|
var projectFactory = new ProjectFactory(_environment);
|
|
|
|
var project = projectFactory.GetProject(
|
|
commandResolverArguments.ProjectDirectory,
|
|
commandResolverArguments.Framework,
|
|
commandResolverArguments.Configuration,
|
|
commandResolverArguments.BuildBasePath,
|
|
commandResolverArguments.OutputPath);
|
|
|
|
if (project == null)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.DidNotFindProject, ProjectToolsCommandResolverName));
|
|
|
|
return null;
|
|
}
|
|
|
|
var tools = project.GetTools();
|
|
|
|
return ResolveCommandSpecFromAllToolLibraries(
|
|
tools,
|
|
commandResolverArguments.CommandName,
|
|
commandResolverArguments.CommandArguments.OrEmptyIfNull(),
|
|
project);
|
|
}
|
|
|
|
private CommandSpec ResolveCommandSpecFromAllToolLibraries(
|
|
IEnumerable<SingleProjectInfo> toolsLibraries,
|
|
string commandName,
|
|
IEnumerable<string> args,
|
|
IProject project)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.ResolvingCommandSpec,
|
|
ProjectToolsCommandResolverName,
|
|
toolsLibraries.Count()));
|
|
|
|
foreach (var toolLibrary in toolsLibraries)
|
|
{
|
|
var commandSpec = ResolveCommandSpecFromToolLibrary(
|
|
toolLibrary,
|
|
commandName,
|
|
args,
|
|
project);
|
|
|
|
if (commandSpec != null)
|
|
{
|
|
return commandSpec;
|
|
}
|
|
}
|
|
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.FailedToResolveCommandSpec,
|
|
ProjectToolsCommandResolverName));
|
|
|
|
return null;
|
|
}
|
|
|
|
private CommandSpec ResolveCommandSpecFromToolLibrary(
|
|
SingleProjectInfo toolLibraryRange,
|
|
string commandName,
|
|
IEnumerable<string> args,
|
|
IProject project)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.AttemptingToResolveCommandSpec,
|
|
ProjectToolsCommandResolverName,
|
|
toolLibraryRange.Name));
|
|
|
|
var possiblePackageRoots = GetPossiblePackageRoots(project).ToList();
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.NuGetPackagesRoot,
|
|
ProjectToolsCommandResolverName,
|
|
string.Join(Environment.NewLine, possiblePackageRoots.Select((p) => $"- {p}"))));
|
|
|
|
List<NuGetFramework> toolFrameworksToCheck = new List<NuGetFramework>();
|
|
toolFrameworksToCheck.Add(project.DotnetCliToolTargetFramework);
|
|
|
|
// NuGet restore in Visual Studio may restore for netcoreapp1.0. So if that happens, fall back to
|
|
// looking for a netcoreapp1.0 or netcoreapp1.1 tool restore.
|
|
if (project.DotnetCliToolTargetFramework.Framework == FrameworkConstants.FrameworkIdentifiers.NetCoreApp &&
|
|
project.DotnetCliToolTargetFramework.Version >= new Version(2, 0, 0))
|
|
{
|
|
toolFrameworksToCheck.Add(NuGetFramework.Parse("netcoreapp1.1"));
|
|
toolFrameworksToCheck.Add(NuGetFramework.Parse("netcoreapp1.0"));
|
|
}
|
|
|
|
|
|
LockFile toolLockFile = null;
|
|
NuGetFramework toolTargetFramework = null; ;
|
|
|
|
foreach (var toolFramework in toolFrameworksToCheck)
|
|
{
|
|
toolLockFile = GetToolLockFile(
|
|
toolLibraryRange,
|
|
toolFramework,
|
|
possiblePackageRoots);
|
|
|
|
if (toolLockFile != null)
|
|
{
|
|
toolTargetFramework = toolFramework;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (toolLockFile == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.FoundToolLockFile,
|
|
ProjectToolsCommandResolverName,
|
|
toolLockFile.Path));
|
|
|
|
var toolLibrary = toolLockFile.Targets
|
|
.FirstOrDefault(t => toolTargetFramework == t.TargetFramework)
|
|
?.Libraries.FirstOrDefault(
|
|
l => StringComparer.OrdinalIgnoreCase.Equals(l.Name, toolLibraryRange.Name));
|
|
if (toolLibrary == null)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.LibraryNotFoundInLockFile,
|
|
ProjectToolsCommandResolverName));
|
|
|
|
return null;
|
|
}
|
|
|
|
var depsFileRoot = Path.GetDirectoryName(toolLockFile.Path);
|
|
|
|
var depsFilePath = GetToolDepsFilePath(
|
|
toolLibraryRange,
|
|
toolTargetFramework,
|
|
toolLockFile,
|
|
depsFileRoot,
|
|
project.ToolDepsJsonGeneratorProject);
|
|
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.AttemptingToCreateCommandSpec,
|
|
ProjectToolsCommandResolverName));
|
|
|
|
var commandSpec = _packagedCommandSpecFactory.CreateCommandSpecFromLibrary(
|
|
toolLibrary,
|
|
commandName,
|
|
args,
|
|
_allowedCommandExtensions,
|
|
toolLockFile,
|
|
s_commandResolutionStrategy,
|
|
depsFilePath,
|
|
null);
|
|
|
|
if (commandSpec == null)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.CommandSpecIsNull,
|
|
ProjectToolsCommandResolverName));
|
|
}
|
|
|
|
commandSpec?.AddEnvironmentVariablesFromProject(project);
|
|
|
|
return commandSpec;
|
|
}
|
|
|
|
private IEnumerable<string> GetPossiblePackageRoots(IProject project)
|
|
{
|
|
if (project.TryGetLockFile(out LockFile lockFile))
|
|
{
|
|
return lockFile.PackageFolders.Select((packageFolder) => packageFolder.Path);
|
|
}
|
|
|
|
return Enumerable.Empty<string>();
|
|
}
|
|
|
|
private LockFile GetToolLockFile(
|
|
SingleProjectInfo toolLibrary,
|
|
NuGetFramework framework,
|
|
IEnumerable<string> possibleNugetPackagesRoot)
|
|
{
|
|
foreach (var packagesRoot in possibleNugetPackagesRoot)
|
|
{
|
|
if (TryGetToolLockFile(toolLibrary, framework, packagesRoot, out LockFile lockFile))
|
|
{
|
|
return lockFile;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
private static async Task<bool> FileExistsWithLock(string path)
|
|
{
|
|
return await ConcurrencyUtilities.ExecuteWithFileLockedAsync(
|
|
path,
|
|
lockedToken => Task.FromResult(File.Exists(path)),
|
|
CancellationToken.None);
|
|
}
|
|
|
|
private bool TryGetToolLockFile(
|
|
SingleProjectInfo toolLibrary,
|
|
NuGetFramework framework,
|
|
string nugetPackagesRoot,
|
|
out LockFile lockFile)
|
|
{
|
|
lockFile = null;
|
|
var lockFilePath = GetToolLockFilePath(toolLibrary, framework, nugetPackagesRoot);
|
|
|
|
if (!FileExistsWithLock(lockFilePath).Result)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
lockFile = new LockFileFormat()
|
|
.ReadWithLock(lockFilePath)
|
|
.Result;
|
|
}
|
|
catch (FileFormatException ex)
|
|
{
|
|
throw ex;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private string GetToolLockFilePath(
|
|
SingleProjectInfo toolLibrary,
|
|
NuGetFramework framework,
|
|
string nugetPackagesRoot)
|
|
{
|
|
var toolPathCalculator = new ToolPathCalculator(nugetPackagesRoot);
|
|
|
|
return toolPathCalculator.GetBestLockFilePath(
|
|
toolLibrary.Name,
|
|
VersionRange.Parse(toolLibrary.Version),
|
|
framework);
|
|
}
|
|
|
|
private string GetToolDepsFilePath(
|
|
SingleProjectInfo toolLibrary,
|
|
NuGetFramework framework,
|
|
LockFile toolLockFile,
|
|
string depsPathRoot,
|
|
string toolDepsJsonGeneratorProject)
|
|
{
|
|
var depsJsonPath = Path.Combine(
|
|
depsPathRoot,
|
|
toolLibrary.Name + FileNameSuffixes.DepsJson);
|
|
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.ExpectDepsJsonAt,
|
|
ProjectToolsCommandResolverName,
|
|
depsJsonPath));
|
|
|
|
EnsureToolJsonDepsFileExists(toolLockFile, framework, depsJsonPath, toolLibrary, toolDepsJsonGeneratorProject);
|
|
|
|
return depsJsonPath;
|
|
}
|
|
|
|
private void EnsureToolJsonDepsFileExists(
|
|
LockFile toolLockFile,
|
|
NuGetFramework framework,
|
|
string depsPath,
|
|
SingleProjectInfo toolLibrary,
|
|
string toolDepsJsonGeneratorProject)
|
|
{
|
|
if (!File.Exists(depsPath))
|
|
{
|
|
GenerateDepsJsonFile(toolLockFile, framework, depsPath, toolLibrary, toolDepsJsonGeneratorProject);
|
|
}
|
|
}
|
|
|
|
internal void GenerateDepsJsonFile(
|
|
LockFile toolLockFile,
|
|
NuGetFramework framework,
|
|
string depsPath,
|
|
SingleProjectInfo toolLibrary,
|
|
string toolDepsJsonGeneratorProject)
|
|
{
|
|
if (string.IsNullOrEmpty(toolDepsJsonGeneratorProject) ||
|
|
!File.Exists(toolDepsJsonGeneratorProject))
|
|
{
|
|
throw new GracefulException(LocalizableStrings.DepsJsonGeneratorProjectNotSet);
|
|
}
|
|
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.GeneratingDepsJson,
|
|
depsPath));
|
|
|
|
var tempDepsFile = Path.GetTempFileName();
|
|
|
|
var args = new List<string>();
|
|
|
|
args.Add(toolDepsJsonGeneratorProject);
|
|
args.Add($"/p:ProjectAssetsFile=\"{toolLockFile.Path}\"");
|
|
args.Add($"/p:ToolName={toolLibrary.Name}");
|
|
args.Add($"/p:ProjectDepsFilePath={tempDepsFile}");
|
|
|
|
var toolTargetFramework = toolLockFile.Targets.First().TargetFramework.GetShortFolderName();
|
|
args.Add($"/p:TargetFramework={toolTargetFramework}");
|
|
|
|
|
|
// Look for the .props file in the Microsoft.NETCore.App package, until NuGet
|
|
// generates .props and .targets files for tool restores (https://github.com/NuGet/Home/issues/5037)
|
|
var platformLibrary = toolLockFile.Targets
|
|
.FirstOrDefault(t => framework == t.TargetFramework)
|
|
?.GetPlatformLibrary();
|
|
|
|
if (platformLibrary != null)
|
|
{
|
|
string buildRelativePath = platformLibrary.Build.FirstOrDefault()?.Path;
|
|
|
|
var platformLibraryPath = toolLockFile.GetPackageDirectory(platformLibrary);
|
|
|
|
if (platformLibraryPath != null && buildRelativePath != null)
|
|
{
|
|
// Get rid of "_._" filename
|
|
buildRelativePath = Path.GetDirectoryName(buildRelativePath);
|
|
|
|
string platformLibraryBuildFolderPath = Path.Combine(platformLibraryPath, buildRelativePath);
|
|
var platformLibraryPropsFile = Directory.GetFiles(platformLibraryBuildFolderPath, "*.props").FirstOrDefault();
|
|
|
|
if (platformLibraryPropsFile != null)
|
|
{
|
|
args.Add($"/p:AdditionalImport={platformLibraryPropsFile}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete temporary file created by Path.GetTempFileName(), otherwise the GenerateBuildDependencyFile target
|
|
// will think the deps file is up-to-date and skip executing
|
|
File.Delete(tempDepsFile);
|
|
|
|
var msBuildExePath = _environment.GetEnvironmentVariable(Constants.MSBUILD_EXE_PATH);
|
|
|
|
msBuildExePath = string.IsNullOrEmpty(msBuildExePath) ?
|
|
Path.Combine(AppContext.BaseDirectory, "MSBuild.dll") :
|
|
msBuildExePath;
|
|
|
|
var result = new MSBuildForwardingAppWithoutLogging(args, msBuildExePath)
|
|
.GetProcessStartInfo()
|
|
.ExecuteAndCaptureOutput(out string stdOut, out string stdErr);
|
|
|
|
if (result != 0)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.UnableToGenerateDepsJson,
|
|
stdOut + Environment.NewLine + stdErr));
|
|
|
|
throw new GracefulException(string.Format(LocalizableStrings.UnableToGenerateDepsJson, toolDepsJsonGeneratorProject));
|
|
}
|
|
|
|
try
|
|
{
|
|
File.Move(tempDepsFile, depsPath);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.UnableToGenerateDepsJson,
|
|
e.Message));
|
|
|
|
try
|
|
{
|
|
File.Delete(tempDepsFile);
|
|
}
|
|
catch (Exception e2)
|
|
{
|
|
Reporter.Verbose.WriteLine(string.Format(
|
|
LocalizableStrings.UnableToDeleteTemporaryDepsJson,
|
|
e2.Message));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|