blob: ca32d767ff17c2273726e2286fb3b6031cad7a87 [file] [log] [blame]
"""A tool to check consistency of an intellij plugin."""
import argparse
import re
import sys
import xml.etree.ElementTree as ET
import zipfile
def load_include(include, external_xmls, cwd, index):
href = include.get("href")
parse = include.get("parse", "xml")
if parse != "xml":
print("only xml parse is supported")
sys.exit(1)
xpointer = include.get("xpointer")
xpath = None
if xpointer:
m = re.match(r"xpointer\((.*)\)", xpointer)
if not m:
print("only xpointers of the form xpointer(xpath) are supported")
sys.exit(1)
xpath = m.group(1)
is_optional = any(child.tag == "{http://www.w3.org/2001/XInclude}fallback" for child in include)
rel = href[1:] if href.startswith("/") else cwd + "/" + href
if rel in external_xmls or is_optional:
return [], None
new_cwd = rel[0:rel.rindex("/")]
if rel not in index:
print("Cannot find file to include %s" % href)
sys.exit(1)
with zipfile.ZipFile(index[rel]) as jar:
res = jar.read(rel)
e = ET.fromstring(res)
if not xpath:
return [e], new_cwd
if not xpath.startswith("/"):
print("Unexpected xpath %s. Only absolute paths are supported" % xpath)
sys.exit(1)
ret = []
root, path = xpath[1:].split("/", 1)
if root == e.tag:
ret = e.findall("./" + path)
if not ret:
print("While including %s, the path %s," % (rel, xpath))
print("did not produce any elements to include")
sys.exit(1)
return ret, new_cwd
def resolve_includes(elem, external_xmls, cwd, index):
""" Resolves xincludes in the given xml element. By replacing xinclude tags like
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="/META-INF/android-plugin.xml" xpointer="xpointer(/idea-plugin/*)"/>
...
with the xml pointed by href and the xpath given in xpointer.
"""
i = 0
while i < len(elem):
e = elem[i]
if e.tag == "{http://www.w3.org/2001/XInclude}include":
nodes, new_cwd = load_include(e, external_xmls, cwd, index)
subtree = ET.Element("nodes")
subtree.extend(nodes)
resolve_includes(subtree, external_xmls, new_cwd, index)
nodes = list(subtree)
if nodes:
for node in nodes[:-1]:
elem.insert(i, node)
i = i + 1
node = nodes[len(nodes)-1]
if e.tail:
node.tail = (node.tail or "") + e.tail
elem[i] = node
else:
resolve_includes(e, external_xmls, cwd, index)
i = i + 1
def check_plugin(plugin_id, files, deps, external_xmls, out):
xmls = {}
index = {}
for file in files:
if file.endswith(".jar"):
with zipfile.ZipFile(file) as jar:
for jar_entry in jar.namelist():
if jar_entry == "META-INF/plugin.xml":
xmls[file + "!" + jar_entry] = jar.read(jar_entry)
if not jar_entry.endswith("/"):
# TODO: Investigate if we can have a strict mode where we fail on duplicate
# files across jars in the same plugin. Currently even IJ plugins fail with
# such a check as they have even .class files duplicated in the same plugin.
index[jar_entry] = file
if len(xmls) != 1:
msg = "\n".join(xmls.keys())
print("Plugin should have exactly one plugin.xml file (found %d)" % len(xmls))
print(msg)
sys.exit(1)
_, xml = list(xmls.items())[0]
element = ET.fromstring(xml)
ids = [id.text for id in element.findall("id")]
if not ids:
# If id is not found, IJ uses name
# https://jetbrains.org/intellij/sdk/docs/basics/plugin_structure/plugin_configuration_file.html
ids = [id.text for id in element.findall("name")]
if len(ids) != 1:
print("Expected exactly one id, but found [%s]" % ",".join(ids))
sys.exit(1)
found_id = ids[0]
# We cannot use ElementInclude because it does not support xpointer
resolve_includes(element, external_xmls, "META-INF", index)
if plugin_id and found_id != plugin_id:
print("Expected plugin id to be %d, but found %s" % (plugin_id, found_id))
sys.exit(1)
if element.tag != 'idea-plugin':
print("Expected plugin.xml root item to be 'idea-plugin' but was %s" % element.tag)
sys.exit(1)
if element.attrib.get("allow-bundled-update", "false") != "false" and found_id != "org.jetbrains.kotlin":
print("Bundled plugin update are not allowed for plugin: %s" % found_id)
sys.exit(1)
if deps is not None:
depends_xml = set()
for e in element.findall("depends"):
# We only validate plugin dependencies not module ones
if e.text in [
"com.intellij.modules.java",
"com.intellij.modules.lang",
"com.intellij.modules.platform",
"com.intellij.modules.vcs",
"com.intellij.modules.xdebugger",
"com.intellij.modules.xml",
"com.intellij.modules.androidstudio",
]:
continue
# Ignore optional dependencies, some are against IJ ultimate plugins which we don't have
if e.get("optional") == "true":
continue
depends_xml.add(e.text)
depends_build = set()
for d in deps:
with open(d, "r") as info:
depends_build.add(info.read())
if depends_build != depends_xml:
print("Error while checking plugin dependencies")
for d in depends_build - depends_xml:
print("The build depends on plugin \"%s\", but this dependency is not declared in the plugin.xml." % d)
for d in depends_xml - depends_build:
print("The plugin.xml declares a dependency on \"%s\", but it's not declared in the build." % d)
sys.exit(1)
with open(out, "w") as info:
info.write(found_id)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--files",
dest="files",
nargs="+",
help="Path to files included in the plugin.")
parser.add_argument(
"--deps",
dest="deps",
default=None,
nargs="*",
help="Ids of the plugins this plugin depends on.")
parser.add_argument(
"--external_xmls",
dest="external_xmls",
default=[],
nargs="*",
help="xmls files that this plugin can include but are not present.")
parser.add_argument(
"--plugin_id",
dest="plugin_id",
help="The expected id of this plugin.")
parser.add_argument(
"--out",
dest="out",
help="Path to a file where to save the plugin information.")
args = parser.parse_args()
check_plugin(args.plugin_id, args.files, args.deps, args.external_xmls, args.out)