using System; using System.Linq; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; namespace OpenTap.Package { [Display("Include Package Dependencies")] class IncludePackageDependencies : ICustomPackageAction { /// /// Defines the IncludePackageDependencies XML element that indicates that the package should inherit plugin dependencies from this file. /// [Display("IncludePackageDependencies")] class IncludePackageDependenciesData : ICustomPackageData { } /// /// It doesn't really matter when this is run. /// /// public int Order() => 10; public PackageActionStage ActionStage => PackageActionStage.Create; private static TraceSource log = Log.CreateSource(nameof(IncludePackageDependencies)); public bool Execute(PackageDef pkgDef, CustomPackageActionArgs customActionArgs) { // Add all package dependencies specified in an xml file with the following hierarchy to 'pkgDef' // // // bool success = true; foreach (var item in pkgDef.Files) { // Dependencies should not be included from this file -- skip if (item.HasCustomData() == false) continue; XDocument document; try { document = XDocument.Load(item.FileName, LoadOptions.SetLineInfo | LoadOptions.PreserveWhitespace); } catch { log.Error($"File '{item.FileName}' is not a valid XML document."); success = false; continue; } var selector = "//Package.Dependencies/Package"; var nodes = document.XPathSelectElements(selector).ToArray(); // No dependencies specified in the file -- skip if (nodes.Length == 0) continue; log.Info($"Inheriting package dependencies from {item.FileName}:"); string AttributeError(string errorMessage, XElement ele) { string details; if (ele is IXmlLineInfo l && l.HasLineInfo()) details = $"({ele} - line {l.LineNumber})"; else details = $"({ele})"; var msg = $"{errorMessage} {details}"; return msg; } foreach (var node in nodes) { if (!(node is XElement ele)) continue; var thisName = ele.Attribute("Name")?.Value; if (string.IsNullOrWhiteSpace(thisName)) { log.Error(AttributeError("Attribute 'Name' is not set.", ele)); success = false; continue; } var thisVersion = ele.Attribute("Version")?.Value; VersionSpecifier thisVersionSpec; SemanticVersion thisSemver; var thisAny = string.IsNullOrWhiteSpace(thisVersion) || thisVersion.Equals("any", StringComparison.OrdinalIgnoreCase); try { if (thisAny) { thisVersionSpec = VersionSpecifier.Any; thisVersion = "any"; thisSemver = null; } else { if (thisVersion.StartsWith("^")) { thisVersionSpec = VersionSpecifier.Parse(thisVersion); thisSemver = SemanticVersion.Parse(thisVersion.Substring(1)); } else { thisVersionSpec = VersionSpecifier.Parse("^" + thisVersion); thisSemver = SemanticVersion.Parse(thisVersion); } } } catch { log.Error("Attribute 'Version' is not a valid version specifier.", ele); success = false; continue; } var thisDep = new PackageDependency(thisName, thisVersionSpec, thisVersion); // Avoid adding duplicate entries var existing = pkgDef.Dependencies.FirstOrDefault(d => d.Name == thisName); // Determine if we should replace the existing version if (existing != null) { var otherAny = existing.RawVersion.Equals("any", StringComparison.OrdinalIgnoreCase); var otherSemver = otherAny ? null : SemanticVersion.Parse(existing.RawVersion); // If the existing dependency is compatible with this one, we can safely ignore it if (thisAny || (otherAny == false && thisVersionSpec.IsCompatible(otherSemver))) continue; // If the new version is compatible with the previous one, we can safely replace the previous version. if (existing.Version.IsCompatible(thisSemver) || otherAny) { pkgDef.Dependencies.Remove(existing); pkgDef.Dependencies.Add(thisDep); } // Otherwise the two versions are in conflict. This error is not recoverable else { log.Error($"Dependency conflict: {thisName} v. '{thisVersion}' and '{existing.RawVersion}' are mutually exclusive."); success = false; continue; } } // Otherwise add a new dependency for this element else { pkgDef.Dependencies.Add(thisDep); } } item.RemoveCustomData(); } return success; } } }