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 ILogger logger; // Only used as a set, but ConcurrentDictionary is the only concurrent set in .NET. private readonly IDictionary usedReferences = new ConcurrentDictionary(); private readonly IDictionary unresolvedReferences = new ConcurrentDictionary(); private readonly List nonGeneratedSources; private readonly List generatedSources; private int dotnetFrameworkVersionVariantCount = 0; private int conflictedReferences = 0; private readonly IDependencyOptions options; private readonly DirectoryInfo sourceDir; private readonly IDotNet dotnet; private readonly FileContent fileContent; private readonly TemporaryDirectory packageDirectory; private readonly TemporaryDirectory legacyPackageDirectory; private readonly TemporaryDirectory missingPackageDirectory; private readonly TemporaryDirectory tempWorkingDirectory; private readonly bool cleanupTempWorkingDirectory; private readonly Lazy runtimeLazy; private Runtime Runtime => runtimeLazy.Value; /// /// 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.logger = logger; this.sourceDir = new DirectoryInfo(srcDir); packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName, "packages")); legacyPackageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName, "legacypackages")); missingPackageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName, "missingpackages")); tempWorkingDirectory = new TemporaryDirectory(FileUtils.GetTemporaryWorkingDirectory(out cleanupTempWorkingDirectory)); try { this.dotnet = DotNet.Make(options, logger, tempWorkingDirectory); runtimeLazy = new Lazy(() => new Runtime(dotnet)); } catch { logger.LogError("Missing dotnet CLI"); throw; } logger.LogInfo($"Finding files in {srcDir}..."); var allFiles = GetAllFiles().ToList(); var binaryFileExtensions = new HashSet(new[] { ".dll", ".exe" }); // TODO: add more binary file extensions. var allNonBinaryFiles = allFiles.Where(f => !binaryFileExtensions.Contains(f.Extension.ToLowerInvariant())).ToList(); var smallNonBinaryFiles = allNonBinaryFiles.SelectSmallFiles(logger).SelectFileNames(); this.fileContent = new FileContent(logger, smallNonBinaryFiles); this.nonGeneratedSources = allNonBinaryFiles.SelectFileNamesByExtension(".cs").ToList(); this.generatedSources = new(); var allProjects = allNonBinaryFiles.SelectFileNamesByExtension(".csproj").ToList(); var allSolutions = allNonBinaryFiles.SelectFileNamesByExtension(".sln").ToList(); var dllPaths = allFiles.SelectFileNamesByExtension(".dll").ToHashSet(); logger.LogInfo($"Found {allFiles.Count} files, {nonGeneratedSources.Count} source files, {allProjects.Count} project files, {allSolutions.Count} solution files, {dllPaths.Count} DLLs."); RestoreNugetPackages(allNonBinaryFiles, allProjects, allSolutions, dllPaths); // Find DLLs in the .Net / Asp.Net Framework // This needs to come after the nuget restore, because the nuget restore might fetch the .NET Core/Framework reference assemblies. var frameworkLocations = AddFrameworkDlls(dllPaths); assemblyCache = new AssemblyCache(dllPaths, frameworkLocations, logger); AnalyseSolutions(allSolutions); foreach (var filename in assemblyCache.AllAssemblies.Select(a => a.Filename)) { UseReference(filename); } RemoveNugetAnalyzerReferences(); ResolveConflicts(frameworkLocations); // Output the findings foreach (var r in usedReferences.Keys.OrderBy(r => r)) { logger.LogInfo($"Resolved reference {r}"); } foreach (var r in unresolvedReferences.OrderBy(r => r.Key)) { logger.LogInfo($"Unresolved reference {r.Key} in project {r.Value}"); } var webViewExtractionOption = Environment.GetEnvironmentVariable("CODEQL_EXTRACTOR_CSHARP_STANDALONE_EXTRACT_WEB_VIEWS"); if (webViewExtractionOption == null || bool.TryParse(webViewExtractionOption, out var shouldExtractWebViews) && shouldExtractWebViews) { GenerateSourceFilesFromWebViews(allNonBinaryFiles); } GenerateSourceFileFromImplicitUsings(); const int align = 6; logger.LogInfo(""); logger.LogInfo("Build analysis summary:"); logger.LogInfo($"{nonGeneratedSources.Count,align} source files in the filesystem"); logger.LogInfo($"{generatedSources.Count,align} generated source files"); logger.LogInfo($"{allSolutions.Count,align} solution files"); logger.LogInfo($"{allProjects.Count,align} project files in the filesystem"); logger.LogInfo($"{usedReferences.Keys.Count,align} resolved references"); logger.LogInfo($"{unresolvedReferences.Count,align} unresolved references"); logger.LogInfo($"{conflictedReferences,align} resolved assembly conflicts"); logger.LogInfo($"{dotnetFrameworkVersionVariantCount,align} restored .NET framework variants"); logger.LogInfo($"Build analysis completed in {DateTime.Now - startTime}"); } private HashSet AddFrameworkDlls(HashSet dllPaths) { var frameworkLocations = new HashSet(); AddNetFrameworkDlls(dllPaths, frameworkLocations); AddAspNetCoreFrameworkDlls(dllPaths, frameworkLocations); AddMicrosoftWindowsDesktopDlls(dllPaths, frameworkLocations); return frameworkLocations; } private void RestoreNugetPackages(List allNonBinaryFiles, IEnumerable allProjects, IEnumerable allSolutions, HashSet dllPaths) { try { var nuget = new NugetPackages(sourceDir.FullName, legacyPackageDirectory, logger); nuget.InstallPackages(); var nugetPackageDlls = legacyPackageDirectory.DirInfo.GetFiles("*.dll", new EnumerationOptions { RecurseSubdirectories = true }); var nugetPackageDllPaths = nugetPackageDlls.Select(f => f.FullName).ToHashSet(); var excludedPaths = nugetPackageDllPaths .Where(path => IsPathInSubfolder(path, legacyPackageDirectory.DirInfo.FullName, "tools")) .ToList(); if (nugetPackageDllPaths.Count > 0) { logger.LogInfo($"Restored {nugetPackageDllPaths.Count} Nuget DLLs."); } if (excludedPaths.Count > 0) { logger.LogInfo($"Excluding {excludedPaths.Count} Nuget DLLs."); } foreach (var excludedPath in excludedPaths) { logger.LogInfo($"Excluded Nuget DLL: {excludedPath}"); } nugetPackageDllPaths.ExceptWith(excludedPaths); dllPaths.UnionWith(nugetPackageDllPaths); } catch (Exception) { logger.LogError("Failed to restore Nuget packages with nuget.exe"); } var restoredProjects = RestoreSolutions(allSolutions, out var assets1); var projects = allProjects.Except(restoredProjects); RestoreProjects(projects, out var assets2); var dependencies = Assets.GetCompilationDependencies(logger, assets1.Union(assets2)); var paths = dependencies .Paths .Select(d => Path.Combine(packageDirectory.DirInfo.FullName, d)) .ToList(); dllPaths.UnionWith(paths); LogAllUnusedPackages(dependencies); DownloadMissingPackages(allNonBinaryFiles, dllPaths); } private static bool IsPathInSubfolder(string path, string rootFolder, string subFolder) { return path.IndexOf( $"{Path.DirectorySeparatorChar}{subFolder}{Path.DirectorySeparatorChar}", rootFolder.Length, StringComparison.InvariantCultureIgnoreCase) >= 0; } private void RemoveNugetAnalyzerReferences() { var packageFolder = packageDirectory.DirInfo.FullName.ToLowerInvariant(); if (packageFolder == null) { return; } foreach (var filename in usedReferences.Keys) { var lowerFilename = filename.ToLowerInvariant(); if (lowerFilename.StartsWith(packageFolder)) { var firstDirectorySeparatorCharIndex = lowerFilename.IndexOf(Path.DirectorySeparatorChar, packageFolder.Length + 1); if (firstDirectorySeparatorCharIndex == -1) { continue; } var secondDirectorySeparatorCharIndex = lowerFilename.IndexOf(Path.DirectorySeparatorChar, firstDirectorySeparatorCharIndex + 1); if (secondDirectorySeparatorCharIndex == -1) { continue; } var subFolderIndex = secondDirectorySeparatorCharIndex + 1; var isInAnalyzersFolder = lowerFilename.IndexOf("analyzers", subFolderIndex) == subFolderIndex; if (isInAnalyzersFolder) { usedReferences.Remove(filename); logger.LogInfo($"Removed analyzer reference {filename}"); } } } } private void SelectNewestFrameworkPath(string frameworkPath, string frameworkType, ISet dllPaths, ISet frameworkLocations) { var versionFolders = GetPackageVersionSubDirectories(frameworkPath); if (versionFolders.Length > 1) { var versions = string.Join(", ", versionFolders.Select(d => d.Name)); logger.LogInfo($"Found multiple {frameworkType} DLLs in NuGet packages at {frameworkPath}. Using the latest version ({versionFolders[0].Name}) from: {versions}."); } var selectedFrameworkFolder = versionFolders.FirstOrDefault()?.FullName; if (selectedFrameworkFolder is null) { logger.LogInfo($"Found {frameworkType} DLLs in NuGet packages at {frameworkPath}, but no version folder was found."); selectedFrameworkFolder = frameworkPath; } dllPaths.Add(selectedFrameworkFolder); frameworkLocations.Add(selectedFrameworkFolder); logger.LogInfo($"Found {frameworkType} DLLs in NuGet packages at {selectedFrameworkFolder}."); } private static DirectoryInfo[] GetPackageVersionSubDirectories(string packagePath) { return new DirectoryInfo(packagePath) .EnumerateDirectories("*", new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = false }) .OrderByDescending(d => d.Name) // TODO: Improve sorting to handle pre-release versions. .ToArray(); } private void AddNetFrameworkDlls(ISet dllPaths, ISet frameworkLocations) { // Multiple dotnet framework packages could be present. // The order of the packages is important, we're adding the first one that is present in the nuget cache. var packagesInPrioOrder = FrameworkPackageNames.NetFrameworks; var frameworkPaths = packagesInPrioOrder .Select((s, index) => (Index: index, Path: GetPackageDirectory(s))) .Where(pair => pair.Path is not null) .ToArray(); var frameworkPath = frameworkPaths.FirstOrDefault(); if (frameworkPath.Path is not null) { foreach (var fp in frameworkPaths) { dotnetFrameworkVersionVariantCount += GetPackageVersionSubDirectories(fp.Path!).Length; } SelectNewestFrameworkPath(frameworkPath.Path, ".NET Framework", dllPaths, frameworkLocations); for (var i = frameworkPath.Index + 1; i < packagesInPrioOrder.Length; i++) { RemoveNugetPackageReference(packagesInPrioOrder[i], dllPaths); } return; } string? runtimeLocation = null; if (fileContent.IsNewProjectStructureUsed) { runtimeLocation = Runtime.NetCoreRuntime; } else if (fileContent.IsLegacyProjectStructureUsed) { runtimeLocation = Runtime.DesktopRuntime; } runtimeLocation ??= Runtime.ExecutingRuntime; logger.LogInfo($".NET runtime location selected: {runtimeLocation}"); dllPaths.Add(runtimeLocation); frameworkLocations.Add(runtimeLocation); } private void RemoveNugetPackageReference(string packagePrefix, ISet dllPaths) { var packageFolder = packageDirectory.DirInfo.FullName.ToLowerInvariant(); if (packageFolder == null) { return; } var packagePathPrefix = Path.Combine(packageFolder, packagePrefix.ToLowerInvariant()); var toRemove = dllPaths.Where(s => s.StartsWith(packagePathPrefix, StringComparison.InvariantCultureIgnoreCase)); foreach (var path in toRemove) { dllPaths.Remove(path); logger.LogInfo($"Removed reference {path}"); } } private bool IsAspNetCoreDetected() { return fileContent.IsNewProjectStructureUsed && fileContent.UseAspNetCoreDlls; } private void AddAspNetCoreFrameworkDlls(ISet dllPaths, ISet frameworkLocations) { if (!IsAspNetCoreDetected()) { return; } // First try to find ASP.NET Core assemblies in the NuGet packages if (GetPackageDirectory(FrameworkPackageNames.AspNetCoreFramework) is string aspNetCorePackage) { SelectNewestFrameworkPath(aspNetCorePackage, "ASP.NET Core", dllPaths, frameworkLocations); return; } if (Runtime.AspNetCoreRuntime is string aspNetCoreRuntime) { logger.LogInfo($"ASP.NET runtime location selected: {aspNetCoreRuntime}"); dllPaths.Add(aspNetCoreRuntime); frameworkLocations.Add(aspNetCoreRuntime); } } private void AddMicrosoftWindowsDesktopDlls(ISet dllPaths, ISet frameworkLocations) { if (GetPackageDirectory(FrameworkPackageNames.WindowsDesktopFramework) is string windowsDesktopApp) { SelectNewestFrameworkPath(windowsDesktopApp, "Windows Desktop App", dllPaths, frameworkLocations); } } private string? GetPackageDirectory(string packagePrefix) { return new DirectoryInfo(packageDirectory.DirInfo.FullName) .EnumerateDirectories(packagePrefix + "*", new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = false }) .FirstOrDefault()? .FullName; } private ICollection GetAllPackageDirectories() { return new DirectoryInfo(packageDirectory.DirInfo.FullName) .EnumerateDirectories("*", new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = false }) .Select(d => d.Name) .ToList(); } private void LogAllUnusedPackages(DependencyContainer dependencies) { var allPackageDirectories = GetAllPackageDirectories(); logger.LogInfo($"Restored {allPackageDirectories.Count} packages"); logger.LogInfo($"Found {dependencies.Packages.Count} packages in project.asset.json files"); allPackageDirectories .Where(package => !dependencies.Packages.Contains(package)) .Order() .ForEach(package => logger.LogInfo($"Unused package: {package}")); } private void GenerateSourceFileFromImplicitUsings() { var usings = new HashSet(); if (!fileContent.UseImplicitUsings) { return; } // Hardcoded values from https://learn.microsoft.com/en-us/dotnet/core/project-sdk/overview#implicit-using-directives usings.UnionWith(new[] { "System", "System.Collections.Generic", "System.IO", "System.Linq", "System.Net.Http", "System.Threading", "System.Threading.Tasks" }); if (fileContent.UseAspNetCoreDlls) { usings.UnionWith(new[] { "System.Net.Http.Json", "Microsoft.AspNetCore.Builder", "Microsoft.AspNetCore.Hosting", "Microsoft.AspNetCore.Http", "Microsoft.AspNetCore.Routing", "Microsoft.Extensions.Configuration", "Microsoft.Extensions.DependencyInjection", "Microsoft.Extensions.Hosting", "Microsoft.Extensions.Logging" }); } usings.UnionWith(fileContent.CustomImplicitUsings); logger.LogInfo($"Generating source file for implicit usings. Namespaces: {string.Join(", ", usings.OrderBy(u => u))}"); if (usings.Count > 0) { var tempDir = GetTemporaryWorkingDirectory("implicitUsings"); var path = Path.Combine(tempDir, "GlobalUsings.g.cs"); using (var writer = new StreamWriter(path)) { writer.WriteLine("// "); writer.WriteLine(""); foreach (var u in usings.OrderBy(u => u)) { writer.WriteLine($"global using global::{u};"); } } this.generatedSources.Add(path); } } private void GenerateSourceFilesFromWebViews(List allFiles) { var views = allFiles.SelectFileNamesByExtension(".cshtml", ".razor").ToArray(); if (views.Length == 0) { return; } logger.LogInfo($"Found {views.Length} cshtml and razor files."); if (!IsAspNetCoreDetected()) { logger.LogInfo("Generating source files from cshtml files is only supported for new (SDK-style) project files"); return; } logger.LogInfo("Generating source files from cshtml and razor files..."); var sdk = new Sdk(dotnet).GetNewestSdk(); if (sdk != null) { try { var razor = new Razor(sdk, dotnet, logger); var targetDir = GetTemporaryWorkingDirectory("razor"); var generatedFiles = razor.GenerateFiles(views, usedReferences.Keys, targetDir); this.generatedSources.AddRange(generatedFiles); } catch (Exception ex) { // It's okay, we tried our best to generate source files from cshtml files. logger.LogInfo($"Failed to generate source files from cshtml files: {ex.Message}"); } } } public DependencyManager(string srcDir) : this(srcDir, DependencyOptions.Default, new ConsoleLogger(Verbosity.Info, logThreadId: true)) { } private IEnumerable GetAllFiles() { IEnumerable files = sourceDir.GetFiles("*.*", new EnumerationOptions { RecurseSubdirectories = true }); if (options.DotNetPath != null) { files = files.Where(f => !f.FullName.StartsWith(options.DotNetPath, StringComparison.OrdinalIgnoreCase)); } files = files.Where(f => { try { if (f.Exists) { return true; } logger.Log(Severity.Warning, $"File {f.FullName} could not be processed."); return false; } catch (Exception ex) { logger.Log(Severity.Warning, $"File {f.FullName} could not be processed: {ex.Message}"); return false; } }); files = new FilePathFilter(sourceDir, logger).Filter(files); return files; } /// /// 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) { 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(FileUtils.GetTemporaryWorkingDirectory(out var _), sb.ToString(), subfolderName); } /// /// Creates a temporary directory with the given subfolder name. /// The created directory might be inside the repo folder, and it is deleted when the object is disposed. /// /// /// private string GetTemporaryWorkingDirectory(string subfolder) { var temp = Path.Combine(tempWorkingDirectory.ToString(), subfolder); Directory.CreateDirectory(temp); return temp; } /// /// 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(IEnumerable frameworkPaths) { var sortedReferences = new List(usedReferences.Count); foreach (var usedReference in usedReferences) { try { var assemblyInfo = assemblyCache.GetAssemblyInfo(usedReference.Key); sortedReferences.Add(assemblyInfo); } catch (AssemblyLoadException) { logger.Log(Severity.Warning, $"Could not load assembly information from {usedReference.Key}"); } } sortedReferences = sortedReferences .OrderAssemblyInfosByPreference(frameworkPaths) .ToList(); logger.LogInfo($"Reference list contains {sortedReferences.Count} assemblies"); 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); } logger.LogInfo($"After conflict resolution, reference list contains {finalAssemblyList.Count} assemblies"); // Report the results foreach (var r in sortedReferences) { var resolvedInfo = finalAssemblyList[r.Name]; if (resolvedInfo.Version != r.Version || resolvedInfo.NetCoreVersion != r.NetCoreVersion) { var asm = resolvedInfo.Id + (resolvedInfo.NetCoreVersion is null ? "" : $" (.NET Core {resolvedInfo.NetCoreVersion})"); logger.LogInfo($"Resolved {r.Id} as {asm}"); ++conflictedReferences; } if (r != resolvedInfo) { logger.LogDebug($"Resolved {r.Id} as {resolvedInfo.Id} from {resolvedInfo.Filename}"); } } } /// /// Store that a particular reference file is used. /// /// The filename of the reference. private void UseReference(string reference) => usedReferences[reference] = true; /// /// The list of resolved reference files. /// public IEnumerable ReferenceFiles => usedReferences.Keys; /// /// All of the generated source files in the source directory. /// public IEnumerable GeneratedSourceFiles => generatedSources; /// /// All of the source files in the source directory. /// public IEnumerable AllSourceFiles => generatedSources.Concat(nonGeneratedSources); /// /// List of assembly IDs which couldn't be resolved. /// public IEnumerable UnresolvedReferences => unresolvedReferences.Select(r => r.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; private void AnalyseSolutions(IEnumerable solutions) { Parallel.ForEach(solutions, new ParallelOptions { MaxDegreeOfParallelism = options.Threads }, solutionFile => { try { var sln = new SolutionFile(solutionFile); logger.LogInfo($"Analyzing {solutionFile}..."); foreach (var proj in sln.Projects.Select(p => new FileInfo(p))) { AnalyseProject(proj); } } catch (Microsoft.Build.Exceptions.InvalidProjectFileException ex) { logger.LogInfo($"Couldn't read solution file {solutionFile}: {ex.BaseMessage}"); } }); } private void AnalyseProject(FileInfo project) { if (!project.Exists) { logger.LogInfo($"Couldn't read project file {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); } } } catch (Exception ex) // lgtm[cs/catch-of-all-exceptions] { logger.LogInfo($"Couldn't read project file {project.FullName}: {ex.Message}"); } } /// /// 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). /// Populates assets with the relative paths to the assets files generated by the restore. /// 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, out IEnumerable assets) { var assetFiles = new List(); var projects = solutions.SelectMany(solution => { logger.LogInfo($"Restoring solution {solution}..."); var res = dotnet.Restore(new(solution, packageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: true)); assetFiles.AddRange(res.AssetsFilePaths); return res.RestoredProjects; }); assets = assetFiles; return projects; } /// /// Executes `dotnet restore` on all projects in projects. /// This is done in parallel for performance reasons. /// Populates assets with the relative paths to the assets files generated by the restore. /// /// A list of paths to project files. private void RestoreProjects(IEnumerable projects, out IEnumerable assets) { var assetFiles = new List(); Parallel.ForEach(projects, new ParallelOptions { MaxDegreeOfParallelism = options.Threads }, project => { logger.LogInfo($"Restoring project {project}..."); var res = dotnet.Restore(new(project, packageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: true)); assetFiles.AddRange(res.AssetsFilePaths); }); assets = assetFiles; } private void DownloadMissingPackages(List allFiles, ISet dllPaths) { var alreadyDownloadedPackages = Directory.GetDirectories(packageDirectory.DirInfo.FullName) .Select(d => Path.GetFileName(d).ToLowerInvariant()); var notYetDownloadedPackages = fileContent.AllPackages .Except(alreadyDownloadedPackages) .ToList(); if (notYetDownloadedPackages.Count == 0) { return; } logger.LogInfo($"Found {notYetDownloadedPackages.Count} packages that are not yet restored"); var nugetConfigs = allFiles.SelectFileNamesByName("nuget.config").ToArray(); string? nugetConfig = null; if (nugetConfigs.Length > 1) { logger.LogInfo($"Found multiple nuget.config files: {string.Join(", ", nugetConfigs)}."); nugetConfig = allFiles .SelectRootFiles(sourceDir) .SelectFileNamesByName("nuget.config") .FirstOrDefault(); if (nugetConfig == null) { logger.LogInfo("Could not find a top-level nuget.config file."); } } else { nugetConfig = nugetConfigs.FirstOrDefault(); } if (nugetConfig != null) { logger.LogInfo($"Using nuget.config file {nugetConfig}."); } Parallel.ForEach(notYetDownloadedPackages, new ParallelOptions { MaxDegreeOfParallelism = options.Threads }, package => { logger.LogInfo($"Restoring package {package}..."); using var tempDir = new TemporaryDirectory(ComputeTempDirectory(package, "missingpackages_workingdir")); var success = dotnet.New(tempDir.DirInfo.FullName); if (!success) { return; } success = dotnet.AddPackage(tempDir.DirInfo.FullName, package); if (!success) { return; } var res = dotnet.Restore(new(tempDir.DirInfo.FullName, missingPackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: false, PathToNugetConfig: nugetConfig)); if (!res.Success) { if (res.HasNugetPackageSourceError) { // Restore could not be completed because the listed source is unavailable. Try without the nuget.config: res = dotnet.Restore(new(tempDir.DirInfo.FullName, missingPackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: false, PathToNugetConfig: null, ForceReevaluation: true)); } // TODO: the restore might fail, we could retry with a prerelease (*-* instead of *) version of the package. if (!res.Success) { logger.LogInfo($"Failed to restore nuget package {package}"); } } }); dllPaths.Add(missingPackageDirectory.DirInfo.FullName); } public void Dispose(TemporaryDirectory? dir, string name) { try { dir?.Dispose(); } catch (Exception exc) { logger.LogInfo($"Couldn't delete {name} directory {exc.Message}"); } } public void Dispose() { Dispose(packageDirectory, "package"); Dispose(legacyPackageDirectory, "legacy package"); Dispose(missingPackageDirectory, "missing package"); if (cleanupTempWorkingDirectory) { Dispose(tempWorkingDirectory, "temporary working"); } } } }