Skip to content

Downgrades modern java bytecode to older versions. at either compile or runtime.

License

Notifications You must be signed in to change notification settings

unimined/JvmDowngrader

Repository files navigation

JvmDowngrader

Downgrades modern java bytecode to older versions. at either compile or runtime.

This is currently capable of downgrading from Java 22 to Java 8. Java 7 may come in the future.

Currently attempting to downgrade to Java 7 will produce valid class files, but some of the API stubs are broken, and many common ones dont exist.

After downgrading you must either shade, or add the api jar to the classpath at runtime.

It is recommended to use the shade task/cli as documented below, as it will only include necessary methods in your jar.

alternatively, you can add the api jar in its entirety to the classpath when running the jar.

The api jar can be found at xyz.wagyourtail.jvmdowngrader:jvmdowngrader-java-api:0.9.0:downgraded-8 there is also a downgraded-11 jar there.

alternatively, to produce other versions you can generate one yourself using the cli: java -jar JvmDowngrader-all.jar -c 53 debug downgradeApi ./java-api-9.jar

Api jars for older java versions can still be used for newer java, but may not be as efficient.

Gradle Plugin

This downgrades the output of a jar task using another task. Note that certain things like reflection and dynamic class definition downgrading will not work without runtime downgrading. dynamic class definitions being things like MethodHandles$Lookup#defineClass and classloader shenanigans.

add my maven in settings.gradle:

JvmDowngrader is now on the gradle plugin portal, so you can skip this step, unless you want to use a snapshot version.

pluginManagement {
    repositories {
        mavenLocal()
        maven {
            url = "https://maven.wagyourtail.xyz/releases"
        }
        maven {
            url = "https://maven.wagyourtail.xyz/snapshots"
        }
        mavenCentral()
        gradlePluginPortal()
    }
}

in build.gradle:

// add the plugin
plugins {
    id 'xyz.wagyourtail.jvmdowngrader' version '$jvmdgVersion'
}

// optionally you can change some globals, here are their default values:
jvmdg.downgradeTo = JavaVersion.VERSION_1_8
jvmdg.apiJar = [this.getClass().getResourceAsStream("jvmdg/java-api-${version}.jar").writeToFile("build/jvmdg/java-api-${version}.jar")]
jvmdg.quiet = false
jvmdg.debug = false
jvmdg.logLevel = "INFO"
jvmdg.ignoreWarningsIn = [].toSet()
jvmdg.debugSkipStubs = [].toSet()
jvmdg.debugDumpClasses = false
jvmdg.shadePath = {
    it.substringBefore(".").substringBeforeLast("-").replace(Regex("[.;\\[/]"), "-")
}
jvmdg.shadeInlining = true
jvmdg.multiReleaseOriginal = false
jvmdg.multiReleaseVersions = [].toSet()

You can see more of what these values mean by checking the docs on the flags classes

This will create a default downgrade task for jar (or shadowJar if present) called downgradeJar that will downgrade the output to java 8 by default. as well as a shadeDowngradedApi to then insert the required classes for not having a runtime dependency on the api jar.

you can change the downgrade version by doing:

downgradeJar {
    downgradeTo = JavaVersion.VERSION_11
}

shadeDowngradedApi {
    downgradeTo = JavaVersion.VERSION_11
}

The tasks have all the same flags as the extension, so you can change them separately, their default value is to use the global one from the extension.

If you are merging multiple downgraded jars, please merge from the downgradeJar tasks, and then shade on the resulting mono-jar. otherwise some API stubs may be missing, due to how shade only includes what is used.

Optionally, you can also depend on the shadeDowngradedApi task when running build.

assemble.dependsOn shadeDowngradedApi

Custom Tasks

task customDowngrade(type: xyz.wagyourtail.jvmdg.gradle.task.DowngradeJar) {
    inputFile = tasks.jar.archiveFile
    downgradeTo = JavaVersion.VERSION_1_8 // default
    classpath = sourceSets.main.compileClasspath // default
    archiveClassifier = "downgraded-8"
}

task customShadeDowngradedApi(type: xyz.wagyourtail.jvmdg.gradle.task.ShadeJar) {
    inputFile = customDowngrade.archiveFile
    archiveClassifier = "downgraded-8-shaded"
}

Downgrade FileCollection

task downgradeFileCollection(type: xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles) {
    inputCollection = files("file1.jar", "file2.jar")
    downgradeTo = JavaVersion.VERSION_1_8 // default
    classpath = sourceSets.main.runtimeClasspath // default
}

// get the output with
downgradeFileCollection.outputCollection

task shadeFileCollection(type: xyz.wagyourtail.jvmdg.gradle.task.files.ShadeFiles) {
    inputCollection = downgradeFileCollection.outputCollection
    downgradeTo = JavaVersion.VERSION_1_8 // default
}

// get the output with
shadeFileCollection.outputCollection

Make sure the task is configured before trying to use the outputCollection, it's computed from the toDowngrade files.

Downgrade Configuration

To depend on more modern jars on a lower java version you can downgrade the configuration.

configurations {
    downgrade
    implementation.extendsFrom downgrade
}

jvmdg.dg(configurations.downgrade) {
    downgradeTo = JavaVersion.VERSION_1_8 // default
}

dependencies {
    downgrade "newer.java:version:1.0"
}

Downgrading for shading/jij'ing dependencies

This is the expected usage of these functions.

jvmdg.dg has optional parameter, shade: Boolean, turn this to false if you plan on shadow'ing this configuration and running shadeDowngradedApi, as otherwise there may be duplicate jvmdg-api classes. ie. jvmdg.dg(configurations.downgrade, false)

you may have to include whatever configuration you're downgrading in ShadowJar yourself. I do not automatically make the configuration shadowed into your output.

Including newer dependencies on lower java version building

It is strongly not recommended due to the following but is still possible. instead, you should just shade dependencies into your project and downgrade the combined output. The dependencies may not be correctly represented in the pom with this method.

There may be issues with metadata breaking because of how early gradle checks the java version, you can disable this by setting artifact in the metadataSources function on repositories to explicitly disable metadata, if you are lucky, you may be able to include mavenPom as well, so trasitive dependencies can resolve, otherwise transitive dependencies will not be included, and must be included manually.

for example:

repositories {
    mavenCentral {
        metadataSources {
            mavenPom() // may still break with this line on some dependencies
            artifact()
        }
    }
}

"Compile" Time Downgrading

Zip/Path Downgrading

Downgrades the contents of a zip file or path to an older version. you can specify multiple targets for bulk operations. (and they will include eachother in classpath searches during downgrading)

ex. java -jar JvmDowngrader-all.jar -c 52 downgrade -t input.jar output.jar -cp classpath.jar -cp classpath2.jar

Shading Api

Analyze a downgraded zip file for API usages and shade them into the output. you can specify multiple targets for bulk operations.

ex. java -jar JvmDowngrader-all.jar -c 52 shade -p "shade/prefix" -t input.jar output.jar

The class version can be replaced with a path to the pre-downgraded api jar to save time, using -d.

Runtime Downgrading

This is basically only here so I can take funny screenshots of minecraft running on java 8. I recommend the agent method, as it's most reliable.

Agent Downgrading

Uses the java agent to downgrade at runtime.

ex. java -javaagent:JvmDowngrader-all.jar -jar myapp.jar

Bootstrap Downgrading

Uses the bootstrap main class

ex. java -jar JvmDowngrader-all.jar bootstrap -cp myapp.jar;classpath.jar;classpath2.jar --main mainclass args

From Code

Downgrading ClassLoader

This is what the bootstrap downgrader essentially uses internally.

// add jar to default downgrading classloader
ClassDowngrader.getCurrentVersionDowngrader().getClassLoader().addDelegate(new URL[] { new File("jarname.jar").toURI().toURL() });

// call main method
ClassDowngrader.getCurrentVersionDowngrader().getClassLoader().loadClass("mainclass").getMethod("main", String[].class).invoke(null, new Object[] { new String[] { "args" } });

You can also create your own downgrading classloader, for more complicated environments.

DowngradingClassLoader loader = new DowngradingClassLoader(ClassDowngrader.getCurrentVersionDowngrader(), parent);

// adding jars
loader.addDelegate(new URL[]{new File("jarname.jar").toURI().toURL()});

inspired by

https://github.com/Chocohead/Not-So-New and https://github.com/luontola/retrolambda