diff --git a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/JVMDowngraderExtension.kt b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/JVMDowngraderExtension.kt index d9ff5d76..5e01241a 100644 --- a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/JVMDowngraderExtension.kt +++ b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/JVMDowngraderExtension.kt @@ -16,6 +16,7 @@ import xyz.wagyourtail.jvmdg.gradle.task.ShadeJar import xyz.wagyourtail.jvmdg.gradle.transform.DowngradeTransform import xyz.wagyourtail.jvmdg.gradle.transform.ShadeTransform import xyz.wagyourtail.jvmdg.util.FinalizeOnRead +import xyz.wagyourtail.jvmdg.util.LazyMutable import xyz.wagyourtail.jvmdg.util.defaultedMapOf import java.io.File import javax.inject.Inject @@ -41,40 +42,65 @@ abstract class JVMDowngraderExtension @Inject constructor(@get:Internal val proj } } - init { - downgradeTo.convention(JavaVersion.VERSION_1_8).finalizeValueOnRead() - apiJar.convention(project.provider { - val apiJar = project.file(".gradle").resolve("jvmdg/java-api-${version}.jar") - if (!apiJar.exists() || project.gradle.startParameter.isRefreshDependencies) { - apiJar.parentFile.mkdirs() - JVMDowngraderExtension::class.java.getResourceAsStream("/META-INF/lib/java-api.jar").use { stream -> - if (stream == null) throw IllegalStateException("java-api.jar not found in resources") - apiJar.outputStream().use { os -> - stream.copyTo(os) - } + /** + * the main api jar to use for downgrading + */ + @get:Internal + var apiJarDefault by LazyMutable { + val apiJar = project.file(".gradle").resolve("jvmdg/java-api-${version}.jar") + if (!apiJar.exists() || project.gradle.startParameter.isRefreshDependencies) { + apiJar.parentFile.mkdirs() + JVMDowngraderExtension::class.java.getResourceAsStream("/META-INF/lib/java-api.jar").use { stream -> + if (stream == null) throw IllegalStateException("java-api.jar not found in resources") + apiJar.outputStream().use { os -> + stream.copyTo(os) } } - apiJar - }).finalizeValueOnRead() + } + apiJar + } + + init { + downgradeTo.convention(JavaVersion.VERSION_1_8).finalizeValueOnRead() + apiJar.convention(project.provider { setOf(apiJarDefault) }).finalizeValueOnRead() quiet.convention(false).finalizeValueOnRead() + logAnsiColors.convention(true).finalizeValueOnRead() + logLevel.convention("INFO").finalizeValueOnRead() + ignoreWarningsIn.convention(emptySet()).finalizeValueOnRead() debug.convention(false).finalizeValueOnRead() debugSkipStubs.convention(emptySet()).finalizeValueOnRead() + debugDumpClasses.convention(false).finalizeValueOnRead() shadePath.convention { it.substringBefore(".").substringBeforeLast("-").replace(Regex("[.;\\[/]"), "-") + "/" } } - @get:Internal - internal val downgradedApis = defaultedMapOf { version -> - val downgradedPath = project.file(".gradle").resolve("jvmdg/java-api-${this.version}-${version}-downgraded.jar") + fun convention(flags: ShadeFlags) { + convention(flags as DowngradeFlags) + flags.shadePath.convention(shadePath).finalizeValueOnRead() + } - if (!downgradedPath.exists() || project.gradle.startParameter.isRefreshDependencies) { - ClassDowngrader.downgradeTo(this.toFlags()).use { - ZipDowngrader.downgradeZip(it, apiJar.get().toPath(), emptySet(), downgradedPath.toPath()) + fun convention(flags: DowngradeFlags) { + flags.downgradeTo.convention(downgradeTo).finalizeValueOnRead() + flags.apiJar.convention(apiJar).finalizeValueOnRead() + flags.quiet.convention(quiet).finalizeValueOnRead() + flags.debug.convention(debug).finalizeValueOnRead() + flags.debugSkipStubs.convention(debugSkipStubs).finalizeValueOnRead() + } + + @get:Internal + internal val downgradedApis = defaultedMapOf> { version -> + val jars = mutableSetOf() + for (path in apiJar.get()) { + val downgraded = path.resolveSibling(path.nameWithoutExtension + "-downgraded-${version}.jar") + if (!downgraded.exists() || project.gradle.startParameter.isRefreshDependencies) { + ClassDowngrader.downgradeTo(this.toFlags()).use { + ZipDowngrader.downgradeZip(it, path.toPath(), emptySet(), downgraded.toPath()) + } } } - downgradedPath + jars } - fun getDowngradedApi(version: JavaVersion): File = downgradedApis[version] + fun getDowngradedApi(version: JavaVersion): Set = downgradedApis[version] @JvmOverloads fun dg(dep: Configuration, shade: Boolean = true, config: DowngradeFlags.() -> Unit = {}) { @@ -97,11 +123,7 @@ abstract class JVMDowngraderExtension @Inject constructor(@get:Internal val proj spec.to.attribute(artifactType, "jar").attribute(downgradeAttr, true).attribute(shadeAttr, false) spec.parameters { - it.downgradeTo.set(downgradeTo) - it.apiJar.set(apiJar) - it.quiet.set(quiet) - it.debug.set(debug) - it.debugSkipStubs.set(debugSkipStubs) + this@JVMDowngraderExtension.convention(it) config(it) javaVersion = it.downgradeTo.get() } @@ -121,14 +143,7 @@ abstract class JVMDowngraderExtension @Inject constructor(@get:Internal val proj spec.to.attribute(artifactType, "jar").attribute(shadeAttr, true).attribute(downgradeAttr, true) spec.parameters { - it.downgradeTo.set(downgradeTo) - it.apiJar.set(project.provider { - downgradedApis[it.downgradeTo.get()] - }) - it.quiet.set(quiet) - it.debug.set(debug) - it.debugSkipStubs.set(debugSkipStubs) - it.shadePath.set(shadePath) + this@JVMDowngraderExtension.convention(it) config(it) } } diff --git a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/flags/DowngradeFlags.kt b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/flags/DowngradeFlags.kt index 5eb22f7d..a51cf0ee 100644 --- a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/flags/DowngradeFlags.kt +++ b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/flags/DowngradeFlags.kt @@ -2,43 +2,109 @@ package xyz.wagyourtail.jvmdg.gradle.flags import org.gradle.api.JavaVersion import org.gradle.api.artifacts.transform.TransformParameters +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.* import xyz.wagyourtail.jvmdg.cli.Flags +import xyz.wagyourtail.jvmdg.logging.Logger import xyz.wagyourtail.jvmdg.util.toOpcode import java.io.File interface DowngradeFlags : TransformParameters { + /** + * sets the target class version to downgrade to, + * default is [JavaVersion.VERSION_1_8] + */ @get:Input @get:Optional val downgradeTo: Property - @get:Input + /** + * sets the api jar to use for downgrading + * default is null + */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) @get:Optional - val apiJar: Property + val apiJar: ListProperty + /** + * sets the log level to [Logger.Level.FATAL] + * @deprecated use [logLevel], if this is true, it will override [logLevel] to [Logger.Level.FATAL] + */ @get:Input @get:Optional + @get:Deprecated(message = "use logLevel", replaceWith = ReplaceWith("logLevel = LogLevel.DEBUG")) val quiet: Property + /** + * sets if the logger should use ansi colors for the console to look pretty + */ + @get:Input + @get:Optional + val logAnsiColors: Property + + + /** + * sets the log level, default is [Logger.Level.INFO] as a string + */ + @get:Input + @get:Optional + val logLevel: Property + + /** + * sets if any classes should be set to ignore missing class/member warnings + * this will prevent [xyz.wagyourtail.jvmdg.version.VersionProvider.printWarnings] for + * any classes in this set + * + * This set also allows for packages to be ignored, by ending with a `*` or `**` to ignore all sub-packages + * @since 0.9.0 + */ @get:Input @get:Optional + val ignoreWarningsIn: ListProperty + + /** + * sets if the logger should print debug messages + * @deprecated use [logLevel], if this is true, it will override [logLevel] to [Logger.Level.DEBUG] + */ + @get:Input + @get:Optional + @get:Deprecated(message = "use logLevel", replaceWith = ReplaceWith("logLevel = LogLevel.DEBUG")) val debug: Property + /** + * this skips applying stubs for the specified input class version, this will still apply the + * [xyz.wagyourtail.jvmdg.version.VersionProvider.otherTransforms] + * such as `INVOKE_INTERFACE` -> `INVOKE_SPECIAL` for private interface methods in java 9 -> 8 + */ @get:Input @get:Optional val debugSkipStubs: SetProperty + + /** + * sets if classes should be dumped to the [xyz.wagyourtail.jvmdg.Constants.DEBUG_DIR] directory + * @since 0.9.0 + */ + @get:Input + @get:Optional + val debugDumpClasses: Property + } fun DowngradeFlags.toFlags(): Flags { val flags = Flags() - flags.api = apiJar.get() - flags.printDebug = debug.get() - flags.quiet = quiet.get() - flags.classVersion = downgradeTo.get().toOpcode() - flags.debugSkipStubs = debugSkipStubs.get().map { it.toOpcode() }.toSet() + flags.api = apiJar.orNull?.toSet() + flags.quiet = quiet.getOrElse(false) + flags.logAnsiColors = logAnsiColors.getOrElse(true) + flags.logLevel = Logger.Level.valueOf(logLevel.getOrElse("INFO").uppercase()) + flags.printDebug = debug.getOrElse(false) + flags.classVersion = downgradeTo.getOrElse(JavaVersion.VERSION_1_8).toOpcode() + flags.debugSkipStubs = debugSkipStubs.getOrElse(emptySet()).map { it.toOpcode() }.toSet() + ignoreWarningsIn.getOrElse(emptyList()).forEach { flags.addIgnore(it) } + flags.debugDumpClasses = debugDumpClasses.getOrElse(false) return flags -} \ No newline at end of file +} diff --git a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/DowngradeJar.kt b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/DowngradeJar.kt index d2affdb7..c80f7d3e 100644 --- a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/DowngradeJar.kt +++ b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/DowngradeJar.kt @@ -34,12 +34,7 @@ abstract class DowngradeJar : Jar(), DowngradeFlags { group = "JVMDowngrader" description = "Downgrades the jar to the specified version" - downgradeTo.convention(jvmdg.downgradeTo).finalizeValueOnRead() - apiJar.convention(jvmdg.apiJar).finalizeValueOnRead() - quiet.convention(jvmdg.quiet).finalizeValueOnRead() - debug.convention(jvmdg.debug).finalizeValueOnRead() - debugSkipStubs.convention(jvmdg.debugSkipStubs).finalizeValueOnRead() - + jvmdg.convention(this) } @TaskAction diff --git a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/ShadeJar.kt b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/ShadeJar.kt index 3d075fd8..cc2d18d0 100644 --- a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/ShadeJar.kt +++ b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/ShadeJar.kt @@ -31,12 +31,7 @@ abstract class ShadeJar : Jar(), ShadeFlags { group = "JVMDowngrader" description = "Downgrades the jar to the specified version" - downgradeTo.convention(jvmdg.downgradeTo).finalizeValueOnRead() - apiJar.convention(jvmdg.apiJar).finalizeValueOnRead() - quiet.convention(jvmdg.quiet).finalizeValueOnRead() - debug.convention(jvmdg.debug).finalizeValueOnRead() - debugSkipStubs.convention(jvmdg.debugSkipStubs).finalizeValueOnRead() - shadePath.convention(jvmdg.shadePath).finalizeValueOnRead() + jvmdg.convention(this) } @TaskAction diff --git a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/DowngradeFiles.kt b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/DowngradeFiles.kt index 1abdae93..fd5304cc 100644 --- a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/DowngradeFiles.kt +++ b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/DowngradeFiles.kt @@ -3,7 +3,6 @@ package xyz.wagyourtail.jvmdg.gradle.task.files import org.gradle.api.file.FileCollection import org.gradle.api.internal.ConventionTask import org.gradle.api.tasks.* -import org.jetbrains.annotations.ApiStatus import xyz.wagyourtail.jvmdg.ClassDowngrader import xyz.wagyourtail.jvmdg.compile.PathDowngrader import xyz.wagyourtail.jvmdg.gradle.flags.DowngradeFlags @@ -23,8 +22,20 @@ abstract class DowngradeFiles : ConventionTask(), DowngradeFlags { project.extensions.getByType(JVMDowngraderExtension::class.java) } + private var _inputCollection: FileCollection by FinalizeOnRead(MustSet()) + @get:InputFiles - var inputCollection: FileCollection by FinalizeOnRead(MustSet()) + var inputCollection: FileCollection + get() = _inputCollection + set(value) { + _inputCollection = value + + // use _inputCollection to finalize it immediately + val fd = _inputCollection.map { it to temporaryDir.resolve(it.name) } + outputs.dirs(*fd.filter { it.first.isDirectory }.map { it.second }.toTypedArray()) + outputs.files(*fd.filter { it.first.isFile }.map { it.second }.toTypedArray()) + + } @get:InputFiles var classpath: FileCollection by FinalizeOnRead(LazyMutable { @@ -41,20 +52,11 @@ abstract class DowngradeFiles : ConventionTask(), DowngradeFlags { */ @get:Internal val outputCollection: FileCollection by lazy { - val fd = inputCollection.map { it to temporaryDir.resolve(it.name) } - - outputs.dirs(*fd.filter { it.first.isDirectory }.map { it.second }.toTypedArray()) - outputs.files(*fd.filter { it.first.isFile }.map { it.second }.toTypedArray()) - outputs.files } init { - downgradeTo.convention(jvmdg.downgradeTo).finalizeValueOnRead() - apiJar.convention(jvmdg.apiJar).finalizeValueOnRead() - quiet.convention(jvmdg.quiet).finalizeValueOnRead() - debug.convention(jvmdg.debug).finalizeValueOnRead() - debugSkipStubs.convention(jvmdg.debugSkipStubs).finalizeValueOnRead() + jvmdg.convention(this) } @TaskAction diff --git a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/ShadeFiles.kt b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/ShadeFiles.kt index f800643c..59bc78de 100644 --- a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/ShadeFiles.kt +++ b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/task/files/ShadeFiles.kt @@ -43,12 +43,7 @@ abstract class ShadeFiles : ConventionTask(), ShadeFlags { } init { - downgradeTo.convention(jvmdg.downgradeTo).finalizeValueOnRead() - apiJar.convention(jvmdg.apiJar).finalizeValueOnRead() - quiet.convention(jvmdg.quiet).finalizeValueOnRead() - debug.convention(jvmdg.debug).finalizeValueOnRead() - debugSkipStubs.convention(jvmdg.debugSkipStubs).finalizeValueOnRead() - shadePath.convention(jvmdg.shadePath).finalizeValueOnRead() + jvmdg.convention(this) } // fun setShadePath(path: String) { diff --git a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/transform/ShadeTransform.kt b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/transform/ShadeTransform.kt index e0d667c3..492922ca 100644 --- a/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/transform/ShadeTransform.kt +++ b/gradle-plugin/src/main/kotlin/xyz/wagyourtail/jvmdg/gradle/transform/ShadeTransform.kt @@ -24,7 +24,7 @@ abstract class ShadeTransform : TransformAction { val flags = parameters val output = outputs.file("${input.nameWithoutExtension}-shaded-${flags.downgradeTo.get()}.${input.extension}") - ApiShader.shadeApis(flags.toFlags(), flags.shadePath.get().invoke(input.nameWithoutExtension), input, output, flags.apiJar.get()) + ApiShader.shadeApis(flags.toFlags(), flags.shadePath.get().invoke(input.nameWithoutExtension), input, output, flags.apiJar.get().toSet()) } } \ No newline at end of file diff --git a/gradle-plugin/test-downgrade/build.gradle.kts b/gradle-plugin/test-downgrade/build.gradle.kts index 12856ed8..6ec8b11c 100644 --- a/gradle-plugin/test-downgrade/build.gradle.kts +++ b/gradle-plugin/test-downgrade/build.gradle.kts @@ -89,7 +89,7 @@ sourceSets { val jvmdg = extensions.getByType(JVMDowngraderExtension::class.java) if (project.hasProperty("runningTest")) { - jvmdg.apiJar = project.file("../../java-api/build/libs/jvmdowngrader-java-api-${props.getProperty("version")}.jar") + jvmdg.apiJar = listOf(project.file("../../java-api/build/libs/jvmdowngrader-java-api-${props.getProperty("version")}.jar")) } val downgrade by configurations.creating diff --git a/gradle.properties b/gradle.properties index 28928fbe..68f03598 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx4G org.gradle.parallel=true -version=0.8.4 +version=0.9.0 asm_version=9.7 diff --git a/java-api/src/java15/java/xyz/wagyourtail/jvmdg/j15/stub/java_base/J_L_CharSequence.java b/java-api/src/java15/java/xyz/wagyourtail/jvmdg/j15/stub/java_base/J_L_CharSequence.java index bd6eb877..ab3d9707 100644 --- a/java-api/src/java15/java/xyz/wagyourtail/jvmdg/j15/stub/java_base/J_L_CharSequence.java +++ b/java-api/src/java15/java/xyz/wagyourtail/jvmdg/j15/stub/java_base/J_L_CharSequence.java @@ -4,7 +4,7 @@ public class J_L_CharSequence { - @Stub + @Stub(excludeChild = "java/lang/String") public static boolean isEmpty(CharSequence cs) { return cs.length() == 0; } diff --git a/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java b/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java index e0622f4a..324ec56a 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/ClassDowngrader.java @@ -49,7 +49,7 @@ public class ClassDowngrader implements Closeable { protected ClassDowngrader(@NotNull Flags flags) { this.flags = flags; this.target = flags.classVersion; - logger = new Logger(ClassDowngrader.class, flags.printDebug ? Logger.Level.DEBUG : flags.quiet ? Logger.Level.FATAL : Logger.Level.INFO, System.out); + logger = new Logger(ClassDowngrader.class, flags.getLogLevel(), flags.logAnsiColors, System.out); try { classLoader = new DowngradingClassLoader(this, ClassDowngrader.class.getClassLoader()); } catch (IOException e) { @@ -113,11 +113,12 @@ public synchronized Map collectProviders() { } public Set getMembers(int version, Type type, Set warnings) throws IOException { - for (int vers = version; vers > target; vers--) { + for (int vers = version; vers > target; ) { VersionProvider downgrader = downgraders.get(vers); if (downgrader == null) { throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet()); } + vers = downgrader.outputVersion; downgrader.ensureInit(this); Type stubbed = downgrader.stubClass(type, warnings); if (!stubbed.equals(type)) { @@ -158,11 +159,12 @@ public Set getMembers(int version, Type type, Set war } public List> getSupertypes(int version, Type type, Set warnings) throws IOException { - for (int vers = version; vers > target; vers--) { + for (int vers = version; vers > target;) { VersionProvider downgrader = downgraders.get(vers); if (downgrader == null) { throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet()); } + vers = downgrader.outputVersion; downgrader.ensureInit(this); Type stubbed = downgrader.stubClass(type, warnings); if (!stubbed.equals(type)) { @@ -191,11 +193,12 @@ public List> getSupertypes(int version, Type type, Set warnings) throws IOException { - for (int vers = version; vers > target; vers--) { + for (int vers = version; vers > target; ) { VersionProvider downgrader = downgraders.get(vers); if (downgrader == null) { throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet()); } + vers = downgrader.outputVersion; downgrader.ensureInit(this); Type stubbed = downgrader.stubClass(type, warnings); if (!stubbed.equals(type)) { @@ -214,11 +217,12 @@ public Boolean isInterface(int version, Type type, Set warnings) throws } public Type stubClass(int version, Type type, Set warnings) { - for (int vers = version; vers > target; vers--) { + for (int vers = version; vers > target;) { VersionProvider downgrader = downgraders.get(vers); if (downgrader == null) { throw new RuntimeException("Unsupported class version: " + vers + " supported: " + downgraders.keySet()); } + vers = downgrader.outputVersion; downgrader.ensureInit(this); Type stubbed = downgrader.stubClass(type, warnings); if (!stubbed.equals(type)) { @@ -286,7 +290,7 @@ public Map downgrade(/* in out */ AtomicReference name, } Map outputs = new HashMap<>(); try { - if (flags.printDebug) System.out.println("Transforming " + name.get()); + logger.trace("Transforming " + name.get()); Set extra = downgrade(node, enableRuntime, new Function() { @Override @@ -303,28 +307,31 @@ public ClassNode apply(String s) { } }); for (ClassNode c : extra) { - if (flags.printDebug) { - File f = new File(Constants.DEBUG_DIR, c.name + ".javasm"); - f.getParentFile().mkdirs(); - try (FileOutputStream fos = new FileOutputStream(f)) { - TraceClassVisitor tcv = new TraceClassVisitor(null, new Textifier(), new PrintWriter(fos)); - c.accept(tcv); - } catch (IOException ignored) { - } - } + // TODO: uncomment with asm 9.8 +// if (flags.debugDumpClasses) { +// File f = new File(Constants.DEBUG_DIR, c.name + ".javasm"); +// f.getParentFile().mkdirs(); +// try (FileOutputStream fos = new FileOutputStream(f)) { +// TraceClassVisitor tcv = new TraceClassVisitor(null, new Textifier(), new PrintWriter(fos)); +// c.accept(tcv); +// } catch (IOException ignored) { +// } +// } outputs.put(c.name, classNodeToBytes(c)); } } catch (Exception e) { throw new RuntimeException("Failed to downgrade " + name.get(), e); } - if (logger.is(Logger.Level.DEBUG)) { + if (logger.is(Logger.Level.DEBUG) || flags.debugDumpClasses) { for (Map.Entry entry : outputs.entrySet()) { if (!entry.getKey().equals(name.get())) { logger.debug("Downgraded " + entry.getKey() + " from unknown to " + target); } else { logger.debug("Downgraded " + entry.getKey() + " from " + version + " to " + target); } - writeBytesToDebug(entry.getKey(), entry.getValue()); + if (flags.debugDumpClasses) { + writeBytesToDebug(entry.getKey(), entry.getValue()); + } } } return outputs; diff --git a/src/main/java/xyz/wagyourtail/jvmdg/Constants.java b/src/main/java/xyz/wagyourtail/jvmdg/Constants.java index 8ce802e7..1e1d1518 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/Constants.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/Constants.java @@ -4,12 +4,18 @@ public class Constants { + @Deprecated public static final String QUIET = "jvmdg.quiet"; + public static final String LOG_ANSI_COLORS = "jvmdg.logAnsiColors"; + public static final String LOG_LEVEL = "jvmdg.logLevel"; public static final String JAVA_API = "jvmdg.java-api"; public static final String ALLOW_MAVEN_LOOKUP = "jvmdg.maven"; + public static final String IGNORE_WARNINGS = "jvmdg.ignoreWarnings"; + public static final String DEBUG = "jvmdg.debug"; public static final String DEBUG_SKIP_STUBS = "jvmdg.debug.skipStubs"; + public static final String DEBUG_DUMP_CLASSES = "jvmdg.debug.dumpClasses"; public static final File DIR = new File(".jvmdg"); diff --git a/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java b/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java index ec4ada64..a982809e 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/classloader/DowngradingClassLoader.java @@ -25,9 +25,11 @@ public class DowngradingClassLoader extends ClassLoader implements Closeable { public DowngradingClassLoader(ClassDowngrader downgrader) throws IOException { super(); - File apiJar = downgrader.flags.findJavaApi(); + Set apiJar = downgrader.flags.findJavaApi(); if (apiJar != null) { - delegates.add(new JarFileResourceProvider(new JarFile(apiJar))); + for (File file : apiJar) { + delegates.add(new JarFileResourceProvider(new JarFile(file))); + } } this.holder = downgrader; if (downgrader.target != Utils.getCurrentClassVersion()) { @@ -40,9 +42,11 @@ public DowngradingClassLoader(ClassDowngrader downgrader) throws IOException { public DowngradingClassLoader(ClassDowngrader downgrader, ClassLoader parent) throws IOException { super(parent); - File apiJar = downgrader.flags.findJavaApi(); + Set apiJar = downgrader.flags.findJavaApi(); if (apiJar != null) { - delegates.add(new JarFileResourceProvider(new JarFile(apiJar))); + for (File file : apiJar) { + delegates.add(new JarFileResourceProvider(new JarFile(file))); + } } this.holder = downgrader; if (downgrader.target != Utils.getCurrentClassVersion()) { diff --git a/src/main/java/xyz/wagyourtail/jvmdg/cli/Flags.java b/src/main/java/xyz/wagyourtail/jvmdg/cli/Flags.java index d2f9c9fc..1fd514c9 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/cli/Flags.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/cli/Flags.java @@ -1,8 +1,11 @@ package xyz.wagyourtail.jvmdg.cli; +import org.jetbrains.annotations.ApiStatus; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; import xyz.wagyourtail.jvmdg.ClassDowngrader; import xyz.wagyourtail.jvmdg.Constants; +import xyz.wagyourtail.jvmdg.logging.Logger; import xyz.wagyourtail.jvmdg.util.Consumer; import xyz.wagyourtail.jvmdg.util.Function; @@ -16,30 +19,95 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; +import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.TreeMap; public class Flags { public static final String jvmdgVersion = Flags.class.getPackage().getImplementationVersion(); + /** + * sets the target class version, default is {@link Opcodes#V1_8} + */ public int classVersion = Opcodes.V1_8; - public File api = null; + + /** + * sets the api jar to use, if null will attempt to automatically find it + */ + public Set api = null; + + /** + * sets the log level to {@link Logger.Level#FATAL} + * @deprecated use logLevel, if this is true, it will override {@link #logLevel} to {@link Logger.Level#FATAL} + */ + @Deprecated public boolean quiet = Boolean.getBoolean(Constants.QUIET); + + /** + * sets if the logger should use ansi colors for the console to look pretty + */ + public boolean logAnsiColors = Boolean.getBoolean(System.getProperty(Constants.LOG_ANSI_COLORS, "true")); + /** + * sets the log level + * @see Logger.Level + * @since 0.9.0 + */ + public Logger.Level logLevel = Logger.Level.valueOf(System.getProperty(Constants.LOG_LEVEL, "INFO").toUpperCase()); + + /** + * sets if any classes should be set to ignore missing class/member warnings + * this will prevent {@link xyz.wagyourtail.jvmdg.version.VersionProvider#printWarnings(Set, String)} for + * any classes in this set + *

+ * This set also allows for packages to be ignored, by ending with a {@code *} or {@code **} to ignore all sub-packages + * @since 0.9.0 + */ + public TreeMap ignoreWarningsIn = new TreeMap<>(); + + /** + * sets if maven lookup is allowed for auto resolving {@link #api} + */ public boolean allowMaven = Boolean.getBoolean(Constants.ALLOW_MAVEN_LOOKUP); // debug + /** + * sets the log level to {@link Logger.Level#DEBUG} + * @deprecated use logLevel, if this is true, it will override {@link #logLevel} to {@link Logger.Level#DEBUG} + */ + @Deprecated public boolean printDebug = Boolean.getBoolean(Constants.DEBUG); + + /** + * this skips applying stubs for the specified input class version, this will still apply the + * {@link xyz.wagyourtail.jvmdg.version.VersionProvider#otherTransforms(ClassNode, Set, Function, Set)} + * such as {@code INVOKE_INTERFACE} -> {@code INVOKE_SPECIAL} for private interface methods in java 9 -> 8 + */ public Set debugSkipStubs = new HashSet<>(getDebugSkip()); + /** + * sets if classes should be dumped to the {@link Constants#DEBUG_DIR} directory + * @since 0.9.0 + */ + public boolean debugDumpClasses = Boolean.getBoolean(Constants.DEBUG_DUMP_CLASSES); + + public Flags() { + getIgnoreWarnings(); + } + public Flags copy() { Flags flags = new Flags(); flags.classVersion = classVersion; flags.api = api; flags.quiet = quiet; + flags.logAnsiColors = logAnsiColors; + flags.logLevel = logLevel; flags.allowMaven = allowMaven; + flags.ignoreWarningsIn = new TreeMap<>(ignoreWarningsIn); flags.printDebug = printDebug; flags.debugSkipStubs = new HashSet<>(debugSkipStubs); + flags.debugDumpClasses = debugDumpClasses; return flags; } @@ -52,15 +120,137 @@ public Flags copy(Consumer modifier) { @Override public String toString() { return "Flags{" + - "classVersion=" + classVersion + - ", api=" + api + - ", quiet=" + quiet + - ", allowMaven=" + allowMaven + - ", printDebug=" + printDebug + - ", debugSkipStubs=" + debugSkipStubs + - '}'; + "classVersion=" + classVersion + + ", api=" + api + + ", quiet=" + quiet + + ", logAnsiColors=" + logAnsiColors + + ", logLevel=" + logLevel + + ", allowMaven=" + allowMaven + + ", printDebug=" + printDebug + + ", debugSkipStubs=" + debugSkipStubs + + ", ignoreWarningsIn=" + ignoreWarningsIn + + '}'; + } + + public void addIgnore(String s) { + if (s.endsWith("**")) { + ignoreWarningsIn.put(s.substring(0, s.length() - 2), WildcardType.DOUBLE); + } else if (s.endsWith("*")) { + ignoreWarningsIn.put(s.substring(0, s.length() - 1), WildcardType.SINGLE); + } else { + ignoreWarningsIn.put(s, WildcardType.NONE); + } + } + + /* getters */ + + /** + * internal method for retrieving the actual log level + */ + @ApiStatus.Internal + public Logger.Level getLogLevel() { + if (quiet) { + return Logger.Level.FATAL; + } + if (printDebug) { + return Logger.Level.DEBUG; + } + return logLevel; + } + + /** + * internal method to resolve the java api jar + */ + private static Set foundApi = null; + + public Set findJavaApi() { + try { + if (api != null) { + return api; + } + if (foundApi != null) { + api = foundApi; + return foundApi; + } + synchronized (Flags.class) { + Constants.DIR.mkdirs(); + Path tmp = Constants.DIR.toPath().resolve("jvmdg-api- " + jvmdgVersion + " .jar"); + Set prop = getJavaApiFromSystemProperty(); + if (prop != null) { + foundApi = prop; + return prop; + } + URL url = getJavaApiFromShade(); + if (Files.exists(tmp)) { + if (url == null) { + foundApi = Collections.singleton(tmp.toFile()); + return foundApi; + } else { + try (InputStream in = url.openStream()) { + try (InputStream in2 = Files.newInputStream(tmp)) { + if (hash(in).equals(hash(in2))) { + foundApi = Collections.singleton(tmp.toFile()); + return foundApi; + } + } + } + + } + } + if (url == null && allowMaven) { + url = getJavaApiFromMaven(); + } + if (url != null) { + try (InputStream in = url.openStream()) { + Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING); + foundApi = Collections.singleton(tmp.toFile()); + return foundApi; + } + } + return null; + } + } catch (IOException e) { + throw new RuntimeException("Failed to find java api", e); + } } + /** + * internal method to check if a class should be ignored for missing class/member warnings + */ + public boolean checkInIgnoreWarnings(String className) { + // find the entry that is <= the className, because a treeSet is sorted, this is either className, + // a prefix of classname, a random other string that is less than className, ie. "aaa" < "bbb", or null if there + // is no entries that are <= className in the map + Map.Entry entry = ignoreWarningsIn.floorEntry(className); + if (entry != null && className.startsWith(entry.getKey())) { + switch (entry.getValue()) { + case NONE: + if (className.equals(entry.getKey())) { + return true; + } + return true; + case SINGLE: + int lastSlash = className.lastIndexOf('/'); + if (lastSlash == -1) { + return true; + } + String parent = className.substring(0, lastSlash + 1); + String value = entry.getKey(); + if (!value.endsWith("/")) { + value = value.substring(0, value.lastIndexOf('/') + 1); + } + if (parent.equals(value)) { + return true; + } + break; + case DOUBLE: + return true; + } + } + return false; + } + + /* initialization methods */ private Set getDebugSkip() { Set skip = new HashSet<>(); String skipStubs = System.getProperty(Constants.DEBUG_SKIP_STUBS); @@ -71,16 +261,30 @@ private Set getDebugSkip() { return skip; } + private TreeMap getIgnoreWarnings() { + String ignoreWarnings = System.getProperty(Constants.IGNORE_WARNINGS); + if (ignoreWarnings == null) return new TreeMap<>(); + TreeMap map = new TreeMap<>(); + for (String s : ignoreWarnings.split(",")) { + addIgnore(s); + } + return map; + } + private URL getJavaApiFromShade() throws IOException { return ClassDowngrader.class.getResource("/META-INF/lib/java-api.jar"); } - private File getJavaApiFromSystemProperty() throws IOException { + private Set getJavaApiFromSystemProperty() throws IOException { String api = System.getProperty(Constants.JAVA_API); if (api == null) { return null; } - return new File(api); + Set files = new HashSet<>(); + for (String s : api.split(File.pathSeparator)) { + files.add(new File(s)); + } + return files; } private URL getJavaApiFromMaven() throws IOException { @@ -114,56 +318,10 @@ private URL getJavaApiFromMaven() throws IOException { } } - private static File foundApi = null; - - public File findJavaApi() { - try { - if (api != null) { - return api; - } - if (foundApi != null) { - api = foundApi; - return foundApi; - } - synchronized (Flags.class) { - Constants.DIR.mkdirs(); - Path tmp = Constants.DIR.toPath().resolve("jvmdg-api- " + jvmdgVersion + " .jar"); - File prop = getJavaApiFromSystemProperty(); - if (prop != null) { - return prop; - } - URL url = getJavaApiFromShade(); - if (Files.exists(tmp)) { - if (url == null) { - foundApi = tmp.toFile(); - return tmp.toFile(); - } else { - try (InputStream in = url.openStream()) { - try (InputStream in2 = Files.newInputStream(tmp)) { - if (hash(in).equals(hash(in2))) { - foundApi = tmp.toFile(); - return tmp.toFile(); - } - } - } - - } - } - if (url == null && allowMaven) { - url = getJavaApiFromMaven(); - } - if (url != null) { - try (InputStream in = url.openStream()) { - Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING); - foundApi = tmp.toFile(); - return tmp.toFile(); - } - } - return null; - } - } catch (IOException e) { - throw new RuntimeException("Failed to find java api", e); - } + public enum WildcardType { + NONE, + SINGLE, + DOUBLE } private String hash(InputStream is) { diff --git a/src/main/java/xyz/wagyourtail/jvmdg/cli/Main.java b/src/main/java/xyz/wagyourtail/jvmdg/cli/Main.java index 71dd367c..16e64262 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/cli/Main.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/cli/Main.java @@ -4,6 +4,7 @@ import xyz.wagyourtail.jvmdg.compile.ApiShader; import xyz.wagyourtail.jvmdg.compile.PathDowngrader; import xyz.wagyourtail.jvmdg.compile.ZipDowngrader; +import xyz.wagyourtail.jvmdg.logging.Logger; import xyz.wagyourtail.jvmdg.util.Utils; import java.io.File; @@ -25,12 +26,15 @@ public static void main(String[] args) throws IOException, ClassNotFoundExceptio parser.addChildren( new Arguments("--help", "Prints this help", new String[]{"-h"}, null), new Arguments("--version", "Prints the version", new String[]{"-v"}, null), - new Arguments("--quiet", "Suppress all warnings", new String[]{"-q"}, null), - new Arguments("--api", "Provide a java-api jar", new String[]{"-a"}, new String[]{"jar"}), + new Arguments("--logLevel", "Set the log level", new String[]{"-l"}, new String[]{"level"}), + new Arguments("--quiet", "[Deprecated] Suppress all warnings", new String[]{"-q"}, null), + new Arguments("--ignoreWarningsIn", "Ignore warnings of missing class/member stubs in package/class matching", new String[]{"-i"}, new String[]{"package or class identifier"}), + new Arguments("--api", "Provide a java-api jar or jars", new String[]{"-a"}, new String[]{"jar"}), new Arguments("--classVersion", "Target class version (ex. \"52\" for java 8)", new String[]{"-c"}, new String[]{"version"}), new Arguments("debug", "Set debug flags/call debug actions", null, null).addChildren( - new Arguments("--print", "Enable printing debug info", new String[]{"-p"}, null), + new Arguments("--print", "[Deprecated] Enable printing debug info", new String[]{"-p"}, null), new Arguments("--skipStubs", "Skip method/class stubs for these class versions", new String[]{"-s"}, new String[]{"versions"}), + new Arguments("--dumpClasses", "Dump classes to the debug folder", new String[]{"-d"}, null), new Arguments("downgradeApi", "Retrieves and downgrades the java api jar", null, new String[]{"outputPath"}) ), new Arguments("downgrade", "Downgrades a jar or folder", null, null).addChildren( @@ -73,19 +77,34 @@ public static void main(String[] args) throws IOException, ClassNotFoundExceptio case "--quiet": flags.quiet = true; break; + case "--logLevel": + if (entry.getValue().size() > 1) { + throw new IllegalArgumentException("Multiple log levels specified"); + } + flags.logLevel = Logger.Level.valueOf(entry.getValue().get(0)[0].toUpperCase()); + break; case "--classVersion": if (entry.getValue().size() > 1) { throw new IllegalArgumentException("Multiple class versions specified"); } flags.classVersion = Integer.parseInt(entry.getValue().get(0)[0]); break; - case "--api": - if (entry.getValue().size() > 1) { - throw new IllegalArgumentException("Multiple api paths specified"); + case "--ignoreWarningsIn": + for (String[] s : entry.getValue()) { + for (String string : s) { + flags.addIgnore(string); + } } - File api = new File(entry.getValue().get(0)[0]); - if (!api.exists()) { - throw new IllegalArgumentException("Api jar does not exist"); + break; + case "--api": + Set api = new HashSet<>(); + for (String[] s : entry.getValue()) { + for (String string : s) { + String[] split = string.split(File.pathSeparator); + for (String s1 : split) { + api.add(new File(s1)); + } + } } flags.api = api; break; @@ -123,6 +142,9 @@ public static void debug(Map> args) throws IOException { case "--print": flags.printDebug = true; break; + case "--dumpClasses": + flags.debugDumpClasses = true; + break; case "--skipStubs": for (String[] s : entry.getValue()) { for (String string : s) { @@ -137,8 +159,14 @@ public static void debug(Map> args) throws IOException { if (args.get("downgradeApi").size() > 1) { throw new IllegalArgumentException("Multiple output paths specified"); } + if (flags.findJavaApi().isEmpty()){ + throw new IllegalArgumentException("No api jar found"); + } + if (flags.findJavaApi().size() > 1) { + throw new IllegalArgumentException("Multiple api jars found"); + } try (ClassDowngrader downgrader = ClassDowngrader.downgradeTo(flags)) { - ZipDowngrader.downgradeZip(downgrader, flags.api.toPath(), new HashSet(), new File(args.get("downgradeApi").get(0)[0]).toPath()); + ZipDowngrader.downgradeZip(downgrader, flags.findJavaApi().iterator().next().toPath(), new HashSet(), new File(args.get("downgradeApi").get(0)[0]).toPath()); } } } @@ -222,16 +250,18 @@ public static void shade(Map> args) throws IOException { if (args.get("--prefix").size() > 1) { throw new IllegalArgumentException("Multiple prefixes specified"); } - File downgradedApi = null; + Set downgradedApi = new HashSet<>(); if (args.containsKey("--downgradedApi")) { - if (args.get("--downgradedApi").size() > 1) { - throw new IllegalArgumentException("Multiple downgraded api paths specified"); - } - downgradedApi = new File(args.get("--downgradedApi").get(0)[0]); - if (!downgradedApi.exists()) { - throw new IllegalArgumentException("Downgraded api jar does not exist"); + for (String[] s : args.get("--downgradedApi")) { + String[] split = s[0].split(File.pathSeparator); + for (String s1 : split) { + downgradedApi.add(new File(s1)); + } } } + if (downgradedApi.isEmpty()) { + downgradedApi = null; + } Map targets = new HashMap<>(); List fileSystems = new ArrayList<>(); diff --git a/src/main/java/xyz/wagyourtail/jvmdg/compile/ApiShader.java b/src/main/java/xyz/wagyourtail/jvmdg/compile/ApiShader.java index 662013e2..ccc3985b 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/compile/ApiShader.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/compile/ApiShader.java @@ -42,92 +42,127 @@ public static void main(String[] args) throws IOException { } Flags flags = new Flags(); flags.classVersion = target; - shadeApis(flags, prefix, input, output, downgradedApi); + shadeApis(flags, prefix, input, output, Collections.singleton(downgradedApi)); System.out.println("Shaded in " + (System.currentTimeMillis() - start) + "ms"); } - public static void downgradedApi(Flags flags, Path api, Path targetPath) throws IOException { - try (ClassDowngrader downgrader = ClassDowngrader.downgradeTo(flags)) { - ZipDowngrader.downgradeZip(downgrader, api, new HashSet(), targetPath); - } - } - - public static void shadeApis(Flags flags, String prefix, File input, File output, File downgradedApi) throws IOException { + public static void shadeApis(Flags flags, String prefix, File input, File output, Set downgradedApi) throws IOException { if (output.exists()) { output.delete(); } if (!prefix.endsWith("/")) { prefix += "/"; } - try (FileSystem apiFs = Utils.openZipFileSystem(resolveDowngradedApi(flags, downgradedApi), false)) { + List apiFs = new ArrayList<>(); + try { + for (File file : downgradedApi) { + apiFs.add(Utils.openZipFileSystem(file.toPath(), false)); + } + try (FileSystem inputFs = Utils.openZipFileSystem(input.toPath(), false)) { try (FileSystem outputFs = Utils.openZipFileSystem(output.toPath(), true)) { - Path apiRoot = apiFs.getPath("/"); - Pair> api = scanApis(apiRoot); - shadeApis(prefix, inputFs.getPath("/"), outputFs.getPath("/"), apiRoot, api.getFirst(), api.getSecond()); + List apiRoots = new ArrayList<>(); + for (FileSystem fs : apiFs) { + apiRoots.add(fs.getPath("/")); + } + Pair> api = scanApis(apiRoots); + shadeApis(prefix, inputFs.getPath("/"), outputFs.getPath("/"), apiRoots, api.getFirst(), api.getSecond()); } } + } finally { + for (FileSystem fs : apiFs) { + fs.close(); + } } } - public static void shadeApis(Flags flags, String prefix, Path inputRoot, Path outputRoot, File downgradedApi) throws IOException { + public static void shadeApis(Flags flags, String prefix, Path inputRoot, Path outputRoot, Set downgradedApi) throws IOException { shadeApis(flags, Collections.singletonList(prefix), Collections.singletonList(inputRoot), Collections.singletonList(outputRoot), downgradedApi); } - public static void shadeApis(Flags flags, List prefix, List inputRoots, List outputRoots, File downgradedApi) throws IOException { + public static void shadeApis(Flags flags, List prefix, List inputRoots, List outputRoots, Set downgradedApi) throws IOException { for (String p : prefix) { if (!p.endsWith("/")) { throw new IllegalArgumentException("prefix \""+ p +"\" must end with /"); } } - Path downgradedApiPath = resolveDowngradedApi(flags, downgradedApi); - try (FileSystem apiFs = Utils.openZipFileSystem(downgradedApiPath,false)) { - Pair> api = scanApis(apiFs.getPath("/")); + Set downgradedApiPath = resolveDowngradedApi(flags, downgradedApi); + List apiFs = new ArrayList<>(); + try { + for (File file : downgradedApi) { + apiFs.add(Utils.openZipFileSystem(file.toPath(), false)); + } + List apiRoots = new ArrayList<>(); + for (FileSystem fs : apiFs) { + apiRoots.add(fs.getPath("/")); + } + Pair> api = scanApis(apiRoots); for (int i = 0; i < inputRoots.size(); i++) { - shadeApis(prefix.get(i % prefix.size()), inputRoots.get(i), outputRoots.get(i), apiFs.getPath("/"), api.getFirst(), api.getSecond()); + shadeApis(prefix.get(i % prefix.size()), inputRoots.get(i), outputRoots.get(i), apiRoots, api.getFirst(), api.getSecond()); + } + } finally { + for (FileSystem fs : apiFs) { + fs.close(); } } } - public static Path resolveDowngradedApi(Flags flags, @Nullable File downgradedApi) throws IOException { + public static Set resolveDowngradedApi(Flags flags, @Nullable Set downgradedApi) throws IOException { // step 1: downgrade the api to the target version - Path temp = Constants.DIR.toPath(); - Path downgradedApiPath = temp.resolve("downgraded-api.jar"); + Set downgradedApis = new HashSet<>(); if (downgradedApi == null) { - downgradeApi(flags, downgradedApiPath); + try (ClassDowngrader downgrader = ClassDowngrader.downgradeTo(flags)) { + for (File file : flags.findJavaApi()) { + String name = file.getName(); + int idx = name.lastIndexOf('.'); + if (idx == -1) { + throw new IllegalArgumentException("File has no extension: " + name); + } + String beforeExt = name.substring(0, idx); + String ext = name.substring(idx); + Path targetPath = file.toPath().resolveSibling(beforeExt + "-downgraded" + flags.classVersion + ext); + ZipDowngrader.downgradeZip(downgrader, file.toPath(), new HashSet(), targetPath); + downgradedApis.add(targetPath); + } + } } else { - downgradedApiPath = downgradedApi.toPath(); + for (File file : downgradedApi) { + downgradedApis.add(file.toPath()); + } } - return downgradedApiPath; + return downgradedApis; } - public static void downgradeApi(Flags flags, Path outputLocation) throws IOException { - downgradedApi(flags, flags.findJavaApi().toPath(), outputLocation); - } - - public static Pair> scanApis(Path apiRoot) throws IOException { + public static Pair> scanApis(List apiRoots) throws IOException { // step 2: collect classes in the api and their references try { ReferenceGraph apiRefs = new ReferenceGraph(); - Map preScan = apiRefs.preScan(apiRoot); - final Set apiClasses = new HashSet<>(preScan.values()); - apiRefs.scan(preScan, new ReferenceGraph.Filter() { - @Override - public boolean shouldInclude(FullyQualifiedMemberNameAndDesc member) { - return apiClasses.contains(member.getOwner()); - } - }); + List> preScans = new ArrayList<>(); + final Set apiClasses = new HashSet<>(); + for (Path apiRoot : apiRoots) { + Map preScan = apiRefs.preScan(apiRoot); + preScans.add(preScan); + apiClasses.addAll(preScan.values()); + } + for (Map preScan : preScans) { + apiRefs.scan(preScan, new ReferenceGraph.Filter() { + @Override + public boolean shouldInclude(FullyQualifiedMemberNameAndDesc member) { + return apiClasses.contains(member.getOwner()); + } + }); + } return new Pair<>(apiRefs, apiClasses); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } } - public static void shadeApis(final String prefix, final Path inputRoot, final Path outputRoot, final Path apiRoot, final ReferenceGraph apiRefs, final Set apiClasses) throws IOException { +public static void shadeApis(final String prefix, final Path inputRoot, final Path outputRoot, final List apiRoots, final ReferenceGraph apiRefs, final Set apiClasses) throws IOException { if (!prefix.endsWith("/")) throw new IllegalArgumentException("prefix must end with /"); try { // step 3: traverse the input classes for references to the api - final ReferenceGraph inputRefs = new ReferenceGraph(); + final ReferenceGraph inputRefs = new ReferenceGraph(true); inputRefs.scan(inputRoot, new ReferenceGraph.Filter() { @Override public boolean shouldInclude(FullyQualifiedMemberNameAndDesc member) { @@ -183,13 +218,18 @@ public void accept(Map.Entry> type) throws IOExcept Future resourceWrite = AsyncUtils.forEachAsync(required.getSecond(), new IOConsumer() { @Override public void accept(String resource) throws IOException { - Path inPath = apiRoot.resolve(resource); - Path outPath = outputRoot.resolve(resource); - Path parent = outPath.getParent(); - if (parent != null) { - Files.createDirectories(parent); + for (Path apiRoot : apiRoots) { + Path inPath = apiRoot.resolve(resource); + if (Files.exists(inPath)) { + Path outPath = outputRoot.resolve(resource); + Path parent = outPath.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.copy(inPath, outPath, StandardCopyOption.REPLACE_EXISTING); + return; + } } - Files.copy(inPath, outPath, StandardCopyOption.REPLACE_EXISTING); } }); diff --git a/src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java b/src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java index 4528ec75..6868aa96 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/logging/Logger.java @@ -11,16 +11,18 @@ public class Logger { private final String prefix; private final Level level; + private final boolean useAnsiColors; private final PrintStream out; - public Logger(String prefix, Level level, PrintStream out) { + public Logger(String prefix, Level level, boolean useAnsiColors, PrintStream out) { this.prefix = prefix; this.level = level; this.out = out; + this.useAnsiColors = useAnsiColors; } - public Logger(Class clazz, Level level, PrintStream out) { - this(clazz.getSimpleName(), level, out); + public Logger(Class clazz, Level level, boolean useAnsiColors, PrintStream out) { + this(clazz.getSimpleName(), level, useAnsiColors, out); } public boolean is(Level level) { @@ -29,7 +31,12 @@ public boolean is(Level level) { public void log(Level level, String message) { if (is(level)) { - out.println(level.wrap("[" + prefix + "] " + level + ": " + message)); + String messageContent = "[" + prefix + "] " + level + ": " + message; + if (useAnsiColors) { + out.println(level.ansiColor(messageContent)); + } else { + out.println(messageContent); + } } } @@ -88,7 +95,7 @@ public void accept(PrintStream printStream) { } public Logger subLogger(String prefix) { - return new Logger(this.prefix + "/" + prefix, level, out); + return new Logger(this.prefix + "/" + prefix, level, useAnsiColors, out); } public Logger subLogger(Class clazz) { @@ -129,7 +136,7 @@ public AnsiColor getAnsiColor() { return ansiColor; } - public String wrap(String message) { + public String ansiColor(String message) { return ansiColor.wrap(message); } } diff --git a/src/main/java/xyz/wagyourtail/jvmdg/runtime/Bootstrap.java b/src/main/java/xyz/wagyourtail/jvmdg/runtime/Bootstrap.java index 3907fcac..3cfc2809 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/runtime/Bootstrap.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/runtime/Bootstrap.java @@ -4,6 +4,7 @@ import xyz.wagyourtail.jvmdg.Constants; import xyz.wagyourtail.jvmdg.cli.Flags; import xyz.wagyourtail.jvmdg.compile.ZipDowngrader; +import xyz.wagyourtail.jvmdg.logging.Logger; import xyz.wagyourtail.jvmdg.util.Utils; import java.io.File; @@ -18,18 +19,13 @@ import java.security.CodeSource; import java.security.MessageDigest; import java.util.HashSet; +import java.util.Set; import java.util.jar.JarFile; -import java.util.logging.Level; -import java.util.logging.Logger; public class Bootstrap { static final Flags flags = new Flags(); static final ClassDowngrader currentVersionDowngrader = ClassDowngrader.getCurrentVersionDowngrader(flags); - private static final Logger LOGGER = Logger.getLogger("JVMDowngrader"); - - static { - LOGGER.setLevel(flags.printDebug ? Level.ALL : Level.WARNING); - } + static final Logger LOGGER = new Logger("JVMDowngrader", flags.getLogLevel(), flags.logAnsiColors, System.out); public static void main(String[] args) { LOGGER.info("Starting JVMDowngrader Bootstrap in standalone mode."); @@ -71,30 +67,38 @@ public static String sha1(Path p) { public static void premain(String args, Instrumentation instrumentation) throws IOException, URISyntaxException, UnmodifiableClassException { LOGGER.info("Starting JVMDowngrader Bootstrap in agent mode."); // downgrade api - Path zip = flags.findJavaApi().toPath(); - String zipSha = sha1(zip); - Path tmp = Constants.DIR.toPath().resolve("java-api-downgraded-" + currentVersionDowngrader.target + "-" + zipSha.substring(0, 8) + ".jar"); - boolean downgrade = false; - if (!Files.exists(tmp)) { - LOGGER.warning("Downgrading java-api.jar as its hash changed or this is first launch, this may take a minute..."); - downgrade = true; - } - if (downgrade) { - Files.createDirectories(tmp.getParent()); - for (File file : tmp.getParent().toFile().listFiles()) { - if (file.isDirectory()) continue; - file.delete(); + for (File zip : flags.findJavaApi()) { + String zipSha = sha1(zip.toPath()); + String name = zip.getName(); + int idx = name.lastIndexOf('.'); + if (idx != -1) { + name = name.substring(0, idx); + } + Path tmp = Constants.DIR.toPath().resolve(name + "-downgraded-" + currentVersionDowngrader.target + "-" + zipSha.substring(0, 8) + ".jar"); + boolean downgrade = false; + if (!Files.exists(tmp)) { + LOGGER.warn("Downgrading " + zip.getName() + " as its hash changed or this is first launch, this may take a minute..."); + downgrade = true; + } + if (downgrade) { + Files.createDirectories(tmp.getParent()); + for (File file : tmp.getParent().toFile().listFiles()) { + if (file.isDirectory()) continue; + if (file.getName().startsWith(name + "-downgraded-" + currentVersionDowngrader.target) && file.getName().endsWith(".jar")) { + file.delete(); + } + } + Files.createDirectories(tmp.getParent()); + ZipDowngrader.downgradeZip(currentVersionDowngrader, zip.toPath(), new HashSet(), tmp); } - Files.createDirectories(tmp.getParent()); - ZipDowngrader.downgradeZip(currentVersionDowngrader, zip, new HashSet(), tmp); + instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(tmp.toFile())); } - instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(tmp.toFile())); // add self URL self = ClassDowngrader.class.getProtectionDomain().getCodeSource().getLocation(); instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(self.toURI().getPath())); instrumentation.addTransformer(new ClassDowngradingAgent(), instrumentation.isRetransformClassesSupported()); if (!instrumentation.isModifiableClass(CodeSource.class) || !instrumentation.isRetransformClassesSupported()) { - LOGGER.severe("CodeSource is not modifiable, this will prevent loading signed classes properly!!!"); + LOGGER.fatal("CodeSource is not modifiable, this will prevent loading signed classes properly!!!"); } else { LOGGER.info("CodeSource is modifiable, attempting to retransform it to fix code signing."); instrumentation.retransformClasses(CodeSource.class); diff --git a/src/main/java/xyz/wagyourtail/jvmdg/runtime/ClassDowngradingAgent.java b/src/main/java/xyz/wagyourtail/jvmdg/runtime/ClassDowngradingAgent.java index 0d55ed86..308010af 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/runtime/ClassDowngradingAgent.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/runtime/ClassDowngradingAgent.java @@ -1,6 +1,7 @@ package xyz.wagyourtail.jvmdg.runtime; import org.objectweb.asm.*; +import xyz.wagyourtail.jvmdg.logging.Logger; import xyz.wagyourtail.jvmdg.util.Function; import xyz.wagyourtail.jvmdg.util.Utils; @@ -13,19 +14,13 @@ import java.security.ProtectionDomain; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Level; -import java.util.logging.Logger; public class ClassDowngradingAgent implements ClassFileTransformer { public static final MethodHandle defineClass; public static final boolean DUMP_CLASSES = Boolean.parseBoolean(System.getProperty("jvmdg.dump", "false")); - private static final Logger LOGGER = Logger.getLogger("JVMDowngrader/Agent"); + private static final Logger LOGGER = Bootstrap.LOGGER.subLogger("Agent"); private static final int currentVersion; - static { - LOGGER.setLevel(Bootstrap.flags.printDebug ? Level.ALL : Level.OFF); - } - static { Method md; try { @@ -89,10 +84,10 @@ public byte[] transform(final ClassLoader loader, String className, Class cla int version = ((bytes[6] & 0xFF) << 8) | (bytes[7] & 0xFF); if (version <= currentVersion) { // already at or below the target version - LOGGER.finer("Ignoring " + className + " as it is already at or below the target version"); + LOGGER.trace("Ignoring " + className + " as it is already at or below the target version"); return null; } - LOGGER.fine("Transforming " + className + " from " + version + " to " + currentVersion); + LOGGER.trace("Transforming " + className + " from " + version + " to " + currentVersion); // if (loader instanceof DowngradingClassLoader) return bytes; // already handled Map outputs = Bootstrap.currentVersionDowngrader.downgrade(new AtomicReference<>(className), bytes, true, new Function() { @Override @@ -106,11 +101,11 @@ public byte[] apply(String s) { } } }); - LOGGER.fine("transform size: " + (outputs == null ? null : outputs.size())); + LOGGER.trace("transform size: " + (outputs == null ? null : outputs.size())); if (outputs == null) return bytes; bytes = null; for (Map.Entry entry : outputs.entrySet()) { - LOGGER.fine("Loading " + entry.getKey() + " into " + loader); + LOGGER.trace("Loading " + entry.getKey() + " into " + loader); if (DUMP_CLASSES) { Bootstrap.currentVersionDowngrader.writeBytesToDebug(entry.getKey(), entry.getValue()); } diff --git a/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java b/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java index 5c2e41d5..c5ba1044 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/version/VersionProvider.java @@ -331,6 +331,7 @@ public void stub(Class clazz) { logger.warn("failed to create stub(s) for " + clazz.getName(), e); } if (!warnings.isEmpty() && logger.is(Logger.Level.WARN)) { + if (downgrader.flags.checkInIgnoreWarnings(clazz.getName())) return; StringBuilder sb = new StringBuilder(); for (String warning : warnings) { sb.append(" ").append(warning).append("\n"); @@ -420,11 +421,13 @@ public ClassNode stubMethods(ClassNode owner, Set extra, boolean enab owner.methods.set(owner.methods.indexOf(method), newMethod); } if (!warnings.isEmpty() && logger.is(Logger.Level.WARN)) { - StringBuilder sb = new StringBuilder(); - for (String warning : warnings) { - sb.append(" ").append(warning).append("\n"); + if (!downgrader.flags.checkInIgnoreWarnings(owner.name + "." + method.name)) { + StringBuilder sb = new StringBuilder(); + for (String warning : warnings) { + sb.append(" ").append(warning).append("\n"); + } + logger.warn("Warnings for " + owner.name + "." + method.name + method.desc + " (" + warnings.size() + ") : \n" + sb); } - logger.warn("Warnings for " + owner.name + "." + method.name + method.desc + " (" + warnings.size() + ") : \n" + sb); } } return owner; @@ -836,12 +839,13 @@ public ClassNode apply(ClassNode classNode) throws IOException { return null; } printWarnings(warnings, className); - clazz.version = inputVersion - 1; + clazz.version = outputVersion; return clazz; } - private void printWarnings(Set warnings, String className) { + public void printWarnings(Set warnings, String className) { if (!warnings.isEmpty() && logger.is(Logger.Level.WARN)) { + if (downgrader.flags.checkInIgnoreWarnings(className)) return; StringBuilder sb = new StringBuilder(); for (String warning : warnings) { sb.append(" ").append(warning).append("\n"); diff --git a/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java b/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java index b14ce474..ede995e9 100644 --- a/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java +++ b/src/main/java/xyz/wagyourtail/jvmdg/version/map/ClassMapping.java @@ -187,6 +187,18 @@ public Pair getParentStubFor(MemberNameAndDesc member, boolean run for (ClassMapping parent : parents.get()) { Pair node = parent.getStubFor(member, false, runtimeAvailable, special, warnings); if (node != null) { + + // check if explicitly excluded from stubbing + // ie. method exists on the subtype in question + for (String child : node.getSecond().excludeChild()) { + if (child.startsWith("L") && child.endsWith(";")) { + child = child.substring(1, child.length() - 1); + } + if (child.equals(current.getInternalName())) { + return null; + } + } + return node; } } diff --git a/src/shared/java/xyz/wagyourtail/jvmdg/util/AsyncUtils.java b/src/shared/java/xyz/wagyourtail/jvmdg/util/AsyncUtils.java index 017773ad..68df1295 100644 --- a/src/shared/java/xyz/wagyourtail/jvmdg/util/AsyncUtils.java +++ b/src/shared/java/xyz/wagyourtail/jvmdg/util/AsyncUtils.java @@ -133,14 +133,6 @@ public void run() { return FileVisitResult.CONTINUE; } }); -// while (!futures.isEmpty()) { -// try { -// futures.poll().get(); -// } catch (InterruptedException | ExecutionException e) { -// throw new RuntimeException(e); -// } -// } -// return null; return (Future) waitForFutures(futures); } diff --git a/src/shared/java/xyz/wagyourtail/jvmdg/version/Stub.java b/src/shared/java/xyz/wagyourtail/jvmdg/version/Stub.java index 9f775952..4b959a72 100644 --- a/src/shared/java/xyz/wagyourtail/jvmdg/version/Stub.java +++ b/src/shared/java/xyz/wagyourtail/jvmdg/version/Stub.java @@ -32,4 +32,6 @@ boolean noSpecial() default false; + String[] excludeChild() default {}; + } \ No newline at end of file diff --git a/src/test/java/xyz/wagyourtail/jvmdg/test/JavaRunner.java b/src/test/java/xyz/wagyourtail/jvmdg/test/JavaRunner.java index d8421231..8c704f8b 100644 --- a/src/test/java/xyz/wagyourtail/jvmdg/test/JavaRunner.java +++ b/src/test/java/xyz/wagyourtail/jvmdg/test/JavaRunner.java @@ -234,7 +234,8 @@ public enum JavaVersion { V19, V20, V21, - V22; + V22, + V23; public static JavaVersion fromClassVers(int vers) { return JavaVersion.values()[vers - 45]; diff --git a/src/test/java/xyz/wagyourtail/jvmdg/test/ClassRunner.java b/src/test/java/xyz/wagyourtail/jvmdg/test/integration/ClassRunner.java similarity index 97% rename from src/test/java/xyz/wagyourtail/jvmdg/test/ClassRunner.java rename to src/test/java/xyz/wagyourtail/jvmdg/test/integration/ClassRunner.java index 1a001eb7..2477b43f 100644 --- a/src/test/java/xyz/wagyourtail/jvmdg/test/ClassRunner.java +++ b/src/test/java/xyz/wagyourtail/jvmdg/test/integration/ClassRunner.java @@ -1,4 +1,4 @@ -package xyz.wagyourtail.jvmdg.test; +package xyz.wagyourtail.jvmdg.test.integration; import org.apache.commons.io.function.IOStream; import org.junit.jupiter.api.BeforeAll; @@ -14,6 +14,7 @@ import xyz.wagyourtail.jvmdg.cli.Flags; import xyz.wagyourtail.jvmdg.compile.ApiShader; import xyz.wagyourtail.jvmdg.compile.ZipDowngrader; +import xyz.wagyourtail.jvmdg.test.JavaRunner; import xyz.wagyourtail.jvmdg.util.Utils; import java.io.File; @@ -68,7 +69,7 @@ private static Stream> zip(Stream a, Stream b) { private static Stream flags() { Flags flags = ClassRunner.flags.copy(); flags.quiet = true; - flags.api = javaApi.toFile(); + flags.api = Set.of(javaApi.toFile()); return Stream.of( new FlagsAndRunner(flags.copy(e -> e.classVersion = JavaRunner.JavaVersion.V1_8.toOpcode()), JavaRunner.JavaVersion.V1_8) @@ -175,7 +176,7 @@ private static synchronized Path getShadedJar(FlagsAndRunner flags) { "downgradetest", getDowngradedJar(flags).toFile(), target.toFile(), - getApiJar(flags).toFile() + Set.of(getApiJar(flags).toFile()) ); return target; } catch (IOException ex) { @@ -197,7 +198,7 @@ private static synchronized Path getApiJar(FlagsAndRunner flags) { Path target = getApiPath(flags); ZipDowngrader.downgradeZip( ClassDowngrader.downgradeTo(flags.flags), - flags.flags.api.toPath(), + javaApi, Set.of(), target ); @@ -218,7 +219,7 @@ public static void clearPrevious() throws IOException { } @ParameterizedTest - @MethodSource({"xyz.wagyourtail.jvmdg.test.ClassRunner#arguments"}) + @MethodSource({"xyz.wagyourtail.jvmdg.test.integration.ClassRunner#arguments"}) @Execution(ExecutionMode.CONCURRENT) public void testDowngrade(String mainClass, FlagsAndRunner javaVersion) throws IOException, InterruptedException { System.out.println("TEST_DOWNGRADE: Running " + mainClass + " on " + javaVersion.readableSlug()); @@ -389,6 +390,7 @@ public void testRuntime(String mainClass, FlagsAndRunner javaVersion) throws IOE } public static void compareResults(String mainClass, FlagsAndRunner javaVersion, Map.Entry originalResult, Map.Entry downgradedResult) { + assertEquals(0, originalResult.getKey()); assertEquals(originalResult.getValue(), downgradedResult.getValue(), "Output mismatch for " + mainClass + " on " + javaVersion.readableSlug()); assertEquals(originalResult.getKey(), downgradedResult.getKey(), "Exit code mismatch for " + mainClass + " on " + javaVersion.readableSlug()); } diff --git a/src/test/java/xyz/wagyourtail/jvmdg/test/unit/TestFlags.java b/src/test/java/xyz/wagyourtail/jvmdg/test/unit/TestFlags.java new file mode 100644 index 00000000..42e2d0b9 --- /dev/null +++ b/src/test/java/xyz/wagyourtail/jvmdg/test/unit/TestFlags.java @@ -0,0 +1,31 @@ +package xyz.wagyourtail.jvmdg.test.unit; + +import org.junit.jupiter.api.Test; +import xyz.wagyourtail.jvmdg.cli.Flags; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestFlags { + + + @Test + public void testClassIn() { + Flags f = new Flags(); + f.addIgnore("aaa/bbb/*"); + f.addIgnore("aaa/bbb/ccc/*"); + f.addIgnore("bbb/**"); + f.addIgnore("ccc/aa/dd*"); + + assertTrue(f.checkInIgnoreWarnings("aaa/bbb/ccc/ddd")); + assertTrue(f.checkInIgnoreWarnings("aaa/bbb/ccc")); + assertFalse(f.checkInIgnoreWarnings("aaa/bbb/ddd/ccc")); + assertTrue(f.checkInIgnoreWarnings("bbb/aaa/ccc")); + assertTrue(f.checkInIgnoreWarnings("bbb/ccc")); + assertTrue(f.checkInIgnoreWarnings("bbb/ccc/ddd")); + assertTrue(f.checkInIgnoreWarnings("ccc/aa/ddd")); + assertTrue(f.checkInIgnoreWarnings("ccc/aa/dddc")); + assertFalse(f.checkInIgnoreWarnings("ccc/aa/dd/ee")); + assertFalse(f.checkInIgnoreWarnings("ccc/aa/de")); + } + +}