From 58f822cfddb880d40b85e6820937264d513e835f Mon Sep 17 00:00:00 2001 From: Jamil RAICHOUNI Date: Sun, 17 Mar 2024 12:02:15 +0100 Subject: [PATCH] wip --- eclipse_extension_builders/__main__.py | 227 ++++++++++++++++++++++++- pyproject.toml | 7 +- 2 files changed, 229 insertions(+), 5 deletions(-) diff --git a/eclipse_extension_builders/__main__.py b/eclipse_extension_builders/__main__.py index 42d596f..08b619d 100644 --- a/eclipse_extension_builders/__main__.py +++ b/eclipse_extension_builders/__main__.py @@ -1,13 +1,46 @@ -# Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 -"""Main entry point into eclipse_extension_builders.""" +"""Tools to build, pack, deploy Eclipse extensions. + +The present CLI needs the command line tools `jar` and `mvn` to be installed +and accessible via the user's PATH. + +`jar` is part of the Java Development Kit (JDK) and is used to create the jar +file of the Eclipse extension. + +`mvn` is the Maven build tool and is used to analyse the dependencies listed +in the `pom.xml` file to build the `.classpath` file. +""" + +import pathlib +import shutil +import subprocess +import sys +import tempfile import click +import lxml.builder +import lxml.etree import eclipse_extension_builders +E = lxml.builder.ElementMaker() +MANIFEST_PATH = "META-INF/MANIFEST.MF" +PLUGIN_XML_PATH = "plugin.xml" +PATH_BLACKLIST = ( + ".pde.", + "/jre/", + "/org.eclipse.equinox.p2.repository/", + "ant", + "artifacts.jar", + "content.jar", + "ease", + "egit", + "jdt.debug", + "jgit", + "pydev", +) + -@click.command() +@click.group() @click.version_option( version=eclipse_extension_builders.__version__, prog_name="eclipse-extension-builders", @@ -17,5 +50,191 @@ def main() -> None: """Console script for eclipse_extension_builders.""" +def _paths() -> tuple[pathlib.Path, pathlib.Path]: + """Return the path to the jar file.""" + classpath_root = _read_xml_file(".classpath") + output = classpath_root.xpath('//classpathentry[@kind="output"]') + if not output: + click.echo( + "Output directory not found. Missing `classpathentry` with kind " + "`output` in `.classpath` file." + ) + sys.exit(1) + output_path = pathlib.Path(output[0].get("path")) + if not list(output_path.iterdir()): + click.echo(f"Output directory `{output_path}` is empty.") + sys.exit(1) + pom = _read_xml_file("pom.xml") + # get the namespace from the root element + ns = {"m": "http://maven.apache.org/POM/4.0.0"} # Register the namespace + artifact_id = pom.xpath("//m:artifactId", namespaces=ns) + version = pom.xpath("//m:version", namespaces=ns) + artifact_id = artifact_id[0].text if artifact_id else "unknown" + version = version[0].text if version else "unknown" + jar_name = f"{artifact_id}-{version}.jar" + jar_path = pathlib.Path("target") / jar_name + return output_path, jar_path + + +def _read_xml_file(path: str) -> lxml.etree.Element: + """Read the classpath file.""" + if not pathlib.Path(path).exists(): + click.echo(f"`File {path}` not found.") + sys.exit(1) + tree = lxml.etree.parse(path) + return tree + + +@main.command() +@click.argument("target_path", type=click.Path(exists=True, dir_okay=True)) +def build_classpath(target_path: pathlib.Path) -> None: + """Build `.classpath` file. + + Parameters + ---------- + target_path : pathlib.Path + The installation directory of an Eclipse/ Capella application + that will be references as target platform to build the classpath. + """ + target_path = pathlib.Path(target_path) + if not target_path.is_dir(): + click.echo( + f"Target platform installation dir `{target_path}` not found." + ) + sys.exit(1) + classpaths = [ + E.classpathentry(kind="src", path="src", including="**/*.java"), + E.classpathentry(kind="output", path="bin"), + E.classpathentry( + kind="con", + path=( + "org.eclipse.jdt.launching.JRE_CONTAINER/" + "org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/" + "JavaSE-17" + ), + ), + ] + with tempfile.NamedTemporaryFile(mode="w", delete=False) as w: + mvn_cmd = [ + "mvn", + "-q", + "dependency:build-classpath", + f"-Dmdep.outputFile={w.name}", + ] + # Run command and wait: + subprocess.run(mvn_cmd, check=True) + with open(w.name, "r", encoding="utf-8") as tmp: + # Replace all colons with newlines and sort the lines: + classpath_3rdparty = tmp.read().replace(":", "\n").splitlines() + classpath_3rdparty.sort() + for path in classpath_3rdparty: + classpaths.append(E.classpathentry(kind="lib", path=path)) + # get .jar files from target dir + # Recursively find all src JARs: + sources: set[pathlib.Path] = set(target_path.glob("**/*.source_*.jar")) + # Recursively find all lib JARs: + libs = list(set(target_path.glob("**/*.jar")) - sources) + srcs = list(sources) + target_classpaths = [] + for src in srcs: + skip = False + for pattern in PATH_BLACKLIST: + skip = pattern in str(src) + if skip: + break + if skip: + continue + # get parent dir + parent = src.parent + # get base name + base = src.name + lib = parent / base.replace(".source_", "_") + try: + libs.remove(lib) + except ValueError: + # print(f"Source JAR {src} has no matching lib!") + pass + if lib.is_file() and src.is_file(): + target_classpaths.append( + E.classpathentry( + kind="lib", path=str(lib), sourcepath=str(src) + ) + ) + for lib in libs: + skip = False + for pattern in PATH_BLACKLIST: + skip = pattern in str(lib) + if skip: + break + if skip: + continue + if lib.is_file(): + target_classpaths.append( + E.classpathentry(kind="lib", path=str(lib)) + ) + target_classpaths.sort(key=lambda x: x.get("path")) + classpath = E.classpath(*(classpaths + target_classpaths)) + tree = lxml.etree.ElementTree(classpath) + xml_string = lxml.etree.tostring( + tree, xml_declaration=True, encoding="utf-8", pretty_print=True + ) + pathlib.Path(".classpath").write_bytes(xml_string) + + +@main.command() +@click.argument("target_path", type=click.Path(exists=True, dir_okay=True)) +def deploy(target_path: pathlib.Path) -> None: + """Deploy the eclipse extension. + + Parameters + ---------- + target_path : pathlib.Path + The installation directory of an Eclipse/ Capella application + where the extension will be deployed into the subdirectory `dropins`. + """ + target_path = pathlib.Path(target_path) / "dropins" + if not target_path.is_dir(): + click.echo(f"Target directory `{target_path}` not found.") + sys.exit(1) + _, jar_path = _paths() + dest = target_path / jar_path.name + dest.unlink(missing_ok=True) + shutil.copy(jar_path, dest) + if dest.is_file(): + click.echo(f"Deployed `{dest.resolve()}`.") + + +@main.command() +def package() -> None: + """Package the eclipse extension.""" + for path in (MANIFEST_PATH, PLUGIN_XML_PATH): + if not pathlib.Path(path).is_file(): + click.echo(f"`{path}` file not found.") + sys.exit(1) + output_path, jar_path = _paths() + jar_path.unlink(missing_ok=True) + jar_cmd = [ + "jar", + "-cmf", + MANIFEST_PATH, + str(jar_path), + "-C", + str(output_path), + ".", + PLUGIN_XML_PATH, + ] + jar_path.parent.mkdir(parents=True, exist_ok=True) + subprocess.run(jar_cmd, check=True) + if jar_path.is_file(): + click.echo(f"Created `{jar_path.resolve()}`.") + + +# Define another subcommand +@main.command() +def clean() -> None: + """Clean the build artifacts.""" + click.echo("Cleaning build artifacts...") + + if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index 9d33612..45043b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ ] dependencies = [ "click", + "lxml", ] [project.urls] @@ -87,7 +88,7 @@ python_version = "3.10" [[tool.mypy.overrides]] # Untyped third party libraries module = [ - # ... + "lxml.*", ] ignore_missing_imports = true @@ -114,6 +115,10 @@ ignore-long-lines = '^\s*(?:(?:__ |\.\. __: )?https?://[^ ]+$|def test_.*|[A-Za- load-plugins = [ "pylint.extensions.mccabe", ] +extension-pkg-allow-list = [ + "lxml.builder", + "lxml.etree", +] max-complexity = 14 max-line-length = 79