using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using Semmle.Util; namespace Semmle.Extraction.CSharp.DependencyFetching { /// /// 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. /// internal class NugetExeWrapper : IDisposable { private readonly string? nugetExe; private readonly Semmle.Util.Logging.ILogger logger; public int PackageCount => fileProvider.PackagesConfigs.Count; private readonly string? backupNugetConfig; private readonly string? nugetConfigPath; private readonly FileProvider fileProvider; /// /// The computed packages directory. /// This will be in the Temp location /// so as to not trample the source tree. /// private readonly TemporaryDirectory packageDirectory; /// /// Create the package manager for a specified source tree. /// public NugetExeWrapper(FileProvider fileProvider, TemporaryDirectory packageDirectory, Semmle.Util.Logging.ILogger logger) { this.fileProvider = fileProvider; this.packageDirectory = packageDirectory; this.logger = logger; if (fileProvider.PackagesConfigs.Count > 0) { logger.LogInfo($"Found packages.config files, trying to use nuget.exe for package restore"); nugetExe = ResolveNugetExe(); if (HasNoPackageSource()) { // We only modify or add a top level nuget.config file nugetConfigPath = Path.Combine(fileProvider.SourceDir.FullName, "nuget.config"); try { if (File.Exists(nugetConfigPath)) { var tempFolderPath = FileUtils.GetTemporaryWorkingDirectory(out _); do { backupNugetConfig = Path.Combine(tempFolderPath, Path.GetRandomFileName()); } while (File.Exists(backupNugetConfig)); File.Copy(nugetConfigPath, backupNugetConfig, true); } else { File.WriteAllText(nugetConfigPath, """ """); } AddDefaultPackageSource(nugetConfigPath); } catch (Exception e) { logger.LogError($"Failed to add default package source to {nugetConfigPath}: {e}"); } } } } /// /// 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 = Win32.IsWindows() ? "nuget.exe" : "nuget"; var nugetPath = FileUtils.FindProgramOnPath(executableName); if (nugetPath is not null) { nugetPath = Path.Combine(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.Combine(sourceDir, ".nuget"); var nuget = Path.Combine(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 => !Win32.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}\"..."); /* 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}\" \"{packagesConfig}\""; } else { exe = nugetExe!; args = $"install -OutputDirectory \"{packageDirectory}\" \"{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 bool HasNoPackageSource() { if (Win32.IsWindows()) { return false; } try { logger.LogInfo("Checking if default package source is available..."); RunMonoNugetCommand("sources list -ForceEnglishOutput", out var stdout); if (stdout.All(line => line != "No sources found.")) { return false; } return true; } catch (Exception e) { logger.LogWarning($"Failed to check if default package source is added: {e}"); return false; } } private void RunMonoNugetCommand(string command, out IList stdout) { string exe, args; if (RunWithMono) { exe = "mono"; args = $"\"{nugetExe}\" {command}"; } else { exe = nugetExe!; args = command; } 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); pi.ReadOutput(out stdout, onOut, onError); } private void AddDefaultPackageSource(string nugetConfig) { logger.LogInfo("Adding default package source..."); RunMonoNugetCommand($"sources add -Name DefaultNugetOrg -Source {NugetPackageRestorer.PublicNugetOrgFeed} -ConfigFile \"{nugetConfig}\"", out _); } public void Dispose() { if (nugetConfigPath is null) { return; } try { if (backupNugetConfig is null) { logger.LogInfo("Removing nuget.config file"); File.Delete(nugetConfigPath); return; } logger.LogInfo("Reverting nuget.config file content"); // The content of the original nuget.config file is reverted without changing the file's attributes or casing: using (var backup = File.OpenRead(backupNugetConfig)) using (var current = File.OpenWrite(nugetConfigPath)) { current.SetLength(0); // Truncate file backup.CopyTo(current); // Restore original content } logger.LogInfo("Deleting backup nuget.config file"); File.Delete(backupNugetConfig); } catch (Exception exc) { logger.LogError($"Failed to restore original nuget.config file: {exc}"); } } } }