/// <summary> /// Update well-known version files. /// </summary> /// <param name="versionProps">Versions.props xml document</param> /// <param name="token">Global.json document</param> /// <param name="itemToUpdate">Item that needs an update.</param> /// <remarks> /// TODO: https://github.com/dotnet/arcade/issues/1095 /// </remarks> private void UpdateVersionFiles(XmlDocument versionProps, JToken token, DependencyDetail itemToUpdate) { string versionElementName = VersionFiles.GetVersionPropsPackageVersionElementName(itemToUpdate.Name); string alternateVersionElementName = VersionFiles.GetVersionPropsAlternatePackageVersionElementName(itemToUpdate.Name); // Select nodes case insensitively, then update the name. XmlNode packageVersionNode = versionProps.DocumentElement.SelectSingleNode( $"//*[translate(local-name(),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')=" + $"'{versionElementName.ToLower()}']"); string foundElementName = versionElementName; // Find alternate names if (packageVersionNode == null) { packageVersionNode = versionProps.DocumentElement.SelectSingleNode( $"//*[translate(local-name(),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')=" + $"'{alternateVersionElementName.ToLower()}']"); foundElementName = alternateVersionElementName; } if (packageVersionNode != null) { packageVersionNode.InnerText = itemToUpdate.Version; // If the node name was updated, then create a new node with the new name, unlink this node // and create a new one in the same location. if (packageVersionNode.LocalName != foundElementName) { { XmlNode parentNode = packageVersionNode.ParentNode; XmlNode newPackageVersionElement = versionProps.CreateElement( foundElementName, versionProps.DocumentElement.NamespaceURI); newPackageVersionElement.InnerText = itemToUpdate.Version; parentNode.ReplaceChild(newPackageVersionElement, packageVersionNode); } { // Update the package name element too. string packageNameElementName = VersionFiles.GetVersionPropsPackageElementName(itemToUpdate.Name); XmlNode packageNameNode = versionProps.DocumentElement.SelectSingleNode( $"//*[translate(local-name(),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')=" + $"'{packageNameElementName.ToLower()}']"); if (packageNameNode != null) { XmlNode parentNode = packageNameNode.ParentNode; XmlNode newPackageNameElement = versionProps.CreateElement( packageNameElementName, versionProps.DocumentElement.NamespaceURI); newPackageNameElement.InnerText = itemToUpdate.Name; parentNode.ReplaceChild(newPackageNameElement, packageNameNode); } } } } // Update the global json too, even if there was an element in the props file, in case // it was listed in both UpdateVersionGlobalJson(itemToUpdate, token); }
private void UpdateVersionGlobalJson(DependencyDetail itemToUpdate, JToken token) { string versionElementName = VersionFiles.CalculateGlobalJsonElementName(itemToUpdate.Name); foreach (JProperty property in token.Children <JProperty>()) { if (property.Name == versionElementName) { property.Value = new JValue(itemToUpdate.Version); break; } UpdateVersionGlobalJson(itemToUpdate, property.Value); } }
/// <summary> /// Verify that any dependency that exists in global.json and Version.Details.xml (e.g. Arcade SDK) /// has matching version numbers. /// </summary> /// <param name="dependencies">Parsed dependencies in the repository.</param> /// <param name="rootToken">Root global.json token.</param> /// <returns></returns> private Task <bool> VerifyMatchingGlobalJson( IEnumerable <DependencyDetail> dependencies, JObject rootToken, out Task <HashSet <string> > utilizedDependencies) { HashSet <string> utilizedSet = new HashSet <string>(); bool result = true; foreach (var dependency in dependencies) { string versionedName = VersionFiles.CalculateGlobalJsonElementName(dependency.Name); JToken dependencyNode = FindDependency(rootToken, versionedName); if (dependencyNode != null) { // Should be a string with matching version. if (dependencyNode.Type != JTokenType.Property || ((JProperty)dependencyNode).Value.Type != JTokenType.String) { _logger.LogError($"The element '{dependency.Name}' in '{VersionFiles.GlobalJson}' should be a property " + $"with a value of type string."); result = false; continue; } JProperty property = (JProperty)dependencyNode; // Validate that the casing matches for consistency if (property.Name != versionedName) { _logger.LogError($"The dependency '{dependency.Name}' has a case mismatch between " + $"'{VersionFiles.GlobalJson}' and '{VersionFiles.VersionDetailsXml}' " + $"('{property.Name}' vs. '{versionedName}')"); result = false; } // Validate version JToken value = (JToken)property.Value; if (value.Value <string>() != dependency.Version) { _logger.LogError($"The dependency '{dependency.Name}' has a version mismatch between " + $"'{VersionFiles.GlobalJson}' and '{VersionFiles.VersionDetailsXml}' " + $"('{value.Value<string>()}' vs. '{dependency.Version}')"); } utilizedSet.Add(dependency.Name); } } utilizedDependencies = Task.FromResult(utilizedSet); return(Task.FromResult(result)); }
/// <summary> /// Verify that any dependency that exists in both Version.props and Version.Details.xml has matching version numbers. /// </summary> /// <param name="dependencies">Parsed dependencies in the repository.</param> /// <param name="versionProps">Parsed version props file</param> /// <returns></returns> private Task <bool> VerifyMatchingVersionProps(IEnumerable <DependencyDetail> dependencies, XmlDocument versionProps, out Task <HashSet <string> > utilizedDependencies) { HashSet <string> utilizedSet = new HashSet <string>(); bool result = true; foreach (var dependency in dependencies) { string versionElementName = VersionFiles.GetVersionPropsPackageVersionElementName(dependency.Name); string alternateVersionElementName = VersionFiles.GetVersionPropsAlternatePackageVersionElementName(dependency.Name); XmlNode versionNode = versionProps.DocumentElement.SelectSingleNode($"//*[local-name()='{versionElementName}']"); if (versionNode == null) { versionNode = versionProps.DocumentElement.SelectSingleNode($"//*[local-name()='{alternateVersionElementName}']"); versionElementName = alternateVersionElementName; } if (versionNode != null) { // Validate that the casing matches for consistency if (versionNode.Name != versionElementName) { _logger.LogError($"The dependency '{dependency.Name}' has a case mismatch between " + $"'{VersionFiles.VersionProps}' and '{VersionFiles.VersionDetailsXml}' " + $"('{versionNode.Name}' vs. '{versionElementName}')"); result = false; } // Validate innner version matches if (versionNode.InnerText != dependency.Version) { _logger.LogError($"The dependency '{dependency.Name}' has a version mismatch between " + $"'{VersionFiles.VersionProps}' and '{VersionFiles.VersionDetailsXml}' " + $"('{versionNode.InnerText}' vs. '{dependency.Version}')"); result = false; } utilizedSet.Add(dependency.Name); } } utilizedDependencies = Task.FromResult(utilizedSet); return(Task.FromResult(result)); }
/// <summary> /// Add a dependency to Versions.props. This has the form: /// <!-- Package names --> /// <PropertyGroup> /// <MicrosoftDotNetApiCompatPackage>Microsoft.DotNet.ApiCompat</MicrosoftDotNetApiCompatPackage> /// </PropertyGroup> /// /// <!-- Package versions --> /// <PropertyGroup> /// <MicrosoftDotNetApiCompatPackageVersion>1.0.0-beta.18478.5</MicrosoftDotNetApiCompatPackageVersion> /// </PropertyGroup> /// /// See https://github.com/dotnet/arcade/blob/master/Documentation/DependencyDescriptionFormat.md for more /// information. /// </summary> /// <param name="repo">Path to Versions.props file</param> /// <param name="dependency">Dependency information to add.</param> /// <returns>Async task.</returns> public async Task AddDependencyToVersionsPropsAsync(string repo, string branch, DependencyDetail dependency) { XmlDocument versionProps = await ReadVersionPropsAsync(repo, null); string documentNamespaceUri = versionProps.DocumentElement.NamespaceURI; string packageNameElementName = VersionFiles.GetVersionPropsPackageElementName(dependency.Name); string packageVersionElementName = VersionFiles.GetVersionPropsPackageVersionElementName(dependency.Name); string packageVersionAlternateElementName = VersionFiles.GetVersionPropsAlternatePackageVersionElementName( dependency.Name); // Select elements by local name, since the Project (DocumentElement) element often has a default // xmlns set. XmlNodeList propertyGroupNodes = versionProps.DocumentElement.SelectNodes($"//*[local-name()='PropertyGroup']"); XmlNode newPackageNameElement = versionProps.CreateElement(packageNameElementName, documentNamespaceUri); newPackageNameElement.InnerText = dependency.Name; bool addedPackageVersionElement = false; bool addedPackageNameElement = false; // There can be more than one property group. Find the appropriate one containing an existing element of // the same type, and add it to the parent. foreach (XmlNode propertyGroupNode in propertyGroupNodes) { if (propertyGroupNode.HasChildNodes) { foreach (XmlNode propertyNode in propertyGroupNode.ChildNodes) { if (!addedPackageVersionElement && propertyNode.Name.EndsWith(VersionFiles.VersionPropsVersionElementSuffix)) { XmlNode newPackageVersionElement = versionProps.CreateElement( packageVersionElementName, documentNamespaceUri); newPackageVersionElement.InnerText = dependency.Version; propertyGroupNode.AppendChild(newPackageVersionElement); addedPackageVersionElement = true; break; } // Test for alternate suffixes. This will eventually be replaced by repo-level configuration: // https://github.com/dotnet/arcade/issues/1095 else if (!addedPackageVersionElement && propertyNode.Name.EndsWith( VersionFiles.VersionPropsAlternateVersionElementSuffix)) { XmlNode newPackageVersionElement = versionProps.CreateElement( packageVersionAlternateElementName, documentNamespaceUri); newPackageVersionElement.InnerText = dependency.Version; propertyGroupNode.AppendChild(newPackageVersionElement); addedPackageVersionElement = true; break; } else if (!addedPackageNameElement && propertyNode.Name.EndsWith(VersionFiles.VersionPropsPackageElementSuffix)) { propertyGroupNode.AppendChild(newPackageNameElement); addedPackageNameElement = true; break; } } if (addedPackageVersionElement && addedPackageNameElement) { break; } } } // Add the property groups if none were present if (!addedPackageVersionElement) { // If the repository doesn't have any package version element, then // use the non-alternate element name. XmlNode newPackageVersionElement = versionProps.CreateElement(packageVersionElementName, documentNamespaceUri); newPackageVersionElement.InnerText = dependency.Version; XmlNode propertyGroupElement = versionProps.CreateElement("PropertyGroup", documentNamespaceUri); XmlNode propertyGroupCommentElement = versionProps.CreateComment("Package versions"); versionProps.DocumentElement.AppendChild(propertyGroupCommentElement); versionProps.DocumentElement.AppendChild(propertyGroupElement); propertyGroupElement.AppendChild(newPackageVersionElement); } if (!addedPackageNameElement) { XmlNode propertyGroupElement = versionProps.CreateElement("PropertyGroup", documentNamespaceUri); XmlNode propertyGroupCommentElement = versionProps.CreateComment("Package names"); versionProps.DocumentElement.AppendChild(propertyGroupCommentElement); versionProps.DocumentElement.AppendChild(propertyGroupElement); propertyGroupElement.AppendChild(newPackageNameElement); } // TODO: This should not be done here. This should return some kind of generic file container to the caller, // who will gather up all updates and then call the git client to write the files all at once: // https://github.com/dotnet/arcade/issues/1095. Today this is only called from the Local interface so // it's okay for now. var file = new GitFile(VersionFiles.VersionProps, versionProps); await _gitClient.PushFilesAsync(new List <GitFile> { file }, repo, branch, $"Add {dependency} to " + $"'{VersionFiles.VersionProps}'"); _logger.LogInformation( $"Dependency '{dependency.Name}' with version '{dependency.Version}' successfully added to " + $"'{VersionFiles.VersionProps}'"); }