MinecraftForge / ForgeGradle

Minecraft mod development framework used by Forge and FML for the gradle build system
GNU Lesser General Public License v2.1
508 stars 436 forks source link

[Feature] Implement support for generating JarJar dependencies and cleaning of the generated POMs #863

Closed marchermans closed 2 years ago

marchermans commented 2 years ago

JarJar

JarJar Task

This new feature allows for the optional generation of JarJar compatible Jar in Jar shadow jars. The feature is optional and needs to be enabled with a call to jarJar.enable() anywhere during project evaluation. Or better said, any call to any of the methods in the jarJar extension (more on it later) which would configure the task, will enable it (for example by pinning a version, also more on that later). By default this adds a single JarJar task which gets enabled by the call to jarJar.enable(), afterwards the task then processes the registered configurations and generates the additional from(...) statements so the files are included into the resulting jar.

JarJar project extension

This PR adds a new extension known as jarJar which handles the configuration for all JarJar tasks (but a call to jarJar.enable() will only enable the default task named jarJar, as opposed to enabling all, however, all other methods in this class operate on all JarJar instances unless otherwise stated).

JarJar configuration

This PR adds a new configuration which is by default added to all JarJar tasks, named jarJar, and is the source of the dependencies which are embedded into the jar during build.

Filtering the dependencies

The JarJar task has a method: dependencies(Action<DependencyFilter> configurator) which can be used to filter the dependencies which get included into the jar, by setting included or excluded dependency specifications.

The configurations of the included dependencies can also be done on a project wide level by invoking the same method on the jarJar extension.

Example of excluding a specific dependency:
tasks.register('greeting', net.minecraftforge.gradle.userdev.tasks.JarJar) {
    dependencies {
        exclude(dependency('com.google.gson.gson:gson:2.9.0'))
    }
}
Example of excluding a group of dependencies:
tasks.register('greeting', net.minecraftforge.gradle.userdev.tasks.JarJar) {
    dependencies {
        exclude(dependency('com.google.gson.*')) //Will exclude all dependencies whoes group matches the regex.
    }
}
Exclusively including a dependency or a group of dependencies
tasks.register('greeting', net.minecraftforge.gradle.userdev.tasks.JarJar) {
    dependencies {
        include(dependency('com.google.gson.gson:gson:2.9.0'))
        include(dependency('com.google.gson.*')) //Will include all dependencies whoes group matches the regex.
    }
}

Dependency version pinning

Since JarJar is first and foremost a way for multiple mods to supply dependencies it is likely a common occurance that two mods provide the same dependency. To that effect JarJar does not support fixed version definitions for the dependencies a user tries to include. However, to allow for the user to force JarJar to include a particular version as a preference of the developer version pinning can be used. To pin a version of a dependency pass it and the requested version to jarJar.pin(...) as follows:

dependencies {
    implementation(group: 'com.google.code.gson', name: 'gson', version: '[2.0,3.0)') {
        jarJar.pin(it, "2.8.0")
    }
}

Dependency range setting

Since in many scenarios it is preferable to compile against a fixed version, but JarJar requires a version range to be given the plugin supports setting a range on a dependency which is then included in the generated metadata:

dependencies {
    implementation(group: 'com.google.code.gson', name: 'gson', version: '2.8.0') {
        jarJar.ranged(it, "[2.0,3.0)")
    }
}

This will tell JarJar to generate a metadata entry for this dependency which supports the 2.x version range of the dependency while your project hard compiles against the 2.8.0 version of the library.

Dependency version changing

If you want to compile against a fixed version in your project, but want to include a different version in the jar for minimal support, this can be achieved by combining the above two techniques:

dependencies {
    implementation(group: 'com.google.code.gson', name: 'gson', version: '2.9.0') {
        jarJar.ranged(it, "[2.0,3.0)")
        jarJar.pin(it, "2.8.0")
    }
}

In this configuration the project would compile against 2.9.0, while including a jar of version 2.8.0, and specifying that it supports the 2.x version range of the artifact.

Implementation details

Publishing a JarJar artifact to maven.

To publish the artifact and its cleaned dependencies it suffices to setup the following maven publication configuration:

    publications {
        mavenJava(MavenPublication) {
            from components.java
            jarJar.component(it)

            //Other statements related to configuring the POM go here
        }
    }

In case you have multiple JarJar tasks you can configure each of the publications to use a particular task as its source:

    publications {
        mavenJava(MavenPublication) {
            from components.java
            jarJar.component(it, tasks.jarJar) //From the jarJar task (note jarJar here alone does not suffice since that is an extension object)

            //Other statements related to configuring the POM go here
        }
        mavenJavaOther(MavenPublication) {
            from components.java
            jarJar.component(it, tasks.jarJarOther) //From the jarJarOther tasks

            //Other statements related to configuring the POM go here
        }
    }

This will add the required artifact from all JarJar tasks (or just from the one given) as well as configure the generated POM file to include the required dependencies.

Invoking jarJar.fromRuntimeConfiguration() (Either on the extension object, or on a JarJar task) will add the runtime configuration to the selected configurations to draw from. Note this requires the user to properly configure the filtering of the dependencies to include to prevent a massive mess from being generated, but this should at least provide a shortcut so that as minimal of a duplication of code in the build.gradle file is generated.

Processing of generated POMs.

Due to the need of handling additional dependencies during POM generation (which requires a cleaned up POM to properly work) FG gains with this PR the ability to clean the POMs generated from Maven artifacts. This cleaning ability can now also be used when the project does not use JarJar.

By invoking fg.component(it) when configuring a publication like so:

    publications {
        mavenJava(MavenPublication) {
            from components.java
            fg.component(it)

            //Other statements related to configuring the POM go here
        }
    }

The given publication will have the POM analyzed during POM generation and then have its dependency on Forge stripped as well as all obfuscated dependencies will be reverted to their original dependency notation (so without the _mapped_ suffix).

Example of minimal buildscript:

build.gradle ```groovy buildscript { repositories { maven { url = 'https://maven.minecraftforge.net' } mavenCentral() mavenLocal() } dependencies { classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '5.1.+', changing: true } } plugins { id 'eclipse' id 'maven-publish' } apply plugin: 'net.minecraftforge.gradle' version = '1.0' group = 'com.yourname.modid' // http://maven.apache.org/guides/mini/guide-naming-conventions.html archivesBaseName = 'modid' // Mojang ships Java 17 to end users in 1.18+, so your mod should target Java 17. java.toolchain.languageVersion = JavaLanguageVersion.of(17) println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}" minecraft { mappings channel: 'official', version: '1.18.2' runs { client { workingDirectory project.file('run') property 'forge.logging.markers', 'REGISTRIES' property 'forge.logging.console.level', 'debug' property 'forge.enabledGameTestNamespaces', 'examplemod' mods { examplemod { source sourceSets.main } } } server { workingDirectory project.file('run') property 'forge.logging.markers', 'REGISTRIES' property 'forge.logging.console.level', 'debug' property 'forge.enabledGameTestNamespaces', 'examplemod' mods { examplemod { source sourceSets.main } } } gameTestServer { workingDirectory project.file('run') property 'forge.logging.markers', 'REGISTRIES' property 'forge.logging.console.level', 'debug' property 'forge.enabledGameTestNamespaces', 'examplemod' mods { examplemod { source sourceSets.main } } } data { workingDirectory project.file('run') property 'forge.logging.markers', 'REGISTRIES' property 'forge.logging.console.level', 'debug' args '--mod', 'examplemod', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') mods { examplemod { source sourceSets.main } } } } } sourceSets.main.resources { srcDir 'src/generated/resources' } repositories { mavenCentral() } dependencies { minecraft 'net.minecraftforge:forge:1.18.2-40.1.48' implementation fg.deobf(group: 'com.google.code.gson', name: 'gson', version: '[2.0,3.0)') jarJar fg.deobf(group: 'com.google.code.gson', name: 'gson', version: '[2.0,3.0)') { jarJar.pin(it, "2.8.0") } } jar.finalizedBy('reobfJar') publishing { publications { mavenJava(MavenPublication) { from components.java jarJar.component(it) } mavenJavaSpecific(MavenPublication) { from components.java jarJar.component(it, tasks.jarJar) } mavenJavaCleaned(MavenPublication) { from components.java fg.component(it) } } repositories { maven { url "file://${project.projectDir}/mcmodsrepo" } } } tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation } jarJar{ enable() dependencies { include(dependency(group: 'com.google.code.gson', name: 'gson', version: '[2.0,3.0)')) exclude(dependency(group: 'com.google.code.gson', name: 'gson', version: '2.9.0')) } } ```
Lanse505 commented 2 years ago

Feedback was requested. Looked it over real quick and it looks good. However I will leave you with a rather nasty stare for naming it "JarJar" >_>