rules_intellij/build_defs/stamp_plugin_xml.py (211 lines of code) (raw):

#!/usr/bin/python3 # # This file is based on Bazel plugin for IntelliJ by The Bazel Authors, licensed under Apache-2.0; # It was modified by JetBrains s.r.o. and contributors # # Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Stamps a plugin xml with build information.""" import argparse import io import re import sys from xml.dom import minidom parser = argparse.ArgumentParser() parser.add_argument( "--plugin_xml", help="The plugin xml file", ) parser.add_argument( "--api_version_txt", help="The file containing the api version info", required=True, ) parser.add_argument( "--stamp_since_build", action="store_true", help="Stamp since-build with the build number", ) parser.add_argument( "--stamp_until_build", action="store_true", help=("Stamp until-build with the major release component of the build " "number"), ) parser.add_argument( "--plugin_id", help="plugin ID to stamp into the plugin.xml", ) parser.add_argument( "--plugin_name", help="plugin name to stamp into the plugin.xml", ) parser.add_argument( "--version", help="Version to stamp into the plugin.xml", ) parser.add_argument( "--version_file", help="Version file to stamp into the plugin.xml", ) parser.add_argument( "--changelog_file", help="Changelog file to add to plugin.xml", ) parser.add_argument( "--description_file", help="File with description element data to add to plugin.xml", ) parser.add_argument( "--vendor_file", help="File with vendor element data to add to plugin.xml", ) parser.add_argument( "--since_build_numbers", metavar="KEY=VALUE", nargs="+", help=("List of key-value pairs to map plugin api versions to the since " "build number to be used for it in plugin.xml"), ) parser.add_argument( "--until_build_numbers", metavar="KEY=VALUE", nargs="+", help=("List of key-value pairs to map plugin api versions to the until " "build number to be used for it in plugin.xml"), ) def parse_key_value_items(items): """Parse key-value parameters and returns them in a dict.""" res = {} for item in items: item_pair = item.split("=", 1) key = item_pair[0].strip() res[key] = item_pair[1] return res def _read_changelog(changelog_file): """Reads the changelog and transforms it into trivial HTML if it's not HTML.""" with io.open(changelog_file, encoding="utf-8") as f: if changelog_file.endswith("html"): return f.read() else: return "\n".join("<p>" + line + "</p>" for line in f.readlines()) def _read_description(description_file): """Reads the description and transforms it into trivial HTML.""" with open(description_file) as f: return f.read() def _read_vendor(vendor_file): """Reads vendor data from an .xml file and returns the vendor element.""" dom = minidom.parse(vendor_file) vendor_elements = dom.getElementsByTagName("vendor") if len(vendor_elements) != 1: raise ValueError("Ambigious or missing vendor element (%d elements)" % len(vendor_elements)) return vendor_elements[0] def _strip_product_code(api_version): """Strips the product code from the api version string.""" match = re.match(r"^([A-Z]+-)?([0-9]+)((\.[0-9]+)*)", api_version) if match is None: raise ValueError("Invalid build number: " + api_version) return match.group(2) + match.group(3) def _parse_major_version(api_version): """Extracts the major version number from a full api version string.""" match = re.match(r"^([A-Z]+-)?([0-9]+)((\.[0-9]+)*)", api_version) if match is None: raise ValueError("Invalid build number: " + api_version) return match.group(2) def _strip_build_number(api_version): """Removes the build number component from a full api version string. If there are more than 2 version number components, return the first 2 components. Some IDEs do not report their full build version to JetBrains, so plugins built against a version with more components may not be discoverable even in a compatible IDE version. Args: api_version: A version string containing the main version and build number. Returns: The first two components of the version string. Raise: ValueError: An incorrectly formatted version string. """ if re.match(r"^([A-Z]+-)?([0-9]+)(\.[0-9]+)+$", api_version): return ".".join(api_version.split(".")[:2]) else: raise ValueError("Unsupported API version %s - the version must be of " % api_version + "the form <alphanum>.<num>, with at least two components.") def main(): args = parser.parse_args() if args.plugin_xml: dom = minidom.parse(args.plugin_xml) else: dom = minidom.parseString("<idea-plugin/>") since_build_numbers = {} until_build_numbers = {} if args.since_build_numbers: since_build_numbers = parse_key_value_items(args.since_build_numbers) if args.until_build_numbers: until_build_numbers = parse_key_value_items(args.until_build_numbers) is_eap = False with open(args.api_version_txt) as f: api_version = f.readline().strip() if api_version.endswith(" EAP"): is_eap = True api_version = api_version[:-4] new_elements = [] idea_plugin = dom.documentElement if args.version and args.version_file: raise ValueError("Cannot supply both version and version_file") if args.version or args.version_file: version_elements = idea_plugin.getElementsByTagName("version") for element in version_elements: idea_plugin.removeChild(element) version_element = dom.createElement("version") new_elements.append(version_element) version_value = None if args.version: version_value = args.version else: with open(args.version_file) as f: version_value = f.read().strip() version_text = dom.createTextNode(version_value) version_element.appendChild(version_text) if args.stamp_since_build or args.stamp_until_build: if idea_plugin.getElementsByTagName("idea-version"): raise ValueError("idea-version element already present") idea_version_element = dom.createElement("idea-version") new_elements.append(idea_version_element) build_version = _parse_major_version(api_version) if args.stamp_since_build: if build_version in since_build_numbers.keys(): idea_version_element.setAttribute("since-build", since_build_numbers[build_version]) else: idea_version_element.setAttribute("since-build", build_version) if args.stamp_until_build: if build_version in until_build_numbers.keys(): idea_version_element.setAttribute("until-build", until_build_numbers[build_version]) else: if is_eap: idea_version_element.setAttribute("until-build", _parse_major_version(build_version) + ".*") else: idea_version_element.setAttribute("until-build", build_version + ".*") if args.changelog_file: if idea_plugin.getElementsByTagName("change-notes"): raise ValueError("change-notes element already in plugin.xml") changelog_element = dom.createElement("change-notes") changelog_text = _read_changelog(args.changelog_file) changelog_cdata = dom.createCDATASection(changelog_text) changelog_element.appendChild(changelog_cdata) new_elements.append(changelog_element) if args.plugin_id: if idea_plugin.getElementsByTagName("id"): raise ValueError("id element already in plugin.xml") id_element = dom.createElement("id") new_elements.append(id_element) id_text = dom.createTextNode(args.plugin_id) id_element.appendChild(id_text) if args.plugin_name: if idea_plugin.getElementsByTagName("name"): raise ValueError("name element already in plugin.xml") name_element = dom.createElement("name") new_elements.append(name_element) name_text = dom.createTextNode(args.plugin_name) name_element.appendChild(name_text) if args.description_file: if idea_plugin.getElementsByTagName("description"): raise ValueError("description element already in plugin.xml") description_element = dom.createElement("description") description_text = _read_description(args.description_file) description_cdata = dom.createCDATASection(description_text) description_element.appendChild(description_cdata) new_elements.append(description_element) if args.vendor_file: if idea_plugin.getElementsByTagName("vendor"): raise ValueError("vendor element already in plugin.xml") vendor_element = dom.createElement("vendor") vendor_src_element = _read_vendor(args.vendor_file) vendor_element.setAttribute("email", vendor_src_element.getAttribute("email")) vendor_element.setAttribute("url", vendor_src_element.getAttribute("url")) vendor_text = dom.createTextNode(vendor_src_element.firstChild.data) vendor_element.appendChild(vendor_text) new_elements.append(vendor_element) for new_element in new_elements: idea_plugin.appendChild(new_element) sys.stdout.buffer.write(dom.toxml(encoding="utf-8")) if __name__ == "__main__": main()