diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetExeWrapper.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetExeWrapper.cs deleted file mode 100644 index e97b0b118c68..000000000000 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetExeWrapper.cs +++ /dev/null @@ -1,304 +0,0 @@ -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 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; - - /// - /// Create the package manager for a specified source tree. - /// - public NugetExeWrapper(FileProvider fileProvider, DependencyDirectory packageDirectory, Semmle.Util.Logging.ILogger logger, Func useDefaultFeed) - { - 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() && useDefaultFeed()) - { - // 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}"); - } - } - } -} diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs index e042285af11c..7bafbb6f4300 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs @@ -161,13 +161,13 @@ public HashSet Restore() reachableFeeds.UnionWith(reachableInheritedFeeds); } - using (var nuget = new NugetExeWrapper(fileProvider, legacyPackageDirectory, logger, IsDefaultFeedReachable)) + using (var packagesConfigRestore = PackagesConfigRestoreFactory.Create(fileProvider, legacyPackageDirectory, logger, IsDefaultFeedReachable)) { - var count = nuget.InstallPackages(); + var count = packagesConfigRestore.InstallPackages(); - if (nuget.PackageCount > 0) + if (packagesConfigRestore.PackageCount > 0) { - compilationInfoContainer.CompilationInfos.Add(("packages.config files", nuget.PackageCount.ToString())); + compilationInfoContainer.CompilationInfos.Add(("packages.config files", packagesConfigRestore.PackageCount.ToString())); compilationInfoContainer.CompilationInfos.Add(("Successfully restored packages.config files", count.ToString())); } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs new file mode 100644 index 000000000000..68a0a746ca91 --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs @@ -0,0 +1,368 @@ +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 : IDisposable + { + /// + /// 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, Func useDefaultFeed) + { + if (SystemBuildActions.Instance.IsWindows() || SystemBuildActions.Instance.IsMonoInstalled()) + { + return new NugetExeWrapper(fileProvider, packageDirectory, logger, useDefaultFeed); + } + + return new NoOpPackagesConfig(fileProvider, 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 string? backupNugetConfig; + private readonly string? nugetConfigPath; + 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 bool IsWindows => SystemBuildActions.Instance.IsWindows(); + + /// + /// Create the package manager for a specified source tree. + /// + public NugetExeWrapper(FileProvider fileProvider, DependencyDirectory packageDirectory, Semmle.Util.Logging.ILogger logger, Func useDefaultFeed) + { + 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 (!HasPackageSource() && useDefaultFeed()) + { + // We only modify or add a top level nuget.config file + nugetConfigPath = Path.Join(fileProvider.SourceDir.FullName, "nuget.config"); + try + { + if (File.Exists(nugetConfigPath)) + { + var tempFolderPath = FileUtils.GetTemporaryWorkingDirectory(out _); + + do + { + backupNugetConfig = Path.Join(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 = 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}\"..."); + + /* 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 HasPackageSource() + { + if (IsWindows) + { + return true; + } + + 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 true; + } + + return false; + } + catch (Exception e) + { + logger.LogWarning($"Failed to check if default package source is added: {e}"); + return true; + } + } + + 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}"); + } + } + } + + private class NoOpPackagesConfig : IPackagesConfigRestore + { + private readonly Semmle.Util.Logging.ILogger logger; + private readonly FileProvider fileProvider; + + public NoOpPackagesConfig(FileProvider fileProvider, Semmle.Util.Logging.ILogger logger) + { + this.fileProvider = fileProvider; + this.logger = logger; + } + + public int PackageCount => fileProvider.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; + } + + public void Dispose() { } + } + } +}