Jar-in-Jar

Revision as of 21:53, 6 July 2022 by SciWhiz12 (talk | contribs) (SciWhiz12 moved page Jar-in-Jar to Jar-in-Jar: capitalization pls)

Jar-in-Jar is a way to handle the dependencies of your mod. Sometimes these are libraries pulled from a central maven repository, sometimes these are libraries specially designed for Minecraft and sometimes these are completely other mods. Whatever the reason for using a library during the development of your mods, you will need to ensure that the end user has them available when he runs your mod in his environment.

Although there are several options available to achieve this, including for example the Shading plugin, this does not work all the time and can even cause problems along the way when for example two mods need the same dependency. Introducing the all-new, all-shiny: Jar-In-Jar.

Central function

Jar-In-Jar is first and foremost a way to load dependencies for mods, from the jars of the mods. To achieve this it looks for a file called: META-INF/jarjar/metadata.json. If you are interested in the format of this file, see the following section of the JarJar library which we expose: JarJar Library - Metadata Source

In short, this metadata file lists a set of dependencies to include, what their maven coordinate is, the accepted version range that your mod supports, the version of the dependency that is included in your mods jar as well as a path to the jar in your mods jar file.

During the startup of the game, FML will first collect all mods and then collect all their dependencies to load. Jar-In-Jar hooks into this second phase and reads all the dependency files (recursively) and then determines what versions of the dependencies to load.

Dependency negotiation

Because different mods might need different versions of the same dependency (and have those included) Jar-In-Jar is first and foremost a negotiation system (hence you having to supply a version range your mod supports). From all dependencies that need to be loaded their supported version range is narrowed down to the agreeable range that all mods support. Then there are in principle three outcomes that can occur:

  1. No agreeable version range is found: Loading can not continue and the user will see an error message that mods require different dependency versions which are not compatible.
  2. An agreeable version range is found, but no jar was included in any of the dependencies which have a version that fits in the agreed range: Loading can not continue and the user will see an error message that the mods have agreed upon a supported range, but no file was found with the required version.
  3. An agreeable version range is found, and a matching jar could be located: That dependency will be loaded.

Dependency loading

Once negotiation ends, the selected jars are loaded into the game. The class loading layer is determined in two ways:

  1. The default way: If a mod, plugin, or language loader is detected then it is loaded in the appropriate layer and processed as such.
  2. The override way: If a library is supposed to be loaded which is not aware of Minecraft (for example JGraphT) then it will be loaded as a game library only. Meaning that your mod has access to it, but not plugins or language loaders. Those as such can only use libraries that are Minecraft aware or Shaded instead of Jar-In-Jarred into their respective jar.

Using ForgeGradle to generate a Jar-In-Jar

Jar-In-Jar is a completely optional system. To enable it, you need to call `jarJar.enable()` anywhere in your buildscript. By default, this will include all the dependencies in the `jarjar` configuration into the task output of the `jarJar` task. Note: The task jarJar is not accessible via the `jarJar` statement, since this references the project extension to manage Jar-In-Jar. If you need to modify the task use: `tasks.jarJar.configure { ... } `

Using the runtime dependencies

To include the runtime dependencies as well, you can invoke `jarJar.fromRuntimeConfiguration()` which will include the dependencies which are found at the runtime. If you use this, it is highly suggested to include a dependency filter, since else every single dependency, including Minecraft, forge, and their dependencies are included as well.

Using dependency filters

While you can filter the dependencies by including them in the `jarJar` configuration or not, this is not always as flexible as you need it to be. To achieve fine grain filtering the dependency configuration endpoint has been added to the `jarJar` extension as well as to the `jarJar` task. Using this endpoint you can configure dependency patterns (including using regular expressions) which you can include and exclude from the configuration:

Exclude gson example
dependencies {
    exclude(dependency('com.google.gson.*')) 
}

This example excludes any dependency which has `com.google.gson.` as a prefix of the group name of that artifact.

Include filters with runtime configuration usage

It is generally recommended to set at least one `include` filter when using the `fromRuntimeConfiguration` option.

Dependency version pinning

Since Jar-In-Jar is first and foremost a negotiation system it is required that you provide a way to supply it with a supported range, by default this is done via the version property of your dependency:

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

However, this might not produce the required result or even the required artifact you wish to include (by default the highest supported version is included based on Gradle dependency resolution rules). Using dependency version pinning you can specify which version of the dependency Jar-In-Jar should include when the jar is made. To achieve this configure the dependency and invoke: `jarJar.pin(<dependency instance>, "version_string")` as follows:

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

The example above will include version 2.8.0 of the GSON library into the Jar-In-Jar jar built by the `jarJar` task.

Dependency range pinning

Next to dependency version pinning Jar-In-Jar also supports pinning the version range which your mod supports, outside of the compile dependency range that you specify in the dependency statement:

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

As you can see this function is very useful when you combine it with runtime configuration-based dependency selection since it allows you to use a single statement to add the dependency to your project, as well as include it with a properly supported version range within your Jar-In-Jar jar.

Publishing a Jar-in-Jar jar to maven

Although in practice this is not really useful to do, for archival reasons FG supports publishing Jar-In-Jar artifacts to a maven of choice. The setup is similar to how the Shadow plugin handles this:

publications {
    mavenJava(MavenPublication) {
        from components.java
        jarJar.component(it)
 
        //Other statements related to configuring the POM go here
    }
}

Using this technique will add all Jar-In-Jar you build in the project to the publication at once and publish them with the other artifacts. If you want to only publish a specific Jar-In-Jar artifact to this publication use the following statement, which accepts a task instance to achieve this:

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