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}"));
}
}