using OpenTap.Cli; using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; namespace OpenTap.Package { [Display("install", Description: "Install the specified image, overwriting the existing installation.", Group: "image")] internal class ImageInstallAction : IsolatedPackageAction { /// /// Path to Image file containing XML or JSON formatted Image specification, or just the string itself, e.g "REST-API,TUI:beta". /// [UnnamedCommandLineArgument("image", Required = true, Description = "The image to install. Supported formats:\n" + "A string describing the image. Example: 'OpenTAP:9.24,TUI:beta,CSV'\n" + "A path to a file containing an XML or JSON formatted image specification.")] public string ImagePath { get; set; } /// /// Option to merge with target installation. Default is false, which means overwrite installation /// [CommandLineArgument("merge", Description = "Merge the requested image with the existing installation instead of overwriting it.")] public bool Merge { get; set; } /// Never prompt for user input. [CommandLineArgument("non-interactive", Description = "Never prompt for user input.")] public bool NonInteractive { get; set; } = false; /// Which operative system to resolve packages for. [CommandLineArgument("os", Description = "The operating system to resolve packages for.")] public string Os { get; set; } /// Which CPU architecture to resolve packages for. [CommandLineArgument("architecture", Description = "The architecture to resolve packages for.")] public CpuArchitecture Architecture { get; set; } = CpuArchitecture.Unspecified; /// Resolve and print a summary of the changes that would be applied, but don't apply them. [CommandLineArgument("dry-run", Description = "Only print the result, don't install the packages.")] public bool DryRun { get; set; } /// Which repositories to use. [CommandLineArgument("repository", ShortName = "r", Description = "Repositories to use for resolving the image.")] public string[] Repositories { get; set; } = null; protected override int LockedExecute(CancellationToken cancellationToken) { Repositories = ExtractRepositoryTokens(Repositories, true); if (NonInteractive) UserInput.SetInterface(new NonInteractiveUserInputInterface()); if (Force) log.Warning($"Using --force does not force an image installation"); if (string.IsNullOrEmpty(ImagePath)) throw new ArgumentException("'image' not specified.", nameof(ImagePath)); var imageString = ImagePath; if (File.Exists(imageString)) imageString = File.ReadAllText(imageString); var imageSpecifier = ImageSpecifier.FromString(imageString); // image specifies any repositories? if (imageSpecifier.Repositories?.Any() != true) { if (Repositories?.Any() == true) imageSpecifier.Repositories = Repositories.ToList(); else imageSpecifier.Repositories = PackageManagerSettings.Current.Repositories .Where(x => x.IsEnabled) .Select(x => x.Url) .ToList(); } else { if (Repositories?.Any() == true) imageSpecifier.Repositories = Repositories.ToList(); } if (!string.IsNullOrWhiteSpace(Os)) imageSpecifier.OS = Os; if (Architecture != CpuArchitecture.Unspecified) imageSpecifier.Architecture = Architecture; try { ImageIdentifier image; var sw = Stopwatch.StartNew(); if (TryFindParentInstallation(Target, out var parent)) { log.Error($"OpenTAP installation detected in directory '{parent}'. Nested installations are not supported."); return 1; } if (Merge) { var deploymentInstallation = new Installation(Target); image = imageSpecifier.MergeAndResolve(deploymentInstallation, cancellationToken); } else { // Here we add the list of currently installed packages to 'AdditionalPackages'. // These packages will be added to the imageSpecifier's internal cache of known packages. // This fixes an edge case where an image fails to resolve because one of the specified // packages is already installed, but is not available in any of the specified repositories // (or the local cache). In that case, if the installed version is compatible with the image, // the image resolution will still succeed even if the package cannot be found. // This is possible e.g. with debug installs where the debug package does not exist in any repository, // or if the package cache has been cleared by the user, and an image install is attempted // without the original source repository of some package. imageSpecifier.AdditionalPackages.AddRange(new Installation(Target).GetPackages()); image = imageSpecifier.Resolve(TapThread.Current.AbortToken); } if (image == null) { log.Error(sw, "Unable to resolve image"); return 1; } log.Debug(sw, "Image resolution done"); if (image.Packages.Count == 0) { // We should prompt the user and warn them that they are about to completely wipe their installation. var req = new WipeInstallationQuestion(); UserInput.Request(req, true); if (req.WipeInstallation == WipeInstallationResponse.No) { throw new OperationCanceledException("Image installation was canceled."); } } log.Debug("Image hash: {0}", image.Id); if (DryRun) { log.Info("Resolved packages:"); foreach (var pkg in image.Packages) { log.Info(" {0}: {1}", pkg.Name, pkg.Version); } return 0; } image.Deploy(Target, cancellationToken); return 0; } catch (AggregateException e) { foreach (var innerException in e.InnerExceptions) log.Error($"- {innerException.Message}"); throw new ExitCodeException((int)PackageExitCodes.PackageDependencyError, e.Message); } } enum WipeInstallationResponse { Yes, No, } [Display("Completely wipe installation?")] class WipeInstallationQuestion { [Browsable(true)] [Layout(LayoutMode.FullRow)] public string Message { get; } = "You are about to completely wipe your installation. Do you wish to Continue?"; [Submit] [Layout(LayoutMode.FullRow | LayoutMode.FloatBottom)] public WipeInstallationResponse WipeInstallation { get; set; } = WipeInstallationResponse.Yes; } } }