449 lines
17 KiB
C#
449 lines
17 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 Microsoft.Build.Construction;
|
|
using Microsoft.DotNet.Internal.ProjectModel;
|
|
using Microsoft.DotNet.Internal.ProjectModel.Graph;
|
|
using System.Linq;
|
|
using System.IO;
|
|
using Newtonsoft.Json.Linq;
|
|
using Microsoft.DotNet.Cli.Sln.Internal;
|
|
using Microsoft.DotNet.Tools.Common;
|
|
using NuGet.Frameworks;
|
|
using NuGet.LibraryModel;
|
|
|
|
namespace Microsoft.DotNet.ProjectJsonMigration
|
|
{
|
|
internal class ProjectDependencyFinder
|
|
{
|
|
public IEnumerable<ProjectDependency> ResolveProjectDependencies(
|
|
IEnumerable<ProjectContext> projectContexts,
|
|
IEnumerable<string> preResolvedProjects = null,
|
|
SlnFile solutionFile=null)
|
|
{
|
|
foreach (var projectContext in projectContexts)
|
|
{
|
|
foreach (var projectDependency in
|
|
ResolveProjectDependencies(projectContext, preResolvedProjects, solutionFile))
|
|
{
|
|
yield return projectDependency;
|
|
}
|
|
}
|
|
}
|
|
|
|
public IEnumerable<ProjectDependency> ResolveProjectDependencies(string projectDir,
|
|
string xprojFile = null, SlnFile solutionFile = null)
|
|
{
|
|
var projectContexts = ProjectContext.CreateContextForEachFramework(projectDir);
|
|
xprojFile = xprojFile ?? FindXprojFile(projectDir);
|
|
|
|
ProjectRootElement xproj = null;
|
|
if (xprojFile != null)
|
|
{
|
|
xproj = ProjectRootElement.Open(xprojFile);
|
|
}
|
|
|
|
return ResolveProjectDependencies(
|
|
projectContexts,
|
|
ResolveXProjProjectDependencyNames(xproj),
|
|
solutionFile);
|
|
}
|
|
|
|
public IEnumerable<ProjectDependency> ResolveAllProjectDependenciesForFramework(
|
|
ProjectDependency projectToResolve,
|
|
NuGetFramework framework,
|
|
IEnumerable<string> preResolvedProjects=null,
|
|
SlnFile solutionFile=null)
|
|
{
|
|
var projects = new List<ProjectDependency> { projectToResolve };
|
|
var allDependencies = new HashSet<ProjectDependency>();
|
|
while (projects.Count > 0)
|
|
{
|
|
var project = projects.First();
|
|
projects.Remove(project);
|
|
if (!File.Exists(project.ProjectFilePath))
|
|
{
|
|
MigrationErrorCodes
|
|
.MIGRATE1018(String.Format(LocalizableStrings.MIGRATE1018Arg, project.ProjectFilePath)).Throw();
|
|
}
|
|
|
|
var projectContext =
|
|
ProjectContext.CreateContextForEachFramework(project.ProjectFilePath).FirstOrDefault();
|
|
if(projectContext == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var dependencies = ResolveDirectProjectDependenciesForFramework(
|
|
projectContext.ProjectFile,
|
|
framework,
|
|
preResolvedProjects,
|
|
solutionFile
|
|
);
|
|
allDependencies.UnionWith(dependencies);
|
|
}
|
|
|
|
return allDependencies;
|
|
}
|
|
|
|
public IEnumerable<ProjectDependency> ResolveDirectProjectDependenciesForFramework(
|
|
Project project,
|
|
NuGetFramework framework,
|
|
IEnumerable<string> preResolvedProjects=null,
|
|
SlnFile solutionFile = null)
|
|
{
|
|
preResolvedProjects = preResolvedProjects ?? new HashSet<string>();
|
|
|
|
var possibleProjectDependencies = FindPossibleProjectDependencies(solutionFile, project.ProjectFilePath);
|
|
|
|
var projectDependencies = new List<ProjectDependency>();
|
|
|
|
IEnumerable<ProjectLibraryDependency> projectFileDependenciesForFramework;
|
|
if (framework == null)
|
|
{
|
|
projectFileDependenciesForFramework = project.Dependencies;
|
|
}
|
|
else
|
|
{
|
|
projectFileDependenciesForFramework = project.GetTargetFramework(framework).Dependencies;
|
|
}
|
|
|
|
foreach (var projectFileDependency in
|
|
projectFileDependenciesForFramework.Where(p =>
|
|
p.LibraryRange.TypeConstraint == LibraryDependencyTarget.Project ||
|
|
p.LibraryRange.TypeConstraint == LibraryDependencyTarget.All))
|
|
{
|
|
var dependencyName = projectFileDependency.Name;
|
|
|
|
ProjectDependency projectDependency;
|
|
|
|
if (preResolvedProjects.Contains(dependencyName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!possibleProjectDependencies.TryGetValue(dependencyName, out projectDependency))
|
|
{
|
|
if (projectFileDependency.LibraryRange.TypeConstraint == LibraryDependencyTarget.Project)
|
|
{
|
|
MigrationErrorCodes
|
|
.MIGRATE1014(String.Format(LocalizableStrings.MIGRATE1014Arg, dependencyName)).Throw();
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
projectDependencies.Add(projectDependency);
|
|
}
|
|
|
|
return projectDependencies;
|
|
}
|
|
|
|
internal IEnumerable<ProjectItemElement> ResolveXProjProjectDependencies(ProjectRootElement xproj)
|
|
{
|
|
if (xproj == null)
|
|
{
|
|
MigrationTrace.Instance.WriteLine(String.Format(LocalizableStrings.NoXprojFileGivenError, nameof(ProjectDependencyFinder)));
|
|
return Enumerable.Empty<ProjectItemElement>();
|
|
}
|
|
|
|
return xproj.Items
|
|
.Where(i => i.ItemType == "ProjectReference")
|
|
.Where(p => p.Includes().Any(
|
|
include => string.Equals(Path.GetExtension(include), ".csproj", StringComparison.OrdinalIgnoreCase)));
|
|
}
|
|
|
|
internal string FindXprojFile(string projectDirectory)
|
|
{
|
|
var allXprojFiles = Directory.EnumerateFiles(projectDirectory, "*.xproj", SearchOption.TopDirectoryOnly);
|
|
|
|
if (allXprojFiles.Count() > 1)
|
|
{
|
|
MigrationErrorCodes
|
|
.MIGRATE1017(String.Format(LocalizableStrings.MultipleXprojFilesError, projectDirectory))
|
|
.Throw();
|
|
}
|
|
|
|
return allXprojFiles.FirstOrDefault();
|
|
}
|
|
|
|
private IEnumerable<ProjectDependency> ResolveProjectDependencies(
|
|
ProjectContext projectContext,
|
|
IEnumerable<string> preResolvedProjects=null,
|
|
SlnFile slnFile=null)
|
|
{
|
|
preResolvedProjects = preResolvedProjects ?? new HashSet<string>();
|
|
|
|
var projectExports = projectContext.CreateExporter("_").GetDependencies();
|
|
var possibleProjectDependencies =
|
|
FindPossibleProjectDependencies(slnFile, projectContext.ProjectFile.ProjectFilePath);
|
|
|
|
var projectDependencies = new List<ProjectDependency>();
|
|
foreach (var projectExport in
|
|
projectExports.Where(p => p.Library.Identity.Type == LibraryType.Project))
|
|
{
|
|
var projectExportName = projectExport.Library.Identity.Name;
|
|
ProjectDependency projectDependency;
|
|
|
|
if (preResolvedProjects.Contains(projectExportName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!possibleProjectDependencies.TryGetValue(projectExportName, out projectDependency))
|
|
{
|
|
if (projectExport.Library.Identity.Type.Equals(LibraryType.Project))
|
|
{
|
|
MigrationErrorCodes
|
|
.MIGRATE1014(String.Format(LocalizableStrings.MIGRATE1014Arg, projectExportName)).Throw();
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
projectDependencies.Add(projectDependency);
|
|
}
|
|
|
|
return projectDependencies;
|
|
}
|
|
|
|
private IEnumerable<string> ResolveXProjProjectDependencyNames(ProjectRootElement xproj)
|
|
{
|
|
var xprojDependencies = ResolveXProjProjectDependencies(xproj).SelectMany(r => r.Includes());
|
|
return new HashSet<string>(xprojDependencies.Select(p => Path.GetFileNameWithoutExtension(
|
|
PathUtility.GetPathWithDirectorySeparator(p))));
|
|
}
|
|
|
|
private Dictionary<string, ProjectDependency> FindPossibleProjectDependencies(
|
|
SlnFile slnFile,
|
|
string projectJsonFilePath)
|
|
{
|
|
var projectRootDirectory = GetRootFromProjectJson(projectJsonFilePath);
|
|
|
|
var projectSearchPaths = new List<string>();
|
|
projectSearchPaths.Add(projectRootDirectory);
|
|
|
|
var globalPaths = GetGlobalPaths(projectRootDirectory);
|
|
projectSearchPaths = projectSearchPaths.Union(globalPaths).ToList();
|
|
|
|
var solutionPaths = GetSolutionPaths(slnFile);
|
|
projectSearchPaths = projectSearchPaths.Union(solutionPaths).ToList();
|
|
|
|
var projects = new Dictionary<string, ProjectDependency>(StringComparer.Ordinal);
|
|
|
|
foreach (var project in GetPotentialProjects(projectSearchPaths))
|
|
{
|
|
if (projects.ContainsKey(project.Name))
|
|
{
|
|
// Remove the existing project if it doesn't have project.json
|
|
// project.json isn't checked until the project is resolved, but here
|
|
// we need to check it up front.
|
|
var otherProject = projects[project.Name];
|
|
|
|
if (project.ProjectFilePath != otherProject.ProjectFilePath)
|
|
{
|
|
var projectExists = File.Exists(project.ProjectFilePath);
|
|
var otherExists = File.Exists(otherProject.ProjectFilePath);
|
|
|
|
if (projectExists != otherExists
|
|
&& projectExists
|
|
&& !otherExists)
|
|
{
|
|
// the project currently in the cache does not exist, but this one does
|
|
// remove the old project and add the current one
|
|
projects[project.Name] = project;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
projects.Add(project.Name, project);
|
|
}
|
|
}
|
|
|
|
return projects;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the parent directory of the project.json.
|
|
/// </summary>
|
|
/// <param name="projectJsonPath">Full path to project.json.</param>
|
|
private static string GetRootFromProjectJson(string projectJsonPath)
|
|
{
|
|
if (!string.IsNullOrEmpty(projectJsonPath))
|
|
{
|
|
var file = new FileInfo(projectJsonPath);
|
|
|
|
// If for some reason we are at the root of the drive this will be null
|
|
// Use the file directory instead.
|
|
if (file.Directory.Parent == null)
|
|
{
|
|
return file.Directory.FullName;
|
|
}
|
|
else
|
|
{
|
|
return file.Directory.Parent.FullName;
|
|
}
|
|
}
|
|
|
|
return projectJsonPath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the list of potential projects from the search paths.
|
|
/// </summary>
|
|
private static List<ProjectDependency> GetPotentialProjects(
|
|
IEnumerable<string> searchPaths)
|
|
{
|
|
var projects = new List<ProjectDependency>();
|
|
|
|
// Resolve all of the potential projects
|
|
foreach (var searchPath in searchPaths)
|
|
{
|
|
var directory = new DirectoryInfo(searchPath);
|
|
|
|
if (!directory.Exists)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var projectDirectory in
|
|
Enumerable.Repeat(directory, 1).Union(directory.GetDirectories()))
|
|
{
|
|
// Create the path to the project.json file.
|
|
var projectFilePath = Path.Combine(projectDirectory.FullName, "project.json");
|
|
|
|
// We INTENTIONALLY do not do an exists check here because it requires disk I/O
|
|
// Instead, we'll do an exists check when we try to resolve
|
|
|
|
// Check if we've already added this, just in case it was pre-loaded into the cache
|
|
var project = new ProjectDependency(
|
|
projectDirectory.Name,
|
|
projectFilePath);
|
|
|
|
projects.Add(project);
|
|
}
|
|
}
|
|
|
|
return projects;
|
|
}
|
|
|
|
internal static List<string> GetGlobalPaths(string rootPath)
|
|
{
|
|
var paths = new List<string>();
|
|
|
|
var globalJsonRoot = ResolveRootDirectory(rootPath);
|
|
|
|
GlobalSettings globalSettings;
|
|
if (GlobalSettings.TryGetGlobalSettings(globalJsonRoot, out globalSettings))
|
|
{
|
|
foreach (var sourcePath in globalSettings.ProjectPaths)
|
|
{
|
|
var path = Path.GetFullPath(Path.Combine(globalJsonRoot, sourcePath));
|
|
|
|
paths.Add(path);
|
|
}
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
|
|
internal static List<string> GetSolutionPaths(SlnFile solutionFile)
|
|
{
|
|
return (solutionFile == null)
|
|
? new List<string>()
|
|
: new List<string>(solutionFile.Projects.Select(p =>
|
|
Path.Combine(solutionFile.BaseDirectory, Path.GetDirectoryName(p.FilePath))));
|
|
}
|
|
|
|
private static string ResolveRootDirectory(string projectPath)
|
|
{
|
|
var di = new DirectoryInfo(projectPath);
|
|
|
|
while (di.Parent != null)
|
|
{
|
|
var globalJsonPath = Path.Combine(di.FullName, GlobalSettings.GlobalFileName);
|
|
|
|
if (File.Exists(globalJsonPath))
|
|
{
|
|
return di.FullName;
|
|
}
|
|
|
|
di = di.Parent;
|
|
}
|
|
|
|
// If we don't find any files then make the project folder the root
|
|
return projectPath;
|
|
}
|
|
|
|
private class GlobalSettings
|
|
{
|
|
public const string GlobalFileName = "global.json";
|
|
public IList<string> ProjectPaths { get; private set; }
|
|
public string PackagesPath { get; private set; }
|
|
public string FilePath { get; private set; }
|
|
|
|
public string RootPath
|
|
{
|
|
get { return Path.GetDirectoryName(FilePath); }
|
|
}
|
|
|
|
public static bool TryGetGlobalSettings(string path, out GlobalSettings globalSettings)
|
|
{
|
|
globalSettings = null;
|
|
|
|
string globalJsonPath = null;
|
|
|
|
if (Path.GetFileName(path) == GlobalFileName)
|
|
{
|
|
globalJsonPath = path;
|
|
path = Path.GetDirectoryName(path);
|
|
}
|
|
else if (!HasGlobalFile(path))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
globalJsonPath = Path.Combine(path, GlobalFileName);
|
|
}
|
|
|
|
globalSettings = new GlobalSettings();
|
|
|
|
try
|
|
{
|
|
var json = File.ReadAllText(globalJsonPath);
|
|
|
|
JObject settings = JObject.Parse(json);
|
|
|
|
var projects = settings["projects"];
|
|
var dependencies = settings["dependencies"] as JObject;
|
|
|
|
globalSettings.ProjectPaths = projects == null ? new string[] { } :
|
|
projects.Select(a => a.Value<string>()).ToArray();
|
|
globalSettings.PackagesPath = settings.Value<string>("packages");
|
|
globalSettings.FilePath = globalJsonPath;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw FileFormatException.Create(ex, globalJsonPath);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static bool HasGlobalFile(string path)
|
|
{
|
|
var projectPath = Path.Combine(path, GlobalFileName);
|
|
|
|
return File.Exists(projectPath);
|
|
}
|
|
}
|
|
}
|
|
}
|