using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; namespace OpenTap.Package { /// /// An defines an OpenTAP installation. The specifier can be resolved to an /// which can be deployed to an actual OpenTAP installation. /// public class ImageSpecifier { static readonly TraceSource log = Log.CreateSource("image"); /// /// Optional name of the ImageSpecifier. Used for debugging purposes. /// public string Name { get; set; } = ""; /// /// Desired packages in the installation /// public List Packages { get; set; } = new List(); /// Installed specifiers. internal PackageSpecifier[] FixedPackages { get; set; } = Array.Empty(); /// The installed packages. internal ImmutableArray InstalledPackages { get; set; } = ImmutableArray.Empty; /// Creates a new instance. public ImageSpecifier(List packages, string name = "") { Packages = packages; Name = name; } /// Creates a new instance. public ImageSpecifier() { } private static bool IsZipFile(string path) { using var fs = File.OpenRead(path); byte[] fileSignature = new byte[4]; int read = fs.Read(fileSignature, 0, 4); if (read != 4) return false; // Check for presence of magic header var zipSignature = "PK\x03\x04"u8.ToArray(); return fileSignature.SequenceEqual(zipSignature); } internal static ImageSpecifier FromAddedPackages(Installation installation, IEnumerable newPackages) { var toInstall = new List(); var installed = installation.GetPackages(true).ToList(); var additionalPackages = new List(); foreach (var package in newPackages) { var existing = installed.FirstOrDefault(x => x.Name == package.Name); if (existing != null) { installed.Remove(existing); additionalPackages.Add(existing); } // IsZipFile handles the special case where there exists a file on disk // with the same name as a package which is not a zip file. if (File.Exists(package.Name) && IsZipFile(package.Name)) { if (PackageDef.TryFromPackage(package.Name, out var package2)) { toInstall.Add(new PackageSpecifier(package2.Name, package2.Version.AsExactSpecifier())); installed.Add(package2); continue; } else { var extension = Path.GetExtension(package.Name); var expectedExtensions = new string[] { ".zip", ".tappackage" }; if (extension != null && expectedExtensions.Any(e => string.Equals(e, extension, StringComparison.OrdinalIgnoreCase))) { throw new Exception($"Package {package.Name} could not be installed. The file is not a valid tap package."); } } } toInstall.Add(package); } var fixedPackages = installed.Where(x => toInstall.Any(y => y.Name == x.Name) == false) .Select(x => new PackageSpecifier(x.Name, x.Version.AsCompatibleSpecifier(), x.Architecture, x.OS)) .ToArray(); toInstall.AddRange(fixedPackages); return new ImageSpecifier { Packages = toInstall, AdditionalPackages = additionalPackages, InstalledPackages = installed.ToImmutableArray(), FixedPackages = fixedPackages }; } /// /// OpenTAP repositories to fetch the desired packages from /// These should be well formed URIs and will be interpreted relative to the BaseAddress set in AuthenticationSettings. /// public List Repositories { get; set; } = new List(); internal List AdditionalPackages { get; set; } = new List(); /// The OS this image specifier targets. public string OS { get; set; } = Installation.Current.OS; /// The CPU architecture that this image specifier targets. public CpuArchitecture Architecture { get; set; } = Installation.Current.Architecture; /// /// Resolve the desired packages from the specified repositories. This will check if the packages are available, compatible and can successfully be deployed as an OpenTAP installation /// /// Cancellation token /// The exception thrown if the image could not be resolved public ImageIdentifier Resolve(CancellationToken cancellationToken) { List repositories = Repositories.Distinct() .Select(PackageRepositoryHelpers.DetermineRepositoryType) .GroupBy(p => p.Url) .Select(g => g.First()) .ToList(); var cache = new PackageDependencyCache(OS, Architecture, Repositories); cache.LoadFromRepositories(); cache.AddPackages(InstalledPackages); cache.AddPackages(AdditionalPackages); var sw = Stopwatch.StartNew(); var resolver = new ImageResolver(cancellationToken); var image = resolver.ResolveImage(this, cache.Graph); if (image.Success == false) { throw new ImageResolveException(image, this, InstalledPackages); } log.Debug(sw, "Resolved image: {0}", this); var packages = image.Packages.Select(x => cache.GetPackageDef(x)).ToArray(); if (packages.Any(x => x == null)) throw new InvalidOperationException("Unable to lookup resolved package"); ImageIdentifier image2 = new ImageIdentifier(packages, repositories.Select(s => s.Url)); return image2; } /// /// Merges and resolves the packages for a number of images. May throw an exception if the packages cannot be resolved. /// /// The images to merge. /// A cancellation token to cancel the operation before time. This will cause an OperationCancelledException to be thrown. /// /// The exception thrown if the image could not be resolved public static ImageIdentifier MergeAndResolve(IEnumerable images, CancellationToken cancellationToken) { var img = new ImageSpecifier(images.SelectMany(x =>x.Packages).Distinct().ToList()); img.Repositories = images.SelectMany(x => x.Repositories).Distinct().ToList(); return img.Resolve(cancellationToken); } /// /// Like Resolve, but includes the current installation as a base. In this case, packages can be upgraded and downgraded, /// but not removed if they do not exist in the image specification. /// internal ImageIdentifier MergeAndResolve(Installation deploymentInstallation, CancellationToken cancellationToken) { var imageSpecifier2 = FromAddedPackages(deploymentInstallation, Packages); imageSpecifier2.Name = Name; if (imageSpecifier2.Repositories?.Any() != true) imageSpecifier2.Repositories = Repositories; imageSpecifier2.OS = OS; imageSpecifier2.Architecture = Architecture; var image = imageSpecifier2.Resolve(cancellationToken); return image; } /// /// Resolve specified packages in the ImageSpecifier with respect to the target installation. /// Specified packages will take precedence over already installed packages /// Already installed packages, which are not specified in the imagespecifier, will remain installed. /// /// OpenTAP installation to merge with and deploy to. /// Standard CancellationToken /// A new Installation /// In case of resolve errors, this method will throw ImageResolveExceptions. public Installation MergeAndDeploy(Installation deploymentInstallation, CancellationToken cancellationToken) { var image = MergeAndResolve(deploymentInstallation, cancellationToken); image.Deploy(deploymentInstallation.Directory, cancellationToken); return new Installation(deploymentInstallation.Directory); } /// /// Create an from JSON or XML value. Throws if value is not valid JSON or XML /// /// JSON or XML formatted /// An public static ImageSpecifier FromString(string value) { return ImageHelper.GetImageFromString(value); } /// Turns an image into a readable string. /// public override string ToString() => string.Join(", ", Packages.Select(x => $"{x.Name}:{x.Version}")); } }