using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using Semmle.Util; namespace Semmle.Extraction.CSharp.DependencyFetching { internal interface IPackagesConfigRestore { /// /// The number of packages.config files found in the source tree. /// int PackageCount { get; } /// /// Download the packages to the temp folder. /// int InstallPackages(); } /// /// Factory for creating a package manager to restore NuGet packages referenced in packages.config files. /// If the environment doesn't support using nuget.exe to restore packages from packages.config files, a no-op implementation is returned. /// It is worth noting that for macOS and Linux, nuget.exe is used with mono. However, mono is being deprecated and the last GitHub images /// to contain mono are: /// - Ubuntu 22.04 /// - macOS 14 /// /// If the packages from the packages.config files are not restored with the packages.config restore functionality below, there is a subsequent /// step that still may succeed in restoring the packages without the help of nuget.exe (by attempting to restore using dotnet). /// internal class PackagesConfigRestoreFactory { public static IPackagesConfigRestore Create(FileProvider fileProvider, DependencyDirectory packageDirectory, Semmle.Util.Logging.ILogger logger, FeedManager feedManager, HashSet reachableFeeds) { if (SystemBuildActions.Instance.IsWindows() || SystemBuildActions.Instance.IsMonoInstalled()) { return new NugetExeWrapper(fileProvider, packageDirectory, logger, feedManager, reachableFeeds); } return new NoOpPackagesConfig(fileProvider.PackagesConfigs, logger); } /// /// Manage the downloading of NuGet packages with nuget.exe. /// Locates packages in a source tree and downloads all of the /// referenced assemblies to a temp folder. /// private class NugetExeWrapper : IPackagesConfigRestore { private readonly string? nugetExe; private readonly Semmle.Util.Logging.ILogger logger; public int PackageCount => fileProvider.PackagesConfigs.Count; private readonly FileProvider fileProvider; /// /// The packages directory. /// This will be in the user-specified or computed Temp location /// so as to not trample the source tree. /// private readonly DependencyDirectory packageDirectory; private readonly FeedManager feedManager; private readonly HashSet reachableFeeds; private bool IsWindows => SystemBuildActions.Instance.IsWindows(); private bool? isDefaultFeedReachable; private bool IsDefaultFeedReachable => isDefaultFeedReachable ??= feedManager.IsDefaultFeedReachable(); /// /// Create the package manager for a specified source tree. /// public NugetExeWrapper(FileProvider fileProvider, DependencyDirectory packageDirectory, Semmle.Util.Logging.ILogger logger, FeedManager feedManager, HashSet reachableFeeds) { this.fileProvider = fileProvider; this.packageDirectory = packageDirectory; this.logger = logger; this.feedManager = feedManager; this.reachableFeeds = reachableFeeds; if (fileProvider.PackagesConfigs.Count > 0) { logger.LogInfo($"Found packages.config files, trying to use nuget.exe for package restore"); nugetExe = ResolveNugetExe(); } } /// /// Tries to find the location of `nuget.exe`. It looks for /// - the environment variable specifying a location, /// - files in the repository, /// - tries to resolve nuget from the PATH, or /// - downloads it if it is not found. /// private string ResolveNugetExe() { var envVarPath = Environment.GetEnvironmentVariable(EnvironmentVariableNames.NugetExePath); if (!string.IsNullOrEmpty(envVarPath)) { logger.LogInfo($"Using nuget.exe from environment variable: '{envVarPath}'"); return envVarPath; } try { return DownloadNugetExe(fileProvider.SourceDir.FullName); } catch (Exception exc) { logger.LogInfo($"Download of nuget.exe failed: {exc.Message}"); } var nugetExesInRepo = fileProvider.NugetExes; if (nugetExesInRepo.Count > 1) { logger.LogInfo($"Found multiple nuget.exe files in the repository: {string.Join(", ", nugetExesInRepo.OrderBy(s => s))}"); } if (nugetExesInRepo.Count > 0) { var path = nugetExesInRepo.First(); logger.LogInfo($"Using nuget.exe from path '{path}'"); return path; } var executableName = IsWindows ? "nuget.exe" : "nuget"; var nugetPath = FileUtils.FindProgramOnPath(executableName); if (nugetPath is not null) { nugetPath = Path.Join(nugetPath, executableName); logger.LogInfo($"Using nuget.exe from PATH: {nugetPath}"); return nugetPath; } throw new Exception("Could not find or download nuget.exe."); } private string DownloadNugetExe(string sourceDir) { var directory = Path.Join(sourceDir, ".nuget"); var nuget = Path.Join(directory, "nuget.exe"); // Nuget.exe already exists in the .nuget directory. if (File.Exists(nuget)) { logger.LogInfo($"Found nuget.exe at {nuget}"); return nuget; } Directory.CreateDirectory(directory); logger.LogInfo("Attempting to download nuget.exe"); FileUtils.DownloadFile(FileUtils.NugetExeUrl, nuget, logger); logger.LogInfo($"Downloaded nuget.exe to {nuget}"); return nuget; } private bool RunWithMono => !IsWindows && !string.IsNullOrEmpty(Path.GetExtension(nugetExe)); /// /// Restore all packages in the specified packages.config file. /// /// The packages.config file. private bool TryRestoreNugetPackage(string packagesConfig) { logger.LogInfo($"Restoring file \"{packagesConfig}\"..."); var sourcesArgument = ""; var feedsToUse = feedManager.FeedsToUse(packagesConfig, reachableFeeds).ToList(); var useDefaultFeed = feedsToUse.Count == 0 && IsDefaultFeedReachable; // Explicitly construct the sources to be used for the restore command when checking feed // responsiveness, using private registries, or falling back to nuget.org. if (feedManager.CheckNugetFeedResponsiveness || feedManager.HasPrivateRegistryFeeds || useDefaultFeed) { if (useDefaultFeed) { feedsToUse.Add(FeedManager.PublicNugetOrgFeed); } sourcesArgument = feedManager.FeedsToRestoreArgument(feedsToUse, "-Source"); } /* Use nuget.exe to install a package. * Note that there is a clutch of NuGet assemblies which could be used to * invoke this directly, which would arguably be nicer. However they are * really unwieldy and this solution works for now. */ string exe, args; if (RunWithMono) { exe = "mono"; args = $"\"{nugetExe}\" install -OutputDirectory \"{packageDirectory}\" {sourcesArgument} \"{packagesConfig}\""; } else { exe = nugetExe!; args = $"install -OutputDirectory \"{packageDirectory}\" {sourcesArgument} \"{packagesConfig}\""; } var pi = new ProcessStartInfo(exe, args) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; var threadId = Environment.CurrentManagedThreadId; void onOut(string s) => logger.LogDebug(s, threadId); void onError(string s) => logger.LogError(s, threadId); var exitCode = pi.ReadOutput(out _, onOut, onError); if (exitCode != 0) { logger.LogError($"Command {pi.FileName} {pi.Arguments} failed with exit code {exitCode}"); return false; } else { logger.LogInfo($"Restored file \"{packagesConfig}\""); return true; } } /// /// Download the packages to the temp folder. /// public int InstallPackages() { return fileProvider.PackagesConfigs.Count(TryRestoreNugetPackage); } } private class NoOpPackagesConfig : IPackagesConfigRestore { private readonly Semmle.Util.Logging.ILogger logger; private readonly ICollection packagesConfigs; public NoOpPackagesConfig(ICollection packagesConfigs, Semmle.Util.Logging.ILogger logger) { this.packagesConfigs = packagesConfigs; this.logger = logger; } public int PackageCount => packagesConfigs.Count; public int InstallPackages() { if (PackageCount > 0) { logger.LogInfo("Found packages.config files, but nuget.exe cannot be used to restore packages on this platform. Skipping restore of packages.config files."); } return 0; } } } }