From b8c3d31c146db15d204018d5ff83584d0c4a7477 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Thu, 29 May 2025 17:20:48 -0700 Subject: [PATCH 1/8] Add GremlinJar and GremlinBootstrap --- .../xyz/jpenilla/gremlin/gradle/GremlinJar.kt | 75 +++++++++ .../jpenilla/gremlin/gradle/GremlinPlugin.kt | 22 ++- .../gremlin/gradle/IndexNestedJars.kt | 46 ++++++ .../gremlin/runtime/GremlinBootstrap.java | 155 ++++++++++++++++++ 4 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinJar.kt create mode 100644 gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/IndexNestedJars.kt create mode 100644 runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinJar.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinJar.kt new file mode 100644 index 0000000..29aadac --- /dev/null +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinJar.kt @@ -0,0 +1,75 @@ +/* + * gremlin + * + * Copyright (c) 2025 Jason Penilla + * + * 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. + */ +package xyz.jpenilla.gremlin.gradle + +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.attributes +import javax.inject.Inject + +abstract class GremlinJar : Jar() { + @get:InputFiles + abstract val gremlinRuntime: ConfigurableFileCollection + + @get:InputFiles + abstract val nestedJars: ConfigurableFileCollection + + @get:InputFile + abstract val nestedJarsIndex: RegularFileProperty + + @get:Inject + abstract val archiveOps: ArchiveOperations + + @get:Input + @get:Optional + abstract val mainClass: Property + + init { + manifest.attributes( + "Main-Class" to "xyz.jpenilla.gremlin.runtime.GremlinBootstrap", + ) + val runtime = gremlinRuntime.elements.map { + it.map { e -> + archiveOps.zipTree(e) + } + } + from(runtime) { + exclude("META-INF/*") + } + from(nestedJars) { + into("nested-jars") + } + from(nestedJarsIndex) { + rename { "nested-jars/index.txt" } + } + } + + override fun copy() { + manifest.attributes( + "Gremlin-Main-Class" to mainClass.get(), + ) + super.copy() + } +} diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt index e5ef115..1f620f8 100644 --- a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt @@ -57,6 +57,12 @@ class GremlinPlugin : Plugin { outputFileName.convention("dependencies.txt") } + val defaultGremlinRuntime = target.configurations.register("defaultGremlinRuntime") { + defaultDependencies { + add(target.dependencies.create(Dependencies.DEFAULT_GREMLIN_RUNTIME)) + } + } + val java = target.extensions.getByType() java.sourceSets.named("main") { resources { @@ -64,6 +70,18 @@ class GremlinPlugin : Plugin { } } + val indexTask = target.tasks.register("indexNestedJars") { + nestedJars.from(target.tasks.named("jar")) + outputFile.set(target.layout.buildDirectory.file("tmp/nested-jars-index.txt")) + } + target.tasks.register("gremlinJar") { + gremlinRuntime.from(defaultGremlinRuntime) + nestedJars.from(target.tasks.named("jar")) + nestedJarsIndex.convention(indexTask.flatMap { it.outputFile }) + archiveClassifier.convention("gremlin") + destinationDirectory.convention(target.layout.buildDirectory.dir("libs")) + } + target.afterEvaluate { if (ext.defaultJarRelocatorDependencies.get()) { target.dependencies { @@ -73,8 +91,8 @@ class GremlinPlugin : Plugin { } } if (ext.defaultGremlinRuntimeDependency.get()) { - target.dependencies { - java.sourceSets.getByName("main").implementationConfigurationName(Dependencies.DEFAULT_GREMLIN_RUNTIME) + target.configurations.named(java.sourceSets.getByName("main").implementationConfigurationName) { + extendsFrom(defaultGremlinRuntime.get()) } } diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/IndexNestedJars.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/IndexNestedJars.kt new file mode 100644 index 0000000..edb3426 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/IndexNestedJars.kt @@ -0,0 +1,46 @@ +/* + * gremlin + * + * Copyright (c) 2025 Jason Penilla + * + * 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. + */ +package xyz.jpenilla.gremlin.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +abstract class IndexNestedJars : DefaultTask() { + @get:InputFiles + abstract val nestedJars: ConfigurableFileCollection + + @get:OutputFile + abstract val outputFile: RegularFileProperty + + @TaskAction + fun run() { + val text = StringBuilder() + nestedJars.files.forEach { jar -> + text.append(jar.name) + text.append("\n") + } + + val outFile = outputFile.get().asFile + outFile.parentFile.mkdirs() + outFile.writeText(text.toString()) + } +} diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java new file mode 100644 index 0000000..773a0d3 --- /dev/null +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -0,0 +1,155 @@ +/* + * gremlin + * + * Copyright (c) 2025 Jason Penilla + * + * 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. + */ +package xyz.jpenilla.gremlin.runtime; + +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Logger; +import xyz.jpenilla.gremlin.runtime.logging.GremlinLogger; +import xyz.jpenilla.gremlin.runtime.logging.JavaGremlinLogger; + +public final class GremlinBootstrap { + public GremlinBootstrap() { + } + + public static void main(final String[] args) { + final InputStream nestedJarsIndexStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream("nested-jars/index.txt"); + if (nestedJarsIndexStream == null) { + throw new IllegalStateException("Could not find nested-jars/index.txt in classpath"); + } + final List nestedJarsPaths = new ArrayList<>(); + try (nestedJarsIndexStream) { + final String indexContent = new String(nestedJarsIndexStream.readAllBytes()); + for (final String line : indexContent.split("\n")) { + nestedJarsPaths.add(line.trim()); + } + } catch (final Exception e) { + throw new RuntimeException("Failed to read nested-jars/index.txt", e); + } + final List jars = new ArrayList<>( + extractNestedJars(nestedJarsPaths, Path.of(".gremlin/nested-jars")) + ); + + DependencySet dependencies = null; + for (final Path jar : jars) { + try (final JarFile jarFile = new JarFile(jar.toFile())) { + final JarEntry entry = jarFile.getJarEntry("dependencies.txt"); + if (entry != null) { + try (final InputStream inputStream = jarFile.getInputStream(entry)) { + dependencies = DependencySet.read(inputStream); + } + break; + } + } catch (final Exception e) { + throw new RuntimeException("Failed to read dependencies.txt from jar: " + jar, e); + } + } + if (dependencies == null) { + throw new IllegalStateException("No dependencies.txt found in nested jars"); + } + + final DependencyCache cache = new DependencyCache(Path.of(".gremlin/dependency-cache")); + + final GremlinLogger logger = new JavaGremlinLogger(Logger.getLogger(GremlinBootstrap.class.getName())); + try (final DependencyResolver resolver = new DependencyResolver(logger)) { + jars.addAll(resolver.resolve(dependencies, cache).jarFiles()); + } + + final URL[] classpath = jars.stream() + .map(Path::toUri) + .map(uri -> { + try { + return uri.toURL(); + } catch (final Exception e) { + throw new RuntimeException("Failed to convert path to URL: " + uri, e); + } + }) + .toArray(URL[]::new); + + final String mainClassName; + try (final InputStream manifestStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF")) { + if (manifestStream == null) { + throw new IllegalStateException("Could not find META-INF/MANIFEST.MF in classpath"); + } + final Manifest manifest = new Manifest(manifestStream); + mainClassName = manifest.getMainAttributes().getValue("Gremlin-Main-Class"); + if (mainClassName == null || mainClassName.isEmpty()) { + throw new IllegalStateException("Gremlin-Main-Class not specified in manifest"); + } + } catch (final Exception e) { + throw new RuntimeException("Failed to invoke main method", e); + } + + final ClassLoader loader = new URLClassLoader(classpath, GremlinBootstrap.class.getClassLoader()); + final Class mainClass; + try { + mainClass = Class.forName(mainClassName, true, loader); + } catch (final ClassNotFoundException e) { + throw new RuntimeException("Main class not found: " + mainClassName, e); + } + final Method main; + try { + main = mainClass.getMethod("main", String[].class); + } catch (final ReflectiveOperationException e) { + throw new RuntimeException("Main method not found in class: " + mainClassName, e); + } + final Thread thread = new Thread(() -> { + try { + main.invoke(null, (Object) args); + } catch (final IllegalAccessException e) { + throw new RuntimeException("Failed to invoke main method of class: " + mainClassName, e); + } catch (final InvocationTargetException e) { + throw new RuntimeException("Uncaught exception in main method of class: " + mainClassName, e.getCause()); + } + }, "Gremlin Bootstrap Thread"); + thread.setContextClassLoader(loader); + thread.start(); + } + + private static List extractNestedJars(final List nestedJarsPaths, final Path into) { + final List paths = new ArrayList<>(); + for (final String path : nestedJarsPaths) { + final InputStream resourceStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream("nested-jars/" + path); + if (resourceStream == null) { + throw new IllegalStateException("Could not find nested jar: " + path); + } + final Path outputPath = into.resolve(path); + try { + Files.createDirectories(outputPath.getParent()); + try (resourceStream) { + Files.copy(resourceStream, outputPath, StandardCopyOption.REPLACE_EXISTING); + } + paths.add(outputPath); + } catch (final Exception e) { + throw new RuntimeException("Failed to extract nested jar: " + path, e); + } + } + return paths; + } +} From fa62b4a0b2ea9bd08e530f504b7af7954748df6e Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Thu, 29 May 2025 17:25:49 -0700 Subject: [PATCH 2/8] Cleanup cache in GremlinBootstrap --- .../xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java index 773a0d3..24438c8 100644 --- a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -130,6 +130,13 @@ public static void main(final String[] args) { }, "Gremlin Bootstrap Thread"); thread.setContextClassLoader(loader); thread.start(); + + try { + cache.cleanup(); + } catch (final Throwable e) { + //noinspection CallToPrintStackTrace + new RuntimeException("Exception cleaning up dependency cache", e).printStackTrace(); + } } private static List extractNestedJars(final List nestedJarsPaths, final Path into) { From 4239baf0cc5fc73c60051bd5f3b92f2187748a30 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Thu, 29 May 2025 18:24:14 -0700 Subject: [PATCH 3/8] Improve manifest error handling --- .../java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java index 24438c8..4719ff3 100644 --- a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -17,6 +17,7 @@ */ package xyz.jpenilla.gremlin.runtime; +import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -102,8 +103,8 @@ public static void main(final String[] args) { if (mainClassName == null || mainClassName.isEmpty()) { throw new IllegalStateException("Gremlin-Main-Class not specified in manifest"); } - } catch (final Exception e) { - throw new RuntimeException("Failed to invoke main method", e); + } catch (final IOException e) { + throw new RuntimeException("Failed to read META-INF/MANIFEST.MF", e); } final ClassLoader loader = new URLClassLoader(classpath, GremlinBootstrap.class.getClassLoader()); From cfe784f7880828bbc14b86869245927a22e282c0 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Fri, 30 May 2025 11:19:35 -0700 Subject: [PATCH 4/8] Cleanup --- .../jpenilla/gremlin/gradle/GremlinPlugin.kt | 23 +-- .../gremlin/runtime/GremlinBootstrap.java | 152 ++++++++++-------- 2 files changed, 102 insertions(+), 73 deletions(-) diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt index 1f620f8..7989e65 100644 --- a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt @@ -51,18 +51,20 @@ class GremlinPlugin : Plugin { runtimeClasspathAttributes(target.objects) } + val gremlinRuntime = target.configurations.register("gremlinRuntime") { + makeResolvable() + runtimeClasspathAttributes(target.objects) + defaultDependencies { + add(target.dependencies.create(Dependencies.DEFAULT_GREMLIN_RUNTIME)) + } + } + val writeDependencies = target.tasks.register("writeDependencies", WriteDependencySet::class) { dependencies.setFrom(runtimeDownload) relocationDependencies.setFrom(jarRelocatorRuntime) outputFileName.convention("dependencies.txt") } - val defaultGremlinRuntime = target.configurations.register("defaultGremlinRuntime") { - defaultDependencies { - add(target.dependencies.create(Dependencies.DEFAULT_GREMLIN_RUNTIME)) - } - } - val java = target.extensions.getByType() java.sourceSets.named("main") { resources { @@ -75,7 +77,7 @@ class GremlinPlugin : Plugin { outputFile.set(target.layout.buildDirectory.file("tmp/nested-jars-index.txt")) } target.tasks.register("gremlinJar") { - gremlinRuntime.from(defaultGremlinRuntime) + this.gremlinRuntime.from(gremlinRuntime) nestedJars.from(target.tasks.named("jar")) nestedJarsIndex.convention(indexTask.flatMap { it.outputFile }) archiveClassifier.convention("gremlin") @@ -91,8 +93,11 @@ class GremlinPlugin : Plugin { } } if (ext.defaultGremlinRuntimeDependency.get()) { - target.configurations.named(java.sourceSets.getByName("main").implementationConfigurationName) { - extendsFrom(defaultGremlinRuntime.get()) + target.configurations.named(java.sourceSets.getByName("main").compileClasspathConfigurationName) { + extendsFrom(gremlinRuntime.get()) + } + target.configurations.named(java.sourceSets.getByName("main").runtimeClasspathConfigurationName) { + extendsFrom(gremlinRuntime.get()) } } diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java index 4719ff3..cf2bcf7 100644 --- a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -32,33 +32,91 @@ import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Logger; +import org.jspecify.annotations.NullMarked; import xyz.jpenilla.gremlin.runtime.logging.GremlinLogger; import xyz.jpenilla.gremlin.runtime.logging.JavaGremlinLogger; +@NullMarked public final class GremlinBootstrap { + private static final String DOT_GREMLIN = ".gremlin"; + private static final String NESTED_JARS = "nested-jars"; + private static final String NESTED_JARS_INDEX = NESTED_JARS + "/index.txt"; + private static final String DEPENDENCY_CACHE = "dependency-cache"; + private static final String GREMLIN_MAIN_CLASS = "Gremlin-Main-Class"; + public GremlinBootstrap() { } public static void main(final String[] args) { - final InputStream nestedJarsIndexStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream("nested-jars/index.txt"); - if (nestedJarsIndexStream == null) { - throw new IllegalStateException("Could not find nested-jars/index.txt in classpath"); + final List jars = new ArrayList<>( + extractNestedJars(Path.of(DOT_GREMLIN + "/" + NESTED_JARS)) + ); + + final DependencySet dependencies = getDependencies(jars); + final DependencyCache cache = new DependencyCache(Path.of(DOT_GREMLIN + "/" + DEPENDENCY_CACHE)); + final GremlinLogger logger = new JavaGremlinLogger(Logger.getLogger(GremlinBootstrap.class.getName())); + try (final DependencyResolver resolver = new DependencyResolver(logger)) { + jars.addAll(resolver.resolve(dependencies, cache).jarFiles()); } - final List nestedJarsPaths = new ArrayList<>(); - try (nestedJarsIndexStream) { - final String indexContent = new String(nestedJarsIndexStream.readAllBytes()); - for (final String line : indexContent.split("\n")) { - nestedJarsPaths.add(line.trim()); + + final String mainClassName = getMainClassName(); + final ClassLoader loader = buildClassLoader(jars); + final Method main = getMainMethod(mainClassName, loader); + + final Thread applicationThread = new Thread(() -> { + try { + main.invoke(null, (Object) args); + } catch (final IllegalAccessException e) { + throw new RuntimeException("Failed to invoke main method of class: " + mainClassName, e); + } catch (final InvocationTargetException e) { + throw new RuntimeException("Uncaught exception during application execution", e.getCause()); } - } catch (final Exception e) { - throw new RuntimeException("Failed to read nested-jars/index.txt", e); + }, "bootstrapped-main"); + applicationThread.setContextClassLoader(loader); + applicationThread.start(); + + try { + cache.cleanup(); + } catch (final Throwable e) { + //noinspection CallToPrintStackTrace + new RuntimeException("Exception cleaning up dependency cache", e).printStackTrace(); } - final List jars = new ArrayList<>( - extractNestedJars(nestedJarsPaths, Path.of(".gremlin/nested-jars")) - ); + } + + private static Method getMainMethod(final String mainClassName, final ClassLoader loader) { + final Class mainClass; + try { + mainClass = Class.forName(mainClassName, true, loader); + } catch (final ClassNotFoundException e) { + throw new RuntimeException("Main class not found: " + mainClassName, e); + } + try { + return mainClass.getMethod("main", String[].class); + } catch (final ReflectiveOperationException e) { + throw new RuntimeException("Main method not found in class: " + mainClassName, e); + } + } + + private static String getMainClassName() { + final String mainClassName; + try (final InputStream manifestStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream(JarFile.MANIFEST_NAME)) { + if (manifestStream == null) { + throw new IllegalStateException("Could not find " + JarFile.MANIFEST_NAME + " in classpath"); + } + final Manifest manifest = new Manifest(manifestStream); + mainClassName = manifest.getMainAttributes().getValue(GREMLIN_MAIN_CLASS); + if (mainClassName == null || mainClassName.isEmpty()) { + throw new IllegalStateException(GREMLIN_MAIN_CLASS + " not specified in manifest"); + } + } catch (final IOException e) { + throw new RuntimeException("Failed to read " + JarFile.MANIFEST_NAME, e); + } + return mainClassName; + } + private static DependencySet getDependencies(final List nestedJars) { DependencySet dependencies = null; - for (final Path jar : jars) { + for (final Path jar : nestedJars) { try (final JarFile jarFile = new JarFile(jar.toFile())) { final JarEntry entry = jarFile.getJarEntry("dependencies.txt"); if (entry != null) { @@ -74,14 +132,10 @@ public static void main(final String[] args) { if (dependencies == null) { throw new IllegalStateException("No dependencies.txt found in nested jars"); } + return dependencies; + } - final DependencyCache cache = new DependencyCache(Path.of(".gremlin/dependency-cache")); - - final GremlinLogger logger = new JavaGremlinLogger(Logger.getLogger(GremlinBootstrap.class.getName())); - try (final DependencyResolver resolver = new DependencyResolver(logger)) { - jars.addAll(resolver.resolve(dependencies, cache).jarFiles()); - } - + private static ClassLoader buildClassLoader(final List jars) { final URL[] classpath = jars.stream() .map(Path::toUri) .map(uri -> { @@ -93,54 +147,24 @@ public static void main(final String[] args) { }) .toArray(URL[]::new); - final String mainClassName; - try (final InputStream manifestStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF")) { - if (manifestStream == null) { - throw new IllegalStateException("Could not find META-INF/MANIFEST.MF in classpath"); - } - final Manifest manifest = new Manifest(manifestStream); - mainClassName = manifest.getMainAttributes().getValue("Gremlin-Main-Class"); - if (mainClassName == null || mainClassName.isEmpty()) { - throw new IllegalStateException("Gremlin-Main-Class not specified in manifest"); - } - } catch (final IOException e) { - throw new RuntimeException("Failed to read META-INF/MANIFEST.MF", e); - } + return new URLClassLoader(classpath, GremlinBootstrap.class.getClassLoader()); + } - final ClassLoader loader = new URLClassLoader(classpath, GremlinBootstrap.class.getClassLoader()); - final Class mainClass; - try { - mainClass = Class.forName(mainClassName, true, loader); - } catch (final ClassNotFoundException e) { - throw new RuntimeException("Main class not found: " + mainClassName, e); - } - final Method main; - try { - main = mainClass.getMethod("main", String[].class); - } catch (final ReflectiveOperationException e) { - throw new RuntimeException("Main method not found in class: " + mainClassName, e); + private static List extractNestedJars(final Path into) { + final InputStream nestedJarsIndexStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream(NESTED_JARS_INDEX); + if (nestedJarsIndexStream == null) { + throw new IllegalStateException("Could not find " + NESTED_JARS_INDEX + " in classpath"); } - final Thread thread = new Thread(() -> { - try { - main.invoke(null, (Object) args); - } catch (final IllegalAccessException e) { - throw new RuntimeException("Failed to invoke main method of class: " + mainClassName, e); - } catch (final InvocationTargetException e) { - throw new RuntimeException("Uncaught exception in main method of class: " + mainClassName, e.getCause()); + final List nestedJarsPaths = new ArrayList<>(); + try (nestedJarsIndexStream) { + final String indexContent = new String(nestedJarsIndexStream.readAllBytes()); + for (final String line : indexContent.split("\n")) { + nestedJarsPaths.add(line.trim()); } - }, "Gremlin Bootstrap Thread"); - thread.setContextClassLoader(loader); - thread.start(); - - try { - cache.cleanup(); - } catch (final Throwable e) { - //noinspection CallToPrintStackTrace - new RuntimeException("Exception cleaning up dependency cache", e).printStackTrace(); + } catch (final Exception e) { + throw new RuntimeException("Failed to read " + NESTED_JARS_INDEX, e); } - } - private static List extractNestedJars(final List nestedJarsPaths, final Path into) { final List paths = new ArrayList<>(); for (final String path : nestedJarsPaths) { final InputStream resourceStream = GremlinBootstrap.class.getClassLoader().getResourceAsStream("nested-jars/" + path); From 631c8f8a72c47cad103ac847312b05d38608afd3 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Fri, 30 May 2025 12:56:26 -0700 Subject: [PATCH 5/8] Fix loading main with wrong context loader --- .../java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java index cf2bcf7..46d87ec 100644 --- a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -61,9 +61,9 @@ public static void main(final String[] args) { final String mainClassName = getMainClassName(); final ClassLoader loader = buildClassLoader(jars); - final Method main = getMainMethod(mainClassName, loader); final Thread applicationThread = new Thread(() -> { + final Method main = getMainMethod(mainClassName, loader); try { main.invoke(null, (Object) args); } catch (final IllegalAccessException e) { From c301df689218e12c548883cdb03a5b9d29aff283 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Fri, 30 May 2025 13:29:24 -0700 Subject: [PATCH 6/8] adjust nesting strategy --- .../gradle/{GremlinJar.kt => BootstrapJar.kt} | 23 ++++------- .../gremlin/gradle/GremlinExtension.kt | 19 +++++++++- .../jpenilla/gremlin/gradle/GremlinPlugin.kt | 20 ++++++---- ...ndexNestedJars.kt => PrepareNestedJars.kt} | 38 +++++++++++++------ .../xyz/jpenilla/gremlin/gradle/utils.kt | 14 +++++++ .../gremlin/runtime/GremlinBootstrap.java | 13 +++++++ 6 files changed, 91 insertions(+), 36 deletions(-) rename gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/{GremlinJar.kt => BootstrapJar.kt} (81%) rename gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/{IndexNestedJars.kt => PrepareNestedJars.kt} (53%) diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinJar.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/BootstrapJar.kt similarity index 81% rename from gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinJar.kt rename to gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/BootstrapJar.kt index 29aadac..13be8ad 100644 --- a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinJar.kt +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/BootstrapJar.kt @@ -19,26 +19,19 @@ package xyz.jpenilla.gremlin.gradle import org.gradle.api.file.ArchiveOperations import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskProvider import org.gradle.jvm.tasks.Jar import org.gradle.kotlin.dsl.attributes import javax.inject.Inject -abstract class GremlinJar : Jar() { +abstract class BootstrapJar : Jar() { @get:InputFiles abstract val gremlinRuntime: ConfigurableFileCollection - @get:InputFiles - abstract val nestedJars: ConfigurableFileCollection - - @get:InputFile - abstract val nestedJarsIndex: RegularFileProperty - @get:Inject abstract val archiveOps: ArchiveOperations @@ -58,12 +51,6 @@ abstract class GremlinJar : Jar() { from(runtime) { exclude("META-INF/*") } - from(nestedJars) { - into("nested-jars") - } - from(nestedJarsIndex) { - rename { "nested-jars/index.txt" } - } } override fun copy() { @@ -72,4 +59,10 @@ abstract class GremlinJar : Jar() { ) super.copy() } + + fun nestJars(task: TaskProvider) { + from(task.flatMap { it.outputDir }) { + into("nested-jars") + } + } } diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinExtension.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinExtension.kt index f45b806..ddd8d5d 100644 --- a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinExtension.kt +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinExtension.kt @@ -18,10 +18,13 @@ package xyz.jpenilla.gremlin.gradle import org.gradle.api.provider.Property +import org.gradle.api.tasks.AbstractCopyTask +import org.gradle.api.tasks.TaskProvider abstract class GremlinExtension { abstract val defaultJarRelocatorDependencies: Property - abstract val defaultGremlinRuntimeDependency: Property + abstract val addGremlinRuntimeToCompileClasspath: Property + abstract val addGremlinRuntimeToRuntimeClasspath: Property init { init() @@ -29,6 +32,18 @@ abstract class GremlinExtension { private fun init() { defaultJarRelocatorDependencies.convention(true) - defaultGremlinRuntimeDependency.convention(true) + addGremlinRuntimeToCompileClasspath.convention(true) + addGremlinRuntimeToRuntimeClasspath.convention(true) + } + + fun nestJars( + prepareNestedJars: TaskProvider, + into: TaskProvider + ) { + into.configure { + from(prepareNestedJars.flatMap { it.outputDir }) { + into("nested-jars") + } + } } } diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt index 7989e65..26c5aac 100644 --- a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/GremlinPlugin.kt @@ -59,6 +59,11 @@ class GremlinPlugin : Plugin { } } + val nestedJars = target.configurations.register("nestedJars") { + makeResolvable() + runtimeClasspathAttributes(target.objects) + } + val writeDependencies = target.tasks.register("writeDependencies", WriteDependencySet::class) { dependencies.setFrom(runtimeDownload) relocationDependencies.setFrom(jarRelocatorRuntime) @@ -72,16 +77,15 @@ class GremlinPlugin : Plugin { } } - val indexTask = target.tasks.register("indexNestedJars") { - nestedJars.from(target.tasks.named("jar")) - outputFile.set(target.layout.buildDirectory.file("tmp/nested-jars-index.txt")) + val prepareNestedJars = target.tasks.register("prepareNestedJars") { + this.nestedJars.setFrom(nestedJars) } - target.tasks.register("gremlinJar") { + + target.tasks.register("bootstrapJar") { this.gremlinRuntime.from(gremlinRuntime) - nestedJars.from(target.tasks.named("jar")) - nestedJarsIndex.convention(indexTask.flatMap { it.outputFile }) archiveClassifier.convention("gremlin") destinationDirectory.convention(target.layout.buildDirectory.dir("libs")) + nestJars(prepareNestedJars) } target.afterEvaluate { @@ -92,10 +96,12 @@ class GremlinPlugin : Plugin { } } } - if (ext.defaultGremlinRuntimeDependency.get()) { + if (ext.addGremlinRuntimeToCompileClasspath.get()) { target.configurations.named(java.sourceSets.getByName("main").compileClasspathConfigurationName) { extendsFrom(gremlinRuntime.get()) } + } + if (ext.addGremlinRuntimeToRuntimeClasspath.get()) { target.configurations.named(java.sourceSets.getByName("main").runtimeClasspathConfigurationName) { extendsFrom(gremlinRuntime.get()) } diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/IndexNestedJars.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/PrepareNestedJars.kt similarity index 53% rename from gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/IndexNestedJars.kt rename to gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/PrepareNestedJars.kt index edb3426..f40bb81 100644 --- a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/IndexNestedJars.kt +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/PrepareNestedJars.kt @@ -19,28 +19,42 @@ package xyz.jpenilla.gremlin.gradle import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.RegularFileProperty +import org.gradle.api.file.DirectoryProperty import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import java.nio.file.Files +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteRecursively +import kotlin.io.path.writeText + +@OptIn(ExperimentalPathApi::class) +abstract class PrepareNestedJars : DefaultTask() { + @get:OutputDirectory + abstract val outputDir: DirectoryProperty -abstract class IndexNestedJars : DefaultTask() { @get:InputFiles abstract val nestedJars: ConfigurableFileCollection - @get:OutputFile - abstract val outputFile: RegularFileProperty + init { + init() + } + + private fun init() { + outputDir.convention(project.layout.buildDirectory.dir("nested-jars/$name")) + } @TaskAction fun run() { - val text = StringBuilder() + val outputPath = outputDir.path + outputPath.deleteRecursively() + Files.createDirectories(outputPath) + + val index = mutableListOf() nestedJars.files.forEach { jar -> - text.append(jar.name) - text.append("\n") + Files.copy(jar.toPath(), outputPath.resolve(jar.name)) + index += jar.name } - - val outFile = outputFile.get().asFile - outFile.parentFile.mkdirs() - outFile.writeText(text.toString()) + outputPath.resolve("index.txt").writeText(index.joinToString("\n")) } } diff --git a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/utils.kt b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/utils.kt index 7b44963..8d8110a 100644 --- a/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/utils.kt +++ b/gradle-plugin/src/main/kotlin/xyz/jpenilla/gremlin/gradle/utils.kt @@ -17,11 +17,25 @@ */ package xyz.jpenilla.gremlin.gradle +import org.gradle.api.file.FileSystemLocationProperty import java.io.InputStream +import java.net.URI +import java.nio.file.FileSystem +import java.nio.file.FileSystems import java.nio.file.Path import java.security.MessageDigest import kotlin.io.path.inputStream +val FileSystemLocationProperty<*>.path: Path + get() = get().asFile.toPath() + +fun Path.jarUri(): URI = URI.create("jar:" + toUri().toString()) + +fun Path.openZip(op: (FileSystem) -> R): R = + FileSystems.newFileSystem(jarUri(), emptyMap()).use { fs -> + op(fs) + } + enum class HashingAlgorithm(val algorithmName: String) { SHA256("SHA-256"), SHA1("SHA-1"); diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java index 46d87ec..9d82e7f 100644 --- a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -32,6 +32,7 @@ import java.util.jar.JarFile; import java.util.jar.Manifest; import java.util.logging.Logger; +import java.util.stream.Stream; import org.jspecify.annotations.NullMarked; import xyz.jpenilla.gremlin.runtime.logging.GremlinLogger; import xyz.jpenilla.gremlin.runtime.logging.JavaGremlinLogger; @@ -182,6 +183,18 @@ private static List extractNestedJars(final Path into) { throw new RuntimeException("Failed to extract nested jar: " + path, e); } } + + try (final Stream s = Files.list(into)) { + for (final Path path : s.toList()) { + if (Files.isRegularFile(path) && !nestedJarsPaths.contains(path.getFileName().toString())) { + // Delete no longer needed nested jars + Files.delete(path); + } + } + } catch (final IOException e) { + throw new RuntimeException("Failed to clean up nested jars directory: " + into, e); + } + return paths; } } From fe022f4cf6012ab5778233fb1728b318c5879182 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Fri, 30 May 2025 14:07:12 -0700 Subject: [PATCH 7/8] Use set for nestedJarsPaths --- .../java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java index 9d82e7f..3d4f106 100644 --- a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -27,7 +27,9 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -156,7 +158,7 @@ private static List extractNestedJars(final Path into) { if (nestedJarsIndexStream == null) { throw new IllegalStateException("Could not find " + NESTED_JARS_INDEX + " in classpath"); } - final List nestedJarsPaths = new ArrayList<>(); + final Set nestedJarsPaths = new LinkedHashSet<>(); try (nestedJarsIndexStream) { final String indexContent = new String(nestedJarsIndexStream.readAllBytes()); for (final String line : indexContent.split("\n")) { From e308bb3b576ef4438c17bb7f55f75104edb536e1 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Fri, 30 May 2025 14:07:36 -0700 Subject: [PATCH 8/8] specify UTF-8 --- .../java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java index 3d4f106..98ed1b2 100644 --- a/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java +++ b/runtime/src/main/java/xyz/jpenilla/gremlin/runtime/GremlinBootstrap.java @@ -23,6 +23,7 @@ import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -160,7 +161,7 @@ private static List extractNestedJars(final Path into) { } final Set nestedJarsPaths = new LinkedHashSet<>(); try (nestedJarsIndexStream) { - final String indexContent = new String(nestedJarsIndexStream.readAllBytes()); + final String indexContent = new String(nestedJarsIndexStream.readAllBytes(), StandardCharsets.UTF_8); for (final String line : indexContent.split("\n")) { nestedJarsPaths.add(line.trim()); }