using OpenTap.Cli; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Xml; #pragma warning disable 1591 // TODO: Add XML Comments in this file, then remove this namespace OpenTap.Package { [Display("show", Group: "package", Description: "Show information about a package.")] public class PackageShowAction : LockingPackageAction { private string Description { get; set; } private List> SubTags { get; } = new List>(); private void ParseDescription(string description) { var doc = new XmlDocument(); doc.LoadXml($"{description}"); foreach (object tag in doc["Description"]) { if (tag is XmlText t) { Description = t.InnerText.Trim(); } else if (tag is XmlElement e) { var text = e.InnerText.Trim(); if (!string.IsNullOrWhiteSpace(text)) SubTags.Add(new Tuple(e.Name, text)); } } } private void AddWritePair(string key, string value) { if (!string.IsNullOrWhiteSpace(value)) { keys.Add(key); values.Add(value); } } private void WritePairs() { var justify = keys.Max(x => x.Length) + 1; int consoleWidth; try { consoleWidth = Console.WindowWidth; } catch // No handle to a console window -- default to something reasonable { consoleWidth = 120; } if (consoleWidth < 10) // assume an error and just use 120 as default. consoleWidth = 120; var wrapLength = Math.Min(consoleWidth - justify - 3, 100); for (int i = 0; i < keys.Count; i++) { var key = keys[i].PadRight(justify); var value = string.Join(Environment.NewLine.PadRight(justify + 2 + Environment.NewLine.Length), WordWrap(values[i], wrapLength).Where(x => !string.IsNullOrWhiteSpace(x))); log.Info($"{key}: {value}"); } } private List values = new List(); private List keys = new List(); [CommandLineArgument("repository", Description = CommandLineArgumentRepositoryDescription, ShortName = "r")] public string[] Repository { get; set; } [CommandLineArgument("offline", Description = "Don't check http repositories.", ShortName = "o")] public bool Offline { get; set; } [UnnamedCommandLineArgument("package", Required = true, Description = "The name of the package to show information for. Can be a .TapPackage file, or a package name to be resolved from the specified repositories.")] public string Name { get; set; } [CommandLineArgument("version", Description = CommandLineArgumentVersionDescription)] public string Version { get; set; } [CommandLineArgument("os", Description = CommandLineArgumentOsDescription)] public string OS { get; set; } [CommandLineArgument("architecture", Description = CommandLineArgumentArchitectureDescription)] public CpuArchitecture Architecture { get; set; } [CommandLineArgument("include-files", Description = "List all files included in the package")] public bool IncludeFiles { get; set; } = false; [CommandLineArgument("include-plugins", Description = "List all plugins included in the package")] public bool IncludePlugins { get; set; } = false; private List repositories = new List(); private VersionSpecifier versionSpec { get; set; } private void DisableHttpRepositories() { repositories = new List(repositories.OfType().ToList()); } private PackageDef GetPackageDef(Installation targetInstallation) { // Try a number of methods to obtain the PackageDef in order of precedence var packageRef = new PackageSpecifier(Name, VersionSpecifier.Parse(Version ?? ""), Architecture, OS); PackageDef package; // a relative or absolute file path if (File.Exists(Name)) { package = PackageDef.FromPackage(Name); if (package != null) { Offline = true; return package; } } // a currently installed package package = targetInstallation.GetPackages().FirstOrDefault(p => p.Name == Name && versionSpec.IsCompatible(p.Version)); if (package != null) return package; if (Offline == false) { try { // a release from repositories package = repositories.SelectMany(x => x.GetPackages(packageRef)) .FindMax(p => p.Version); if (package != null) return package; } catch (System.Net.WebException e) { // not connected to the internet log.Error(e.Message); log.Warning("Could not connect to repository. Showing results for local install"); DisableHttpRepositories(); package = repositories.SelectMany(x => x.GetPackages(packageRef)) .FindMax(p => p.Version); if (package != null) return package; } } if (Offline == false && string.IsNullOrWhiteSpace(Version)) { // a prerelease from repositories packageRef = new PackageSpecifier(Name, VersionSpecifier.Parse("any"), Architecture, OS); package = repositories.SelectMany(x => x.GetPackages(packageRef)) .FindMax(p => p.Version); } return package; } protected override int LockedExecute(CancellationToken cancellationToken) { repositories = PackageManagerSettings.Current.GetEnabledRepositories(Repository); if (Offline) { DisableHttpRepositories(); } Name = AutoCorrectPackageNames.Correct(new[] { Name }, repositories)[0]; if (Target == null) Target = FileSystemHelper.GetCurrentInstallationDirectory(); versionSpec = VersionSpecifier.Any; if (!String.IsNullOrWhiteSpace(Version)) { versionSpec = VersionSpecifier.Parse(Version); } var targetInstallation = new Installation(Target); PackageDef package = GetPackageDef(targetInstallation); if (package == null) { var versionString = string.IsNullOrWhiteSpace(Version) ? "" : $" version '{Version}'"; throw new ExitCodeException((int)ExitCodes.ArgumentError, $"Package '{Name}'{versionString} not found in specified repositories or local install."); } var packageVersions = Offline ? new List() : repositories.SelectMany(repo => repo.GetPackageVersions(package.Name, cancellationToken, null)) .Where(p => versionSpec.IsCompatible(p.Version)); // Remove prereleases if found package is a release if (string.IsNullOrWhiteSpace(package.Version.PreRelease)) packageVersions = packageVersions.Where(p => string.IsNullOrWhiteSpace(p.Version.PreRelease)); if (Architecture != CpuArchitecture.Unspecified) packageVersions = packageVersions.Where(p => ArchitectureHelper.CompatibleWith(Architecture, p.Architecture)); if (!string.IsNullOrWhiteSpace(OS)) packageVersions = packageVersions.Where(p => p.OS.Contains(OS)); GetPackageInfo(package, packageVersions.ToList(), targetInstallation); return (int)ExitCodes.Success; } private void GetPackageInfo(PackageDef package, List packageVersions, Installation installation) { var allPackages = installation.GetPackages(); var latestVersion = packageVersions?.Max(p => p.Version); var packageInstalled = allPackages.Contains(package); if (!packageInstalled) allPackages.Add(package); var tree = DependencyAnalyzer.BuildAnalyzerContext(allPackages); var issues = tree.GetIssues(package); ParseDescription(package.Description); // Get similar releases to get available platforms and architectures var similarReleases = packageVersions.Where(x => x.Version.Major == package.Version.Major && x.Version.Minor == package.Version.Minor).ToList(); AddWritePair("Package Name", package.Name); AddWritePair("Group", package.Group); var installedVersion = installation.GetPackages().Where(x => x.Name == package.Name)?.FirstOrDefault()?.Version; var installedString = installedVersion == null ? "(not installed)" : $"({installedVersion} installed)"; AddWritePair("Version", $"{package.Version} {installedString}"); if (latestVersion != null && installedVersion != null && latestVersion != installedVersion) AddWritePair("Newest Version in Repository", $"{latestVersion}"); AddWritePair("Compatible Architectures", string.Join(Environment.NewLine, similarReleases.Select(x => x.Architecture).Distinct())); var platforms = similarReleases .SelectMany(x => x.OS.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)) .Distinct(StringComparer.OrdinalIgnoreCase); AddWritePair("Compatible Platforms", string.Join(Environment.NewLine, platforms)); if (packageInstalled == false) { AddWritePair("Compatibility Issues", issues.Any() ? $"{(string.Join(Environment.NewLine, issues.Select(x => $"{x.PackageName} - {Utils.EnumToReadableString(x.IssueType)}")))}" : "none"); } AddWritePair("Owner", package.Owner); AddWritePair("Source License", package.SourceLicense); var licenseString = (package.LicenseRequired ?? "").Replace("&", " and ").Replace("|", " or "); AddWritePair("License Required", licenseString); AddWritePair("SourceUrl", package.SourceUrl); if (package.PackageSource is IRepositoryPackageDefSource repoPkg) AddWritePair("Repository", repoPkg.RepositoryUrl); AddWritePair("Package Type", package.FileType); AddWritePair("Package Class", package.Class); AddWritePair("Architecture", package.Architecture.ToString()); AddWritePair("Platform", package.OS ?? ""); var tags = package.Tags?.Split(new string[] { " ", "," }, StringSplitOptions.RemoveEmptyEntries); if (tags?.Length > 0) AddWritePair("Package Tags", string.Join(" ", tags)); if (package.Dependencies.Count > 0) { var pad = package.Dependencies.Select(x => x.Name.Length).Max(); var aligned = package.Dependencies.Select(pkg => $"{pkg.Name.PadRight(pad)} - {pkg.Version}"); AddWritePair("Dependencies", string.Join(Environment.NewLine, aligned)); } foreach (var (key, value) in SubTags) { AddWritePair(key, value); } AddWritePair("Description", Description); if (IncludeFiles) { if (package.Files.Count > 0) { AddWritePair("Files", string.Join(Environment.NewLine, package.Files.Select(x => x.RelativeDestinationPath.Replace("\\", "/")))); } } if (IncludePlugins) { var plugins = package.Files.Select(x => x.Plugins); var sb = new StringBuilder(); foreach (var plugin in plugins) { foreach (var p in plugin) { var name = p.Name; var desc = p.Description; sb.Append(!string.IsNullOrWhiteSpace(desc) ? $"{name} ({desc}){Environment.NewLine}" : $"{name}{Environment.NewLine}"); } } AddWritePair("Plugins", sb.ToString()); } if (package.MetaData.Any()) { foreach (var metadata in package.MetaData) AddWritePair(metadata.Key, metadata.Value); } WritePairs(); } private IEnumerable WordWrap(string input, int breakLength) { // In addition to whitespace, break on these characters var breakableCharacters = new List() { '-', ' ', '\t', '\n' }; var progress = 0; var currentLength = 0; var lastBreakableCharacter = -1; foreach (var c in input) { currentLength++; if (breakableCharacters.Contains(c)) lastBreakableCharacter = currentLength; // Break if line length exceeds break length, or if the sentence contains a literal newline if (currentLength > breakLength || c == '\n') { // Keep reading until we can break if (lastBreakableCharacter == -1) continue; yield return input.Substring(progress, lastBreakableCharacter).Trim(); progress += lastBreakableCharacter; currentLength -= lastBreakableCharacter; lastBreakableCharacter = -1; } } yield return input.Substring(progress).Trim(); } } }