using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Semmle.Util;
using Semmle.Util.Logging;
namespace Semmle.Extraction.CSharp.DependencyFetching
{
///
/// Main implementation of the build analysis.
///
public sealed class DependencyManager : IDisposable
{
private readonly AssemblyCache assemblyCache;
private readonly ProgressMonitor progressMonitor;
private readonly IDictionary usedReferences = new ConcurrentDictionary();
private readonly IDictionary sources = new ConcurrentDictionary();
private readonly IDictionary unresolvedReferences = new ConcurrentDictionary();
private int failedProjects;
private int succeededProjects;
private readonly List allSources;
private int conflictedReferences = 0;
private readonly IDependencyOptions options;
private readonly DirectoryInfo sourceDir;
private readonly DotNet dotnet;
private readonly FileContent fileContent;
private readonly TemporaryDirectory packageDirectory;
private TemporaryDirectory? razorWorkingDirectory;
private readonly Git git;
///
/// Performs C# dependency fetching.
///
/// Dependency fetching options
/// Logger for dependency fetching progress.
public DependencyManager(string srcDir, IDependencyOptions options, ILogger logger)
{
var startTime = DateTime.Now;
this.options = options;
this.progressMonitor = new ProgressMonitor(logger);
this.sourceDir = new DirectoryInfo(srcDir);
try
{
this.dotnet = new DotNet(options, progressMonitor);
}
catch
{
progressMonitor.MissingDotNet();
throw;
}
this.progressMonitor.FindingFiles(srcDir);
packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName));
var allFiles = GetAllFiles().ToList();
var smallFiles = allFiles.SelectSmallFiles(progressMonitor).SelectFileNames();
this.fileContent = new FileContent(progressMonitor, smallFiles);
this.allSources = allFiles.SelectFileNamesByExtension(".cs").ToList();
var allProjects = allFiles.SelectFileNamesByExtension(".csproj");
var solutions = options.SolutionFile is not null
? new[] { options.SolutionFile }
: allFiles.SelectFileNamesByExtension(".sln");
// If DLL reference paths are specified on the command-line, use those to discover
// assemblies. Otherwise (the default), query the git CLI to determine which DLL files
// are tracked as part of the repository.
this.git = new Git(this.progressMonitor);
var dllDirNames = options.DllDirs.Count == 0 ? this.git.ListFiles("*.dll") : options.DllDirs.Select(Path.GetFullPath).ToList();
// Find DLLs in the .Net / Asp.Net Framework
if (options.ScanNetFrameworkDlls)
{
var runtime = new Runtime(dotnet);
var runtimeLocation = runtime.GetRuntime(options.UseSelfContainedDotnet);
progressMonitor.LogInfo($".NET runtime location selected: {runtimeLocation}");
dllDirNames.Add(runtimeLocation);
if (fileContent.UseAspNetDlls && runtime.GetAspRuntime() is string aspRuntime)
{
progressMonitor.LogInfo($"ASP.NET runtime location selected: {aspRuntime}");
dllDirNames.Add(aspRuntime);
}
}
if (options.UseNuGet)
{
dllDirNames.Add(packageDirectory.DirInfo.FullName);
try
{
var nuget = new NugetPackages(sourceDir.FullName, packageDirectory, progressMonitor);
nuget.InstallPackages();
}
catch (FileNotFoundException)
{
progressMonitor.MissingNuGet();
}
var restoredProjects = RestoreSolutions(solutions);
var projects = allProjects.Except(restoredProjects);
RestoreProjects(projects);
DownloadMissingPackages(allFiles);
}
assemblyCache = new AssemblyCache(dllDirNames, progressMonitor);
AnalyseSolutions(solutions);
foreach (var filename in assemblyCache.AllAssemblies.Select(a => a.Filename))
{
UseReference(filename);
}
ResolveConflicts();
// Output the findings
foreach (var r in usedReferences.Keys.OrderBy(r => r))
{
progressMonitor.ResolvedReference(r);
}
foreach (var r in unresolvedReferences.OrderBy(r => r.Key))
{
progressMonitor.UnresolvedReference(r.Key, r.Value);
}
var webViewExtractionOption = Environment.GetEnvironmentVariable("CODEQL_EXTRACTOR_CSHARP_STANDALONE_EXTRACT_WEB_VIEWS");
if (bool.TryParse(webViewExtractionOption, out var shouldExtractWebViews) &&
shouldExtractWebViews)
{
GenerateSourceFilesFromWebViews(allFiles);
}
progressMonitor.Summary(
AllSourceFiles.Count(),
ProjectSourceFiles.Count(),
MissingSourceFiles.Count(),
ReferenceFiles.Count(),
UnresolvedReferences.Count(),
conflictedReferences,
succeededProjects + failedProjects,
failedProjects,
DateTime.Now - startTime);
}
private void GenerateSourceFilesFromWebViews(List allFiles)
{
progressMonitor.LogInfo($"Generating source files from cshtml and razor files.");
var views = allFiles.SelectFileNamesByExtension(".cshtml", ".razor").ToArray();
if (views.Length > 0)
{
progressMonitor.LogInfo($"Found {views.Length} cshtml and razor files.");
var sdk = new Sdk(dotnet).GetNewestSdk();
if (sdk != null)
{
try
{
var razor = new Razor(sdk, dotnet, progressMonitor);
razorWorkingDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName, "razor"));
var generatedFiles = razor.GenerateFiles(views, usedReferences.Keys, razorWorkingDirectory.ToString());
this.allSources.AddRange(generatedFiles);
}
catch (Exception ex)
{
// It's okay, we tried our best to generate source files from cshtml files.
progressMonitor.LogInfo($"Failed to generate source files from cshtml files: {ex.Message}");
}
}
}
}
public DependencyManager(string srcDir) : this(srcDir, DependencyOptions.Default, new ConsoleLogger(Verbosity.Info)) { }
private IEnumerable GetAllFiles() =>
sourceDir.GetFiles("*.*", new EnumerationOptions { RecurseSubdirectories = true })
.Where(d => d.Extension != ".dll" && !options.ExcludesFile(d.FullName));
///
/// Computes a unique temp directory for the packages associated
/// with this source tree. Use a SHA1 of the directory name.
///
/// The full path of the temp directory.
private static string ComputeTempDirectory(string srcDir, string subfolderName = "packages")
{
var bytes = Encoding.Unicode.GetBytes(srcDir);
var sha = SHA1.HashData(bytes);
var sb = new StringBuilder();
foreach (var b in sha.Take(8))
sb.AppendFormat("{0:x2}", b);
return Path.Combine(Path.GetTempPath(), "GitHub", subfolderName, sb.ToString());
}
///
/// Resolves conflicts between all of the resolved references.
/// If the same assembly name is duplicated with different versions,
/// resolve to the higher version number.
///
private void ResolveConflicts()
{
var sortedReferences = new List();
foreach (var usedReference in usedReferences)
{
try
{
var assemblyInfo = assemblyCache.GetAssemblyInfo(usedReference.Key);
sortedReferences.Add(assemblyInfo);
}
catch (AssemblyLoadException)
{
progressMonitor.Log(Util.Logging.Severity.Warning, $"Could not load assembly information from {usedReference.Key}");
}
}
var emptyVersion = new Version(0, 0);
sortedReferences = sortedReferences.OrderBy(r => r.NetCoreVersion ?? emptyVersion).ThenBy(r => r.Version ?? emptyVersion).ToList();
var finalAssemblyList = new Dictionary();
// Pick the highest version for each assembly name
foreach (var r in sortedReferences)
{
finalAssemblyList[r.Name] = r;
}
// Update the used references list
usedReferences.Clear();
foreach (var r in finalAssemblyList.Select(r => r.Value.Filename))
{
UseReference(r);
}
// Report the results
foreach (var r in sortedReferences)
{
var resolvedInfo = finalAssemblyList[r.Name];
if (resolvedInfo.Version != r.Version || resolvedInfo.NetCoreVersion != r.NetCoreVersion)
{
progressMonitor.ResolvedConflict(r.Id, resolvedInfo.Id + resolvedInfo.NetCoreVersion is null ? "" : $" (.NET Core {resolvedInfo.NetCoreVersion})");
++conflictedReferences;
}
}
}
///
/// Store that a particular reference file is used.
///
/// The filename of the reference.
private void UseReference(string reference) => usedReferences[reference] = true;
///
/// Store that a particular source file is used (by a project file).
///
/// The source file.
private void UseSource(FileInfo sourceFile) => sources[sourceFile.FullName] = sourceFile.Exists;
///
/// The list of resolved reference files.
///
public IEnumerable ReferenceFiles => usedReferences.Keys;
///
/// The list of source files used in projects.
///
public IEnumerable ProjectSourceFiles => sources.Where(s => s.Value).Select(s => s.Key);
///
/// All of the source files in the source directory.
///
public IEnumerable AllSourceFiles => allSources;
///
/// List of assembly IDs which couldn't be resolved.
///
public IEnumerable UnresolvedReferences => unresolvedReferences.Select(r => r.Key);
///
/// List of source files which were mentioned in project files but
/// do not exist on the file system.
///
public IEnumerable MissingSourceFiles => sources.Where(s => !s.Value).Select(s => s.Key);
///
/// Record that a particular reference couldn't be resolved.
/// Note that this records at most one project file per missing reference.
///
/// The assembly ID.
/// The project file making the reference.
private void UnresolvedReference(string id, string projectFile) => unresolvedReferences[id] = projectFile;
///
/// Reads all the source files and references from the given list of projects.
///
/// The list of projects to analyse.
private void AnalyseProjectFiles(IEnumerable projectFiles)
{
foreach (var proj in projectFiles)
{
AnalyseProject(proj);
}
}
private void AnalyseProject(FileInfo project)
{
if (!project.Exists)
{
progressMonitor.MissingProject(project.FullName);
return;
}
try
{
var csProj = new CsProjFile(project);
foreach (var @ref in csProj.References)
{
try
{
var resolved = assemblyCache.ResolveReference(@ref);
UseReference(resolved.Filename);
}
catch (AssemblyLoadException)
{
UnresolvedReference(@ref, project.FullName);
}
}
foreach (var src in csProj.Sources)
{
// Make a note of which source files the projects use.
// This information doesn't affect the build but is dumped
// as diagnostic output.
UseSource(new FileInfo(src));
}
++succeededProjects;
}
catch (Exception ex) // lgtm[cs/catch-of-all-exceptions]
{
++failedProjects;
progressMonitor.FailedProjectFile(project.FullName, ex.Message);
}
}
private bool RestoreProject(string project, out string stdout, string? pathToNugetConfig = null) =>
dotnet.RestoreProjectToDirectory(project, packageDirectory.DirInfo.FullName, out stdout, pathToNugetConfig);
private bool RestoreSolution(string solution, out IEnumerable projects) =>
dotnet.RestoreSolutionToDirectory(solution, packageDirectory.DirInfo.FullName, out projects);
///
/// Executes `dotnet restore` on all solution files in solutions.
/// As opposed to RestoreProjects this is not run in parallel using PLINQ
/// as `dotnet restore` on a solution already uses multiple threads for restoring
/// the projects (this can be disabled with the `--disable-parallel` flag).
/// Returns a list of projects that are up to date with respect to restore.
///
/// A list of paths to solution files.
private IEnumerable RestoreSolutions(IEnumerable solutions) =>
solutions.SelectMany(solution =>
{
RestoreSolution(solution, out var restoredProjects);
return restoredProjects;
});
///
/// Executes `dotnet restore` on all projects in projects.
/// This is done in parallel for performance reasons.
/// To ensure that output is not interleaved, the output of each
/// restore is collected and printed.
///
/// A list of paths to project files.
private void RestoreProjects(IEnumerable projects)
{
var stdoutLines = projects
.AsParallel()
.WithDegreeOfParallelism(options.Threads)
.Select(project =>
{
RestoreProject(project, out var stdout);
return stdout;
})
.ToList();
foreach (var line in stdoutLines)
{
Console.WriteLine(line);
}
}
private void DownloadMissingPackages(List allFiles)
{
var nugetConfigs = allFiles.SelectFileNamesByName("nuget.config").ToArray();
string? nugetConfig = null;
if (nugetConfigs.Length > 1)
{
progressMonitor.MultipleNugetConfig(nugetConfigs);
nugetConfig = allFiles
.SelectRootFiles(sourceDir)
.SelectFileNamesByName("nuget.config")
.FirstOrDefault();
if (nugetConfig == null)
{
progressMonitor.NoTopLevelNugetConfig();
}
}
else
{
nugetConfig = nugetConfigs.FirstOrDefault();
}
var alreadyDownloadedPackages = Directory.GetDirectories(packageDirectory.DirInfo.FullName)
.Select(d => Path.GetFileName(d).ToLowerInvariant());
var notYetDownloadedPackages = fileContent.AllPackages.Except(alreadyDownloadedPackages);
foreach (var package in notYetDownloadedPackages)
{
progressMonitor.NugetInstall(package);
using var tempDir = new TemporaryDirectory(ComputeTempDirectory(package));
var success = dotnet.New(tempDir.DirInfo.FullName);
if (!success)
{
continue;
}
success = dotnet.AddPackage(tempDir.DirInfo.FullName, package);
if (!success)
{
continue;
}
success = RestoreProject(tempDir.DirInfo.FullName, out var stdout, nugetConfig);
Console.WriteLine(stdout);
// TODO: the restore might fail, we could retry with a prerelease (*-* instead of *) version of the package.
if (!success)
{
progressMonitor.FailedToRestoreNugetPackage(package);
}
}
}
private void AnalyseSolutions(IEnumerable solutions)
{
Parallel.ForEach(solutions, new ParallelOptions { MaxDegreeOfParallelism = options.Threads }, solutionFile =>
{
try
{
var sln = new SolutionFile(solutionFile);
progressMonitor.AnalysingSolution(solutionFile);
AnalyseProjectFiles(sln.Projects.Select(p => new FileInfo(p)).Where(p => p.Exists));
}
catch (Microsoft.Build.Exceptions.InvalidProjectFileException ex)
{
progressMonitor.FailedProjectFile(solutionFile, ex.BaseMessage);
}
});
}
public void Dispose()
{
packageDirectory?.Dispose();
razorWorkingDirectory?.Dispose();
}
}
}