diff options
Diffstat (limited to 'docs/handbook/forgewrapper/building.md')
| -rw-r--r-- | docs/handbook/forgewrapper/building.md | 1843 |
1 files changed, 1843 insertions, 0 deletions
diff --git a/docs/handbook/forgewrapper/building.md b/docs/handbook/forgewrapper/building.md new file mode 100644 index 0000000000..84fa3bb1ec --- /dev/null +++ b/docs/handbook/forgewrapper/building.md @@ -0,0 +1,1843 @@ +# ForgeWrapper — Building & Gradle Build System Reference + +This document provides a comprehensive, line-by-line analysis of the ForgeWrapper +build system. Every statement references actual source code from the Gradle build +files. Variable names, values, and configuration blocks are taken directly from +the project. + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [Multi-Project Structure](#2-multi-project-structure) +3. [settings.gradle Analysis](#3-settingsgradle-analysis) +4. [gradle.properties Analysis](#4-gradleproperties-analysis) +5. [Gradle Wrapper Configuration](#5-gradle-wrapper-configuration) +6. [Root build.gradle — Complete Line-by-Line Analysis](#6-root-buildgradle--complete-line-by-line-analysis) + - 6.1 [Imports](#61-imports) + - 6.2 [Plugins Block](#62-plugins-block) + - 6.3 [Java Source/Target Compatibility](#63-java-sourcetarget-compatibility) + - 6.4 [Version, Group, and Archives Base Name](#64-version-group-and-archives-base-name) + - 6.5 [The multirelase Configuration](#65-the-multirelase-configuration) + - 6.6 [Repositories](#66-repositories) + - 6.7 [Dependencies](#67-dependencies) + - 6.8 [Sources JAR](#68-sources-jar) + - 6.9 [JAR Manifest Attributes](#69-jar-manifest-attributes) + - 6.10 [Multi-Release JAR Packing](#610-multi-release-jar-packing) + - 6.11 [Publishing Configuration](#611-publishing-configuration) + - 6.12 [getVersionSuffix()](#612-getversionsuffix) +7. [Jigsaw Subproject build.gradle — Complete Analysis](#7-jigsaw-subproject-buildgradle--complete-analysis) + - 7.1 [Plugins](#71-plugins) + - 7.2 [Java 9 Toolchain Auto-Detection](#72-java-9-toolchain-auto-detection) + - 7.3 [JVM Version Attribute Override](#73-jvm-version-attribute-override) +8. [Multi-Release JAR — Deep Dive](#8-multi-release-jar--deep-dive) +9. [Build Pipeline Diagram](#9-build-pipeline-diagram) +10. [Build Targets and Tasks](#10-build-targets-and-tasks) +11. [Step-by-Step Build Guide](#11-step-by-step-build-guide) +12. [CI/CD Integration](#12-cicd-integration) +13. [Artifact Output Structure](#13-artifact-output-structure) +14. [Publishing to Local Maven Repository](#14-publishing-to-local-maven-repository) +15. [Java Version Requirements](#15-java-version-requirements) +16. [Source File Layout and the Dual-ModuleUtil Pattern](#16-source-file-layout-and-the-dual-moduleutil-pattern) +17. [Troubleshooting](#17-troubleshooting) +18. [Quick Reference Card](#18-quick-reference-card) + +--- + +## 1. Project Overview + +ForgeWrapper is a Java library that allows third-party Minecraft launchers +(originally MultiMC, now adopted more broadly) to launch Minecraft 1.13+ with +Forge and NeoForge. The build system produces a **Multi-Release JAR** (MRJAR) +that contains: + +- **Java 8 bytecode** in the standard class path for maximum compatibility. +- **Java 9+ bytecode** under `META-INF/versions/9/` for environments running on + the Java Platform Module System (JPMS / Project Jigsaw). + +The Gradle build is a **multi-project build** consisting of: + +| Project | Directory | Java Target | Purpose | +|------------|-------------------|-------------|---------------------------------| +| Root | `forgewrapper/` | Java 8 | Main ForgeWrapper library | +| `jigsaw` | `forgewrapper/jigsaw/` | Java 9 | JPMS-aware `ModuleUtil` overlay | + +The root project's JAR task assembles both outputs into a single artifact with +`Multi-Release: true` in its manifest. + +--- + +## 2. Multi-Project Structure + +The ForgeWrapper build is organized as a Gradle multi-project build. The +directory tree relevant to the build system is: + +``` +forgewrapper/ +├── build.gradle ← Root project build script +├── settings.gradle ← Declares root name + subprojects +├── gradle.properties ← Project-wide properties +├── gradlew ← Gradle wrapper (Linux/macOS) +├── gradlew.bat ← Gradle wrapper (Windows) +├── gradle/ +│ └── wrapper/ +│ └── gradle-wrapper.properties ← Wrapper distribution URL + version +├── src/ +│ └── main/ +│ └── java/ +│ └── io/github/zekerzhayard/forgewrapper/installer/ +│ ├── Bootstrap.java +│ ├── Installer.java +│ ├── Main.java +│ ├── detector/ +│ │ ├── DetectorLoader.java +│ │ ├── IFileDetector.java +│ │ └── MultiMCFileDetector.java +│ └── util/ +│ └── ModuleUtil.java ← Java 8 stub (no-op methods) +├── jigsaw/ +│ ├── build.gradle ← Subproject build script +│ └── src/ +│ └── main/ +│ └── java/ +│ └── io/github/zekerzhayard/forgewrapper/installer/ +│ └── util/ +│ └── ModuleUtil.java ← Java 9 full implementation +└── build/ ← Generated outputs (after build) + ├── libs/ + │ ├── ForgeWrapper-<version>.jar + │ └── ForgeWrapper-<version>-sources.jar + └── maven/ ← Local maven publish target +``` + +The root project compiles all code under `src/` with Java 8. The `jigsaw` +subproject compiles its own `ModuleUtil.java` with Java 9. At JAR assembly time, +the jigsaw output is injected into `META-INF/versions/9/` inside the root JAR, +creating the Multi-Release JAR. + +--- + +## 3. settings.gradle Analysis + +**File: `forgewrapper/settings.gradle`** (2 lines of active content) + +```groovy +rootProject.name = 'ForgeWrapper' + +include 'jigsaw' +``` + +### Line-by-line: + +**`rootProject.name = 'ForgeWrapper'`** + +Sets the human-readable name of the root Gradle project to `ForgeWrapper`. This +name is used as: + +- The default `archivesBaseName` (overridden in `build.gradle` to + `rootProject.name`, which resolves to the same value `ForgeWrapper`). +- The `Specification-Title` and `Implementation-Title` in the JAR manifest. +- The base filename for produced artifacts: `ForgeWrapper-<version>.jar`. + +**`include 'jigsaw'`** + +Includes the subdirectory `jigsaw/` as a Gradle subproject. Gradle will look for +`jigsaw/build.gradle` and compile it as a separate project within the same build. +This subproject is referenced in the root `build.gradle` via +`project(":jigsaw")` in the `multirelase` dependency declaration. + +The relationship between settings.gradle and the two build.gradle files: + +``` +settings.gradle + │ + ├── rootProject.name = 'ForgeWrapper' + │ ↓ + │ build.gradle (root) + │ archivesBaseName = rootProject.name → "ForgeWrapper" + │ dependencies { multirelase project(":jigsaw") } + │ │ + └── include 'jigsaw' │ + ↓ ↓ + jigsaw/build.gradle ←─────── resolved here +``` + +--- + +## 4. gradle.properties Analysis + +**File: `forgewrapper/gradle.properties`** (2 active properties) + +```properties +org.gradle.daemon = false + +fw_version = projt +``` + +### `org.gradle.daemon = false` + +Disables the Gradle daemon for this project. Normally Gradle keeps a long-lived +JVM process (the daemon) running between builds to speed up subsequent +invocations. Setting this to `false` means every `./gradlew` invocation starts a +fresh JVM. This is typical for: + +- **CI/CD environments** where daemon state can cause flaky builds. +- **Projects with infrequent builds** where the daemon would just consume memory. +- **Reproducibility** — no cached daemon state between runs. + +### `fw_version = projt` + +Defines the base version string for ForgeWrapper as the literal value `projt`. +This property is referenced in `build.gradle` via string interpolation: + +```groovy +version = "${fw_version}${-> getVersionSuffix()}" +``` + +The `${fw_version}` is replaced with `projt` from `gradle.properties`, and then +the lazy GString closure `${-> getVersionSuffix()}` appends a suffix. The final +version string will be one of: + +| Environment | Resulting Version | +|--------------------------------------|-------------------------------| +| Local development | `projt-LOCAL` | +| GitHub Actions CI | `projt-2026-04-05` | +| IS_PUBLICATION env var set | `projt-2026-04-05` | + +The `fw_version` property is available to all projects in the multi-project +build because `gradle.properties` in the root directory is automatically loaded +for all subprojects. + +--- + +## 5. Gradle Wrapper Configuration + +**File: `forgewrapper/gradle/wrapper/gradle-wrapper.properties`** + +```properties +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +``` + +### Property-by-property: + +**`distributionBase=GRADLE_USER_HOME`** + +The base directory where the downloaded Gradle distribution will be stored. +`GRADLE_USER_HOME` defaults to `~/.gradle` on Linux/macOS and +`%USERPROFILE%\.gradle` on Windows. + +**`distributionPath=wrapper/dists`** + +Relative path under `distributionBase` where distributions are unpacked. The +full absolute path becomes `~/.gradle/wrapper/dists/`. + +**`distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip`** + +The URL from which Gradle **7.3.3** is downloaded. Key observations: + +- **Version 7.3.3** — Released December 2021. This is a Gradle 7.x release that + supports Java toolchains (used in the jigsaw subproject) and is compatible + with both Java 8 and Java 17+. +- **`-all` variant** — Includes Gradle source code and documentation, not just + the binaries (`-bin`). This allows IDEs to show Gradle DSL documentation and + enables source-level debugging of Gradle itself. +- The `\:` escape is standard `.properties` file syntax for a literal colon. + +**`zipStoreBase=GRADLE_USER_HOME`** and **`zipStorePath=wrapper/dists`** + +Where the downloaded ZIP file itself is cached before extraction. Same base +directory as the unpacked distribution. + +### Gradle Version Compatibility Matrix + +| Feature Used in Build | Minimum Gradle Version | Actual | +|--------------------------------|------------------------|--------| +| `java` plugin | 1.0 | 7.3.3 | +| `maven-publish` plugin | 1.3 | 7.3.3 | +| `javaToolchains.compilerFor` | 6.7 | 7.3.3 | +| `layout.buildDirectory.dir()` | 7.1 | 7.3.3 | +| `TargetJvmVersion` attribute | 6.0 | 7.3.3 | + +All features used in the build are supported by Gradle 7.3.3. + +--- + +## 6. Root build.gradle — Complete Line-by-Line Analysis + +**File: `forgewrapper/build.gradle`** (90 lines) + +This is the primary build script. We will examine every section. + +### 6.1 Imports + +```groovy +import java.text.SimpleDateFormat +``` + +Line 1 imports `SimpleDateFormat`, used in the `getVersionSuffix()` method at +line 89 to format the current date as `-yyyy-MM-dd` (e.g., `-2026-04-05`). +This import is at the top of the script, outside any block, making it available +to all closures in the file. + +### 6.2 Plugins Block + +```groovy +plugins { + id "java" + id "eclipse" + id "maven-publish" +} +``` + +Lines 3–7: Three plugins are applied. + +**`java`** — The core Java compilation plugin. It provides: +- `compileJava` task — compiles `src/main/java/**/*.java`. +- `jar` task — packages compiled classes into a JAR. +- `sourceSets` — the `main` and `test` source sets. +- `sourceCompatibility` / `targetCompatibility` — Java version settings. +- `configurations` — `implementation`, `compileOnly`, `runtimeOnly`, etc. +- The `components.java` software component used by `maven-publish`. + +**`eclipse`** — Generates Eclipse IDE project files (`.project`, `.classpath`, +`.settings/`). Allows developers to run `./gradlew eclipse` to import into +Eclipse. This does not affect the build output. + +**`maven-publish`** — Enables publishing artifacts to Maven repositories. It +provides: +- The `publishing { }` DSL block. +- The `publish` task (and per-repository `publishToMavenLocal`, etc.). +- `MavenPublication` type for defining what gets published. + +### 6.3 Java Source/Target Compatibility + +```groovy +sourceCompatibility = targetCompatibility = 1.8 +compileJava { + sourceCompatibility = targetCompatibility = 1.8 +} +``` + +Lines 9–12: Java 8 compatibility is set **twice** — at the project level and +inside the `compileJava` task configuration. This double declaration ensures the +setting is applied regardless of evaluation order. + +- **`sourceCompatibility = 1.8`** — The Java source code must be valid Java 8 + syntax. The compiler will reject Java 9+ language features (e.g., `var`, + private interface methods). +- **`targetCompatibility = 1.8`** — The emitted bytecode targets Java 8 (class + file version 52.0). JVMs older than Java 8 will refuse to load these classes. + +This is critical because ForgeWrapper must run on Minecraft launchers that may +still use Java 8. The `ModuleUtil.java` in the root source tree contains no-op +stubs precisely because Java 8 has no `java.lang.module` API: + +```java +// forgewrapper/src/main/java/.../util/ModuleUtil.java +public class ModuleUtil { + public static void addModules(String modulePath) { + // nothing to do with Java 8 + } + public static void addExports(List<String> exports) { + // nothing to do with Java 8 + } + public static void addOpens(List<String> opens) { + // nothing to do with Java 8 + } + // ... +} +``` + +### 6.4 Version, Group, and Archives Base Name + +```groovy +version = "${fw_version}${-> getVersionSuffix()}" +group = "io.github.zekerzhayard" +archivesBaseName = rootProject.name +``` + +Lines 14–16: Maven coordinates and artifact naming. + +**`version`** — A Groovy GString with a lazy evaluation closure. The `fw_version` +from `gradle.properties` is resolved immediately to `projt`. The closure +`${-> getVersionSuffix()}` is evaluated lazily — only when the GString is +converted to a String (at task execution time, not configuration time). This +matters because `getVersionSuffix()` calls `System.getenv()` and `new Date()`, +which should reflect the actual build time, not the configuration phase time. + +**`group = "io.github.zekerzhayard"`** — The Maven `groupId`. Follows the +reverse-domain convention for the project's GitHub namespace +(`github.com/ZekerZhayard`). + +**`archivesBaseName = rootProject.name`** — Set to `ForgeWrapper` (from +`settings.gradle`). This is the filename prefix for all produced JARs: +`ForgeWrapper-projt-LOCAL.jar`. + +### 6.5 The multirelase Configuration + +```groovy +configurations { + multirelase { + implementation.extendsFrom multirelase + } +} +``` + +Lines 18–22: This block creates a **custom Gradle configuration** named +`multirelase` (note: this is intentionally or accidentally spelled without the +second "e" — it is not `multirelease`). + +The statement `implementation.extendsFrom multirelase` establishes a dependency +inheritance chain: everything in the `multirelase` configuration is also visible +to the `implementation` configuration. This means: + +1. When `multirelase project(":jigsaw")` is declared (line 38), the jigsaw + subproject's classes are available on the root project's compile classpath. +2. The `multirelase` configuration is also used separately in the `jar` block + (line 60) to extract the jigsaw classes and pack them into + `META-INF/versions/9/`. + +The configuration flow: + +``` +multirelase config + │ + ├──→ implementation config (via extendsFrom) + │ → compileJava can see jigsaw classes + │ + └──→ jar task (via configurations.multirelase.files) + → copies jigsaw classes into META-INF/versions/9/ +``` + +This dual use is the heart of the Multi-Release JAR mechanism. The root source +code can reference `ModuleUtil` (which it does — in `Bootstrap.java` and +`Main.java`), and at compile time Gradle resolves the Java 8 stubs from the root +`src/`. At runtime, the JVM selects the appropriate `ModuleUtil` class: the Java +8 stub from the main class path on Java 8 JVMs, or the Java 9 implementation +from `META-INF/versions/9/` on Java 9+ JVMs. + +### 6.6 Repositories + +```groovy +repositories { + mavenCentral() + maven { + name = "forge" + url = "https://maven.minecraftforge.net/" + } +} +``` + +Lines 24–29: Two Maven repositories are declared. + +**`mavenCentral()`** — Maven Central Repository (`https://repo.maven.apache.org/maven2/`). +Used to resolve: +- `com.google.code.gson:gson:2.8.7` +- `net.sf.jopt-simple:jopt-simple:5.0.4` + +**Forge Maven** (`https://maven.minecraftforge.net/`) — MinecraftForge's official +Maven repository. Named `"forge"` for logging clarity. Used to resolve: +- `cpw.mods:modlauncher:8.0.9` +- `net.minecraftforge:installer:2.2.7` + +Note: The repository named `"forge"` is a Gradle naming convention for +readability; the name does not affect dependency resolution behavior. + +### 6.7 Dependencies + +```groovy +dependencies { + compileOnly "com.google.code.gson:gson:2.8.7" + compileOnly "cpw.mods:modlauncher:8.0.9" + compileOnly "net.minecraftforge:installer:2.2.7" + compileOnly "net.sf.jopt-simple:jopt-simple:5.0.4" + + multirelase project(":jigsaw") +} +``` + +Lines 31–38: All external dependencies use the `compileOnly` scope. + +#### Why `compileOnly`? + +`compileOnly` means the dependency is available at compile time but is **not** +included in the runtime classpath or the published POM. ForgeWrapper is loaded +into an environment (the Minecraft launcher) that already provides these +libraries. Bundling them would cause version conflicts and inflate the JAR. + +#### Dependency Analysis + +**`com.google.code.gson:gson:2.8.7`** + +- **What**: Google's JSON serialization/deserialization library. +- **Why compileOnly**: The Minecraft launcher and Forge installer both bundle + Gson. Including it again would create version conflicts. +- **Classes used**: `com.google.gson.Gson`, `com.google.gson.JsonObject`, etc. + Used in `Installer.java` to parse the Forge installer's JSON metadata. + +**`cpw.mods:modlauncher:8.0.9`** + +- **What**: The Forge ModLauncher framework by cpw (ChickenPatches Warrior). + Manages mod loading, class transformation, and game launch orchestration. +- **Why compileOnly**: ModLauncher is part of the Forge runtime. ForgeWrapper + interacts with its API to set up the launch environment but doesn't ship it. +- **Classes used**: Launch target interfaces and transformation services. +- **Version 8.0.9**: Targets Forge for Minecraft 1.16.x–1.17.x era. + +**`net.minecraftforge:installer:2.2.7`** + +- **What**: The Forge installer library. Contains code for downloading, + extracting, and setting up Forge libraries. +- **Why compileOnly**: ForgeWrapper loads the installer JAR at runtime via a + `URLClassLoader` (see `Main.java` lines 48–50). It does not bundle it. +- **Classes used**: Invoked reflectively — `installer.getMethod("getData", ...)` + and `installer.getMethod("install", ...)` in `Main.java`. + +**`net.sf.jopt-simple:jopt-simple:5.0.4`** + +- **What**: A command-line option parser for Java. +- **Why compileOnly**: Provided by the launcher environment (Forge uses it for + argument parsing). ForgeWrapper reads parsed arguments but doesn't need its + own copy. +- **Classes used**: `OptionParser`, `OptionSet` — for parsing launch arguments + like `--fml.mcVersion`, `--fml.forgeVersion`, `--fml.neoForgeVersion`. + +**`multirelase project(":jigsaw")`** + +- **What**: A project dependency on the `jigsaw` subproject. +- **Scope**: The custom `multirelase` configuration (not `compileOnly` or + `implementation` directly). Since `implementation.extendsFrom multirelase`, + the jigsaw classes are on the compile classpath. +- **Purpose**: The jigsaw project produces a JAR with Java 9 bytecode. This JAR + is consumed by the root project's `jar` task and unpacked into + `META-INF/versions/9/`. + +### 6.8 Sources JAR + +```groovy +java { + withSourcesJar() +} +``` + +Lines 40–42: Registers a `sourcesJar` task that produces +`ForgeWrapper-<version>-sources.jar` containing all `.java` files from +`src/main/java`. This artifact is included in Maven publications alongside the +main JAR, enabling downstream users and IDEs to access source code for debugging. + +### 6.9 JAR Manifest Attributes + +```groovy +jar { + manifest.attributes([ + "Specification-Title": "${project.name}", + "Specification-Vendor": "ZekerZhayard", + "Specification-Version": "${project.version}".split("-")[0], + "Implementation-Title": "${project.name}", + "Implementation-Version": "${project.version}", + "Implementation-Vendor" :"ZekerZhayard", + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + "Automatic-Module-Name": "${project.group}.${project.archivesBaseName}".toString().toLowerCase(), + "Multi-Release": "true", + "GitCommit": String.valueOf(System.getenv("GITHUB_SHA")) + ]) +``` + +Lines 44–56: The `jar` task configures `META-INF/MANIFEST.MF` with these +attributes. Each one explained: + +**`Specification-Title: ForgeWrapper`** + +Identifies the specification this JAR implements. Set to `project.name` which +resolves to `ForgeWrapper`. Part of the JAR Specification versioning convention +defined in `java.lang.Package`. + +**`Specification-Vendor: ZekerZhayard`** + +The organization or individual that maintains the specification. Hardcoded to +the project author's GitHub handle. + +**`Specification-Version: projt`** + +The spec version. Note the expression `"${project.version}".split("-")[0]` — +this takes the full version string (e.g., `projt-LOCAL` or `projt-2026-04-05`) +and splits on `-`, taking only the first element: `projt`. This gives a stable +specification version irrespective of the build suffix. + +**`Implementation-Title: ForgeWrapper`** + +Same as `Specification-Title`. Identifies this particular implementation. + +**`Implementation-Version: projt-LOCAL`** (or `projt-2026-04-05`) + +The full version string including the suffix. Unlike `Specification-Version`, +this captures the exact build variant. + +**`Implementation-Vendor: ZekerZhayard`** + +The vendor of this implementation. Same as `Specification-Vendor`. + +**`Implementation-Timestamp: 2026-04-05T14:30:00+0000`** (example) + +The build timestamp in ISO 8601 format. Generated by `new Date().format(...)`. +The format pattern `yyyy-MM-dd'T'HH:mm:ssZ` produces: +- `yyyy` — 4-digit year +- `MM` — 2-digit month +- `dd` — 2-digit day +- `'T'` — literal T separator +- `HH:mm:ss` — 24-hour time +- `Z` — timezone offset (e.g., `+0000`) + +**`Automatic-Module-Name: io.github.zekerzhayard.forgewrapper`** + +The JPMS module name for this JAR when it is placed on the module path. The +expression `"${project.group}.${project.archivesBaseName}".toString().toLowerCase()` +concatenates `io.github.zekerzhayard` + `.` + `ForgeWrapper`, converts to +lowercase: `io.github.zekerzhayard.forgewrapper`. This allows other JPMS modules +to `requires io.github.zekerzhayard.forgewrapper;`. + +**`Multi-Release: true`** + +The critical attribute. Tells the JVM (Java 9+) that this JAR contains +version-specific class files under `META-INF/versions/<N>/`. Without this +attribute, the JVM ignores the `META-INF/versions/` directory entirely. Defined +in JEP 238 (Multi-Release JAR Files). + +**`GitCommit: <sha>`** (or `null`) + +Captures the Git commit SHA from the `GITHUB_SHA` environment variable. +`String.valueOf(System.getenv("GITHUB_SHA"))` returns: +- The 40-character SHA hex string when built in GitHub Actions. +- The string `"null"` when built locally (since `getenv()` returns Java `null`, + and `String.valueOf(null)` returns the string `"null"`). + +This attribute enables tracing a built artifact back to its exact source commit. + +### 6.10 Multi-Release JAR Packing + +```groovy + into "META-INF/versions/9", { + from configurations.multirelase.files.collect { + zipTree(it) + } + exclude "META-INF/**" + } +} +``` + +Lines 58–63: This block within the `jar { }` closure is the mechanism that +creates the Multi-Release JAR. Detailed breakdown: + +**`into "META-INF/versions/9"`** — All files produced by this `from` block are +placed inside the `META-INF/versions/9/` directory within the JAR. This is the +Java 9 version overlay directory per JEP 238. + +**`configurations.multirelase.files`** — Resolves the `multirelase` configuration, +which contains `project(":jigsaw")`. This evaluates to the jigsaw subproject's +JAR file (e.g., `jigsaw/build/libs/jigsaw.jar`). + +**`.collect { zipTree(it) }`** — For each file in the resolved configuration +(there's exactly one: the jigsaw JAR), `zipTree()` treats it as a ZIP archive +and returns a file tree of its contents. This effectively "explodes" the jigsaw +JAR. + +**`exclude "META-INF/**"`** — Excludes the jigsaw JAR's own `META-INF/` +directory (which contains its own `MANIFEST.MF`). Only actual class files are +copied. This prevents nested/conflicting manifests. + +The net result: the jigsaw subproject's compiled class files (specifically +`io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class`) are +placed at: + +``` +META-INF/versions/9/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class +``` + +When the JVM is Java 9+, it will load this class **instead of** the Java 8 stub +at: + +``` +io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class +``` + +### 6.11 Publishing Configuration + +```groovy +publishing { + publications { + maven(MavenPublication) { + groupId "${project.group}" + artifactId "${project.archivesBaseName}" + version "${project.version}" + + from components.java + } + } + repositories { + maven { + url = layout.buildDirectory.dir("maven") + } + } +} +tasks.publish.dependsOn build +``` + +Lines 65–82: Maven publishing setup. + +**Publication: `maven(MavenPublication)`** + +Defines a Maven publication with coordinates: +- `groupId` = `io.github.zekerzhayard` +- `artifactId` = `ForgeWrapper` +- `version` = `projt-LOCAL` (or dated variant) + +**`from components.java`** — Publishes the Java component, which includes: +- The main JAR (`ForgeWrapper-<version>.jar`) +- The sources JAR (`ForgeWrapper-<version>-sources.jar`) +- A generated POM file with dependency metadata + +**Repository target:** + +```groovy +url = layout.buildDirectory.dir("maven") +``` + +Publishes to a local directory inside the build output: +`forgewrapper/build/maven/`. This is **not** Maven Central or any remote +repository. The published artifacts end up at: + +``` +build/maven/io/github/zekerzhayard/ForgeWrapper/<version>/ +├── ForgeWrapper-<version>.jar +├── ForgeWrapper-<version>-sources.jar +├── ForgeWrapper-<version>.pom +├── ForgeWrapper-<version>.module +└── (checksums: .md5, .sha1, .sha256, .sha512) +``` + +**`tasks.publish.dependsOn build`** — Ensures the project is fully built before +publishing. Without this, Gradle could attempt to publish before the JAR is +assembled. + +### 6.12 getVersionSuffix() + +```groovy +static String getVersionSuffix() { + if (System.getenv("IS_PUBLICATION") != null || System.getenv("GITHUB_ACTIONS") == "true") + return new SimpleDateFormat("-yyyy-MM-dd").format(new Date()) + + return "-LOCAL" +} +``` + +Lines 84–89: A static method that determines the version suffix. + +**Logic:** + +1. If the environment variable `IS_PUBLICATION` is set (to any value, including + empty string — `!= null` is the check), append a date suffix. +2. **OR** if the environment variable `GITHUB_ACTIONS` equals the string + `"true"` (which GitHub Actions always sets), append a date suffix. +3. Otherwise, append `-LOCAL`. + +**Important Groovy/Java note:** The `==` comparison with `System.getenv()` uses +Java `String.equals()` semantics in Groovy when comparing strings. However, +there is a subtle behavior: `System.getenv("GITHUB_ACTIONS")` returns `null` +when not in GitHub Actions, and `null == "true"` evaluates to `false` in Groovy +(no NPE), which is the desired behavior. + +**Date format:** `SimpleDateFormat("-yyyy-MM-dd")` produces strings like +`-2026-04-05`. The leading `-` is part of the pattern, so the result +includes the hyphen separator. + +**Version assembly flow:** + +``` +gradle.properties getVersionSuffix() Final version +───────────────── ────────────────── ───────────── +fw_version = projt + "-LOCAL" → "projt-LOCAL" +fw_version = projt + "-2026-04-05" → "projt-2026-04-05" +``` + +--- + +## 7. Jigsaw Subproject build.gradle — Complete Analysis + +**File: `forgewrapper/jigsaw/build.gradle`** (27 lines) + +This subproject compiles the Java 9+ version of `ModuleUtil.java`. + +### 7.1 Plugins + +```groovy +plugins { + id "java" + id "eclipse" +} +``` + +Lines 1–4: Only `java` and `eclipse`. No `maven-publish` — the jigsaw subproject +is never published independently. Its output is consumed exclusively by the root +project's `jar` task via the `multirelase` configuration. + +### 7.2 Java 9 Toolchain Auto-Detection + +```groovy +compileJava { + if (JavaVersion.current() < JavaVersion.VERSION_1_9) { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(9) + } + } + sourceCompatibility = 9 + targetCompatibility = 9 +} +``` + +Lines 6–14: The compilation block with intelligent Java version detection. + +**The `if` check:** `JavaVersion.current()` returns the JVM version running +Gradle itself. If Gradle is running on Java 8 (which is `< VERSION_1_9`), then +the `javaCompiler` is overridden using Gradle's **Java Toolchain** feature. + +**`javaToolchains.compilerFor { languageVersion = JavaLanguageVersion.of(9) }`** + +This tells Gradle: "Find a Java 9 compiler on this system and use it for this +compilation task." Gradle will search: +1. JDK installations registered in the toolchain registry. +2. Standard JDK installation directories (`/usr/lib/jvm/`, etc.). +3. JDKs managed by tools like SDKMAN!, jabba, or Gradle's auto-provisioning. + +If no Java 9+ JDK is found, the build fails with an error. + +**If Gradle is already running on Java 9+**, the `if` block is skipped entirely. +The current JVM's compiler is used, with `sourceCompatibility = 9` and +`targetCompatibility = 9` ensuring Java 9 bytecode is produced. + +**`sourceCompatibility = 9`** — Accept Java 9 language features (modules, +private interface methods, etc.). The jigsaw `ModuleUtil.java` uses: +- `java.lang.module.Configuration` +- `java.lang.module.ModuleFinder` +- `java.lang.module.ModuleReference` +- `java.lang.module.ResolvedModule` +- `ModuleLayer.boot()` +- `List.of()` +- `ClassLoader.getPlatformClassLoader()` + +These APIs do not exist in Java 8, which is why this code cannot be in the root +project. + +**`targetCompatibility = 9`** — Emit class file version 53.0 (Java 9). The JVM +only loads classes from `META-INF/versions/9/` if they are version 53.0 or +higher. + +### 7.3 JVM Version Attribute Override + +```groovy +configurations { + apiElements { + attributes { + attribute TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8 + } + } + runtimeElements { + attributes { + attribute TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8 + } + } +} +``` + +Lines 16–27: This is a subtle but critical configuration. + +**Problem:** The jigsaw subproject compiles with `targetCompatibility = 9`, so +Gradle automatically sets the `TargetJvmVersion` attribute on its +`apiElements` and `runtimeElements` configurations to `9`. When the root project +(which targets Java 8) depends on `project(":jigsaw")`, Gradle's dependency +resolution would detect a JVM version mismatch and fail: + +``` +> Could not resolve project :jigsaw. + > Incompatible because this component declares a component compatible + with Java 9 and the consumer needed a component compatible with Java 8 +``` + +**Solution:** Override the `TARGET_JVM_VERSION_ATTRIBUTE` to `8` on both +outgoing configurations (`apiElements` and `runtimeElements`). This tells +Gradle: "Even though this project compiles with Java 9, treat its output as +Java 8-compatible for dependency resolution purposes." + +This is safe because: +1. The jigsaw JAR is **never loaded directly** on Java 8. It is packed into + `META-INF/versions/9/` and only loaded by Java 9+ JVMs. +2. The attribute override only affects Gradle's dependency resolution metadata, + not the actual bytecode version. + +**`apiElements`** — The configuration used when another project declares a +`compileOnly` or `api` dependency on this project. + +**`runtimeElements`** — The configuration used when another project declares an +`implementation` or `runtimeOnly` dependency on this project. + +Both must be overridden because the root project's `multirelase` configuration +(which extends `implementation`) resolves through `runtimeElements`. + +--- + +## 8. Multi-Release JAR — Deep Dive + +The Multi-Release JAR (MRJAR) is the defining feature of ForgeWrapper's build +system. Here is the complete picture. + +### What is a Multi-Release JAR? + +Defined by [JEP 238](https://openjdk.org/jeps/238) and standardized in Java 9, +a Multi-Release JAR allows a single JAR file to contain multiple versions of +the same class, each compiled for a different Java version. The JVM +automatically selects the appropriate version at runtime based on the JVM +version. + +### JAR Internal Structure + +``` +ForgeWrapper-projt-LOCAL.jar +│ +├── META-INF/ +│ ├── MANIFEST.MF +│ │ ├── Multi-Release: true ← Activates MRJAR behavior +│ │ ├── Specification-Title: ForgeWrapper +│ │ ├── Specification-Vendor: ZekerZhayard +│ │ ├── Specification-Version: projt +│ │ ├── Implementation-Title: ForgeWrapper +│ │ ├── Implementation-Version: projt-LOCAL +│ │ ├── Implementation-Vendor: ZekerZhayard +│ │ ├── Implementation-Timestamp: 2026-04-05T... +│ │ ├── Automatic-Module-Name: io.github.zekerzhayard.forgewrapper +│ │ └── GitCommit: null +│ │ +│ └── versions/ +│ └── 9/ +│ └── io/github/zekerzhayard/forgewrapper/installer/util/ +│ └── ModuleUtil.class ← Java 9 bytecode (version 53.0) +│ +├── io/github/zekerzhayard/forgewrapper/installer/ +│ ├── Bootstrap.class ← Java 8 bytecode (version 52.0) +│ ├── Installer.class +│ ├── Main.class +│ ├── detector/ +│ │ ├── DetectorLoader.class +│ │ ├── IFileDetector.class +│ │ └── MultiMCFileDetector.class +│ └── util/ +│ └── ModuleUtil.class ← Java 8 bytecode (no-op stubs) +``` + +### Runtime Class Selection + +``` +JVM Version Class Loaded for ModuleUtil +─────────── ────────────────────────────────────────────────── +Java 8 io/github/.../util/ModuleUtil.class (root, no-ops) +Java 9 META-INF/versions/9/io/github/.../util/ModuleUtil.class +Java 10 META-INF/versions/9/io/github/.../util/ModuleUtil.class +Java 11 META-INF/versions/9/io/github/.../util/ModuleUtil.class + ... (same — highest ≤ JVM version wins) +Java 21 META-INF/versions/9/io/github/.../util/ModuleUtil.class +``` + +The JVM always selects the highest versioned class that is ≤ the running JVM +version. Since ForgeWrapper only has a version 9 overlay, all Java 9+ JVMs +use the same Java 9 `ModuleUtil`. + +### The Dual ModuleUtil Classes + +**Root ModuleUtil** (`src/main/java/.../util/ModuleUtil.java`) — 42 lines: + +```java +public class ModuleUtil { + public static void addModules(String modulePath) { + // nothing to do with Java 8 + } + public static void addExports(List<String> exports) { + // nothing to do with Java 8 + } + public static void addOpens(List<String> opens) { + // nothing to do with Java 8 + } + public static void setupClassPath(Path libraryDir, List<String> paths) + throws Throwable { + // Uses URLClassLoader.addURL() via reflection + } + public static Class<?> setupBootstrapLauncher(Class<?> mainClass) { + // nothing to do with Java 8 + return mainClass; + } + public static ClassLoader getPlatformClassLoader() { + // PlatformClassLoader does not exist in Java 8 + return null; + } +} +``` + +**Jigsaw ModuleUtil** (`jigsaw/src/main/java/.../util/ModuleUtil.java`) — 150+ lines: + +The Java 9 version provides actual JPMS integration: +- `addModules()` — Dynamically adds module paths to the boot module layer at + runtime using `sun.misc.Unsafe` to access `MethodHandles.Lookup.IMPL_LOOKUP` + and reflectively manipulate the module graph. +- `addExports()` / `addOpens()` — Adds `--add-exports` / `--add-opens` at + runtime via `Module.implAddExports()` and `Module.implAddOpens()`. +- `setupClassPath()` — Uses the module system's `ModuleLayer` to properly add + libraries. +- `setupBootstrapLauncher()` — Sets up the BootstrapLauncher class introduced + in newer Forge versions. +- `getPlatformClassLoader()` — Returns `ClassLoader.getPlatformClassLoader()` + (Java 9+ API, does not exist in Java 8). + +--- + +## 9. Build Pipeline Diagram + +### Complete Build Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ./gradlew build │ +└──────────────────────────────┬──────────────────────────────────┘ + │ + ┌────────────────┴────────────────┐ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────┐ +│ :compileJava │ │ :jigsaw:compileJava │ +│ (Java 8, v52.0) │ │ (Java 9, v53.0) │ +│ │ │ │ +│ Source: │ │ Source: │ +│ src/main/java/ │ │ jigsaw/src/main/ │ +│ └─ ...ModuleUtil │ │ └─ ...ModuleUtil │ +│ (no-op stubs) │ │ (full JPMS impl)│ +└──────────┬───────────┘ └──────────┬───────────┘ + │ │ + ▼ ▼ +┌──────────────────────┐ ┌──────────────────────┐ +│ :processResources │ │ :jigsaw:jar │ +└──────────┬───────────┘ │ → jigsaw.jar │ + │ └──────────┬───────────┘ + │ │ + │ ┌───────────────────────────┘ + │ │ configurations.multirelase + │ │ resolves to jigsaw.jar + ▼ ▼ +┌──────────────────────────────────────────────┐ +│ :jar │ +│ │ +│ 1. Pack root classes → / │ +│ 2. zipTree(jigsaw.jar) │ +│ exclude META-INF/** │ +│ → META-INF/versions/9/ │ +│ 3. Generate MANIFEST.MF with attributes │ +│ including Multi-Release: true │ +│ │ +│ Output: build/libs/ForgeWrapper-<ver>.jar │ +└──────────────────────┬───────────────────────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────┐ ┌──────────────┐ +│ :sourcesJar │ │ :check │ │ :assemble │ +│ (sources) │ │ (tests) │ │ (lifecycle) │ +└──────┬───────┘ └────┬─────┘ └──────┬───────┘ + │ │ │ + └──────────────┼──────────────┘ + ▼ + ┌──────────────────┐ + │ :build │ + └──────────────────┘ +``` + +### Publishing Flow + +``` +┌──────────────┐ +│ :build │ +└──────┬───────┘ + │ tasks.publish.dependsOn build + ▼ +┌──────────────────────────────────────────────────────┐ +│ :publish │ +│ │ +│ Publication: maven(MavenPublication) │ +│ groupId: io.github.zekerzhayard │ +│ artifactId: ForgeWrapper │ +│ version: projt-LOCAL (or projt-YYYY-MM-DD) │ +│ │ +│ from components.java: │ +│ ├── ForgeWrapper-<ver>.jar │ +│ └── ForgeWrapper-<ver>-sources.jar │ +│ │ +│ Target: build/maven/ │ +│ └── io/github/zekerzhayard/ForgeWrapper/<ver>/ │ +│ ├── ForgeWrapper-<ver>.jar │ +│ ├── ForgeWrapper-<ver>-sources.jar │ +│ ├── ForgeWrapper-<ver>.pom │ +│ └── ForgeWrapper-<ver>.module │ +└──────────────────────────────────────────────────────┘ +``` + +### Version Resolution Flow + +``` +gradle.properties build.gradle +┌──────────────────┐ ┌──────────────────────────────────┐ +│ fw_version=projt │─────▶│ version="${fw_version}${->...}" │ +└──────────────────┘ │ │ │ + │ ▼ │ + │ getVersionSuffix() │ + │ │ │ + │ ├─ IS_PUBLICATION!=null? │ + │ │ └─ YES → "-YYYY-MM-DD" │ + │ │ │ + │ ├─ GITHUB_ACTIONS=="true"? │ + │ │ └─ YES → "-YYYY-MM-DD" │ + │ │ │ + │ └─ else → "-LOCAL" │ + │ │ + │ Result: "projt-LOCAL" │ + │ or: "projt-2026-04-05" │ + └──────────────────────────────────┘ +``` + +--- + +## 10. Build Targets and Tasks + +### Key Tasks + +| Task | Type | Description | +|-----------------------------|-------------------|------------------------------------------------| +| `:compileJava` | JavaCompile | Compiles root `src/main/java` with Java 8 | +| `:processResources` | Copy | Copies `src/main/resources` to build dir | +| `:classes` | Lifecycle | Depends on compileJava + processResources | +| `:jar` | Jar | Assembles the Multi-Release JAR | +| `:sourcesJar` | Jar | Assembles sources JAR | +| `:assemble` | Lifecycle | Depends on jar + sourcesJar | +| `:check` | Lifecycle | Runs tests (none configured) | +| `:build` | Lifecycle | Depends on assemble + check | +| `:publish` | Lifecycle | Publishes to build/maven/ (depends on build) | +| `:jigsaw:compileJava` | JavaCompile | Compiles jigsaw `src/main/java` with Java 9 | +| `:jigsaw:jar` | Jar | Packages jigsaw classes into jigsaw.jar | +| `:eclipse` | Lifecycle | Generates Eclipse project files | +| `:jigsaw:eclipse` | Lifecycle | Generates Eclipse files for jigsaw subproject | +| `:clean` | Delete | Removes `build/` directory | +| `:jigsaw:clean` | Delete | Removes `jigsaw/build/` directory | + +### Task Dependency Chain + +Running `./gradlew build` triggers this chain: + +``` +:build +├── :assemble +│ ├── :jar +│ │ ├── :classes +│ │ │ ├── :compileJava +│ │ │ └── :processResources +│ │ └── [multirelase config resolution] +│ │ └── :jigsaw:jar +│ │ └── :jigsaw:classes +│ │ ├── :jigsaw:compileJava +│ │ └── :jigsaw:processResources +│ └── :sourcesJar +└── :check + └── :test (no tests configured → no-op) +``` + +### What Each Task Produces + +| Task | Output | +|-------------------------|-----------------------------------------------------| +| `:compileJava` | `build/classes/java/main/**/*.class` | +| `:jigsaw:compileJava` | `jigsaw/build/classes/java/main/**/*.class` | +| `:jigsaw:jar` | `jigsaw/build/libs/jigsaw.jar` | +| `:jar` | `build/libs/ForgeWrapper-<ver>.jar` | +| `:sourcesJar` | `build/libs/ForgeWrapper-<ver>-sources.jar` | +| `:publish` | `build/maven/io/github/zekerzhayard/ForgeWrapper/` | + +--- + +## 11. Step-by-Step Build Guide + +### Prerequisites + +1. **Java 8 or higher** — To run Gradle itself. Gradle 7.3.3 supports Java + 8 through Java 17. +2. **Java 9 or higher JDK** — Required to compile the jigsaw subproject. If + Gradle runs on Java 8, the toolchain must find a Java 9+ JDK. If Gradle + runs on Java 9+, no additional JDK is needed. +3. **Internet access** — To download the Gradle wrapper distribution and + Maven dependencies (first build only; cached after that). + +### Clone and Build + +```bash +# Navigate to the forgewrapper directory +cd forgewrapper/ + +# Make the wrapper executable (Linux/macOS) +chmod +x gradlew + +# Build the project +./gradlew build +``` + +On Windows: +```cmd +cd forgewrapper\ +gradlew.bat build +``` + +### Build Output + +After a successful build: + +``` +build/libs/ +├── ForgeWrapper-projt-LOCAL.jar ← Main MRJAR artifact +└── ForgeWrapper-projt-LOCAL-sources.jar ← Source code archive +``` + +### Build + Publish + +```bash +./gradlew publish +``` + +This runs `build` first (due to `tasks.publish.dependsOn build`), then publishes: + +``` +build/maven/ +└── io/ + └── github/ + └── zekerzhayard/ + └── ForgeWrapper/ + └── projt-LOCAL/ + ├── ForgeWrapper-projt-LOCAL.jar + ├── ForgeWrapper-projt-LOCAL-sources.jar + ├── ForgeWrapper-projt-LOCAL.pom + └── ForgeWrapper-projt-LOCAL.module +``` + +### Clean Build + +```bash +./gradlew clean build +``` + +Removes all generated files in `build/` and `jigsaw/build/` before rebuilding. + +### Individual Tasks + +```bash +# Compile only the root project +./gradlew compileJava + +# Compile only the jigsaw subproject +./gradlew :jigsaw:compileJava + +# Build only the JAR (no tests, no publish) +./gradlew jar + +# Generate Eclipse project files +./gradlew eclipse + +# List all available tasks +./gradlew tasks --all + +# Show the dependency tree +./gradlew dependencies +``` + +### Simulating CI Build Locally + +```bash +# Set environment variables to simulate GitHub Actions +GITHUB_ACTIONS=true GITHUB_SHA=abc123def456 ./gradlew build + +# Or force a publication-style version +IS_PUBLICATION=1 ./gradlew build +``` + +With either of these, the version suffix changes from `-LOCAL` to the current +date (e.g., `-2026-04-05`), and `GitCommit` in the manifest is set to the +provided SHA. + +### Verbose/Debug Build + +```bash +# Show task execution details +./gradlew build --info + +# Full debug logging +./gradlew build --debug + +# Show dependency resolution details +./gradlew build --scan +``` + +--- + +## 12. CI/CD Integration + +The build system detects CI environments via environment variables. + +### GitHub Actions Detection + +Two environment variables are checked in `getVersionSuffix()` (line 86 of +`build.gradle`): + +**`GITHUB_ACTIONS`** — Automatically set to `"true"` by GitHub Actions runners. +When detected, the version suffix switches from `-LOCAL` to a date stamp. + +**`GITHUB_SHA`** — The full SHA of the commit that triggered the workflow. This +is embedded in the JAR manifest as the `GitCommit` attribute (line 55 of +`build.gradle`): + +```groovy +"GitCommit": String.valueOf(System.getenv("GITHUB_SHA")) +``` + +### IS_PUBLICATION + +**`IS_PUBLICATION`** — A custom environment variable. When set (to any non-null +value), it forces date-stamped versioning regardless of whether the build runs +in GitHub Actions. This allows publication builds from other CI systems or +local environments: + +```bash +IS_PUBLICATION=yes ./gradlew publish +``` + +### CI Build vs Local Build Comparison + +``` +┌───────────────────────┬─────────────────────┬──────────────────────┐ +│ Aspect │ Local Build │ CI Build │ +├───────────────────────┼─────────────────────┼──────────────────────┤ +│ Version suffix │ -LOCAL │ -2026-04-05 │ +│ GitCommit manifest │ null │ abc123def456... │ +│ Gradle daemon │ disabled (property) │ disabled (property) │ +│ Full version example │ projt-LOCAL │ projt-2026-04-05 │ +│ Spec-Version │ projt │ projt │ +│ Impl-Version │ projt-LOCAL │ projt-2026-04-05 │ +└───────────────────────┴─────────────────────┴──────────────────────┘ +``` + +### Environment Variable Summary + +| Variable | Set By | Used In | Effect | +|------------------|------------------|---------------------------|--------------------------------| +| `GITHUB_ACTIONS` | GitHub Actions | `getVersionSuffix()` | Enables date-stamped version | +| `GITHUB_SHA` | GitHub Actions | `jar.manifest.attributes` | Records commit SHA in manifest | +| `IS_PUBLICATION` | Manual / CI | `getVersionSuffix()` | Forces date-stamped version | + +--- + +## 13. Artifact Output Structure + +### Primary Artifact: ForgeWrapper JAR + +**File:** `build/libs/ForgeWrapper-<version>.jar` + +Inspecting the JAR contents (example): + +```bash +jar tf build/libs/ForgeWrapper-projt-LOCAL.jar +``` + +Expected output: + +``` +META-INF/ +META-INF/MANIFEST.MF +io/ +io/github/ +io/github/zekerzhayard/ +io/github/zekerzhayard/forgewrapper/ +io/github/zekerzhayard/forgewrapper/installer/ +io/github/zekerzhayard/forgewrapper/installer/Bootstrap.class +io/github/zekerzhayard/forgewrapper/installer/Installer.class +io/github/zekerzhayard/forgewrapper/installer/Main.class +io/github/zekerzhayard/forgewrapper/installer/detector/ +io/github/zekerzhayard/forgewrapper/installer/detector/DetectorLoader.class +io/github/zekerzhayard/forgewrapper/installer/detector/IFileDetector.class +io/github/zekerzhayard/forgewrapper/installer/detector/MultiMCFileDetector.class +io/github/zekerzhayard/forgewrapper/installer/util/ +io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class +META-INF/versions/ +META-INF/versions/9/ +META-INF/versions/9/io/ +META-INF/versions/9/io/github/ +META-INF/versions/9/io/github/zekerzhayard/ +META-INF/versions/9/io/github/zekerzhayard/forgewrapper/ +META-INF/versions/9/io/github/zekerzhayard/forgewrapper/installer/ +META-INF/versions/9/io/github/zekerzhayard/forgewrapper/installer/util/ +META-INF/versions/9/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class +``` + +### Sources Artifact + +**File:** `build/libs/ForgeWrapper-<version>-sources.jar` + +Contains `.java` source files from the root project's `src/main/java/` tree. + +### Published Maven Artifacts + +**Directory:** `build/maven/io/github/zekerzhayard/ForgeWrapper/<version>/` + +| File | Description | +|------------------------------------------|------------------------------------| +| `ForgeWrapper-<ver>.jar` | The MRJAR binary | +| `ForgeWrapper-<ver>-sources.jar` | Source code archive | +| `ForgeWrapper-<ver>.pom` | Maven POM with dependency metadata | +| `ForgeWrapper-<ver>.module` | Gradle Module Metadata (GMM) | +| `*.md5`, `*.sha1`, `*.sha256`, `*.sha512`| Integrity checksums | + +### Manifest File Content + +The `META-INF/MANIFEST.MF` in the produced JAR: + +``` +Manifest-Version: 1.0 +Specification-Title: ForgeWrapper +Specification-Vendor: ZekerZhayard +Specification-Version: projt +Implementation-Title: ForgeWrapper +Implementation-Version: projt-LOCAL +Implementation-Vendor: ZekerZhayard +Implementation-Timestamp: 2026-04-05T12:00:00+0000 +Automatic-Module-Name: io.github.zekerzhayard.forgewrapper +Multi-Release: true +GitCommit: null +``` + +--- + +## 14. Publishing to Local Maven Repository + +### How It Works + +The `publishing` block in `build.gradle` (lines 65–80) configures a Maven +publication with a **local filesystem repository**: + +```groovy +repositories { + maven { + url = layout.buildDirectory.dir("maven") + } +} +``` + +`layout.buildDirectory.dir("maven")` resolves to `build/maven/`. This is a +Gradle 7+ API (`layout.buildDirectory` replaces the deprecated `$buildDir`). + +### Running the Publish + +```bash +./gradlew publish +``` + +The `publish` task depends on `build` (line 81: `tasks.publish.dependsOn build`), +so running `publish` implicitly runs the full build first. + +### Generated POM + +The POM is auto-generated from `components.java`. Since all external dependencies +are `compileOnly`, they are **not** included in the POM. The POM contains only +the GAV (Group, Artifact, Version) coordinates: + +```xml +<?xml version="1.0" encoding="UTF-8"?> +<project xsi:schemaLocation="..."> + <modelVersion>4.0.0</modelVersion> + <groupId>io.github.zekerzhayard</groupId> + <artifactId>ForgeWrapper</artifactId> + <version>projt-LOCAL</version> +</project> +``` + +No `<dependencies>` section is generated because `compileOnly` dependencies are +not published. + +### Using the Local Maven Repo + +Other projects can consume the published artifact by adding the local directory +as a Maven repository: + +```groovy +repositories { + maven { + url = file("/path/to/forgewrapper/build/maven") + } +} + +dependencies { + implementation "io.github.zekerzhayard:ForgeWrapper:projt-LOCAL" +} +``` + +--- + +## 15. Java Version Requirements + +### Summary Table + +| Component | Minimum Java | Configured As | +|---------------------|--------------|---------------------------------------------| +| Gradle execution | Java 8 | Gradle 7.3.3 supports Java 8–17 | +| Root compilation | Java 8 | `sourceCompatibility = targetCompatibility = 1.8` | +| Jigsaw compilation | Java 9 | `sourceCompatibility = targetCompatibility = 9` | +| Runtime (Java 8) | Java 8 | Uses root ModuleUtil (no-op stubs) | +| Runtime (Java 9+) | Java 9 | Uses jigsaw ModuleUtil (full JPMS) | + +### Toolchain Behavior + +The jigsaw subproject uses Gradle's Java Toolchain feature with conditional +logic (lines 7–9 of `jigsaw/build.gradle`): + +``` +┌────────────────────────────────┐ +│ Gradle running on Java 8? │ +│ │ +│ YES → javaToolchains finds │ +│ Java 9 JDK on system │ +│ and uses it to compile │ +│ │ +│ NO → Current JVM (≥9) │ +│ compiles with │ +│ sourceCompatibility=9 │ +└────────────────────────────────┘ +``` + +### Finding Java Toolchains + +When Gradle needs a Java 9 toolchain, it searches these locations: + +1. **Environment** — `JAVA_HOME`, `JDK_HOME`, and `PATH` entries. +2. **Standard paths** — `/usr/lib/jvm/` (Linux), `/Library/Java/JavaVirtualMachines/` (macOS), + Windows Registry. +3. **Tool managers** — SDKMAN!, jabba, IntelliJ installations. +4. **Gradle auto-provisioning** — If enabled, Gradle can download a JDK + automatically from AdoptOpenJDK/Adoptium. + +### Bytecode Versions + +| Class Origin | Target | Class File Version | `-target` flag | +|---------------------|--------|--------------------|----------------| +| Root project | 1.8 | 52.0 | `1.8` | +| Jigsaw subproject | 9 | 53.0 | `9` | + +You can verify bytecode versions with `javap`: + +```bash +# Check root ModuleUtil (should be 52.0 = Java 8) +javap -v build/classes/java/main/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class \ + | grep "major version" + +# Check jigsaw ModuleUtil (should be 53.0 = Java 9) +javap -v jigsaw/build/classes/java/main/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class \ + | grep "major version" +``` + +--- + +## 16. Source File Layout and the Dual-ModuleUtil Pattern + +### Why Two ModuleUtil Classes? + +ForgeWrapper must run on both Java 8 and Java 9+ JVMs. The Java 9+ `ModuleUtil` +uses APIs that don't exist in Java 8: + +| API Used in Jigsaw ModuleUtil | Introduced In | +|-----------------------------------------|---------------| +| `java.lang.module.Configuration` | Java 9 | +| `java.lang.module.ModuleFinder` | Java 9 | +| `java.lang.module.ModuleReference` | Java 9 | +| `java.lang.module.ResolvedModule` | Java 9 | +| `ModuleLayer.boot()` | Java 9 | +| `ClassLoader.getPlatformClassLoader()` | Java 9 | +| `List.of()` | Java 9 | +| `Module.implAddExports()` | Java 9 (internal) | +| `Module.implAddOpens()` | Java 9 (internal) | + +If these APIs were in the main source tree compiled with Java 8, the build +would fail with compilation errors. The Multi-Release JAR approach solves this +by keeping the Java 9 code in a separate compilation unit. + +### Method Signature Compatibility + +Both `ModuleUtil` classes must have **identical method signatures** so that +callers in the root project (e.g., `Bootstrap.java`, `Main.java`) can reference +`ModuleUtil` without caring which version is loaded at runtime. + +**Root ModuleUtil (Java 8 stubs):** +```java +public static void addModules(String modulePath) // line 10: empty body +public static void addExports(List<String> exports) // line 14: empty body +public static void addOpens(List<String> opens) // line 18: empty body +public static void setupClassPath(Path, List<String>) // line 22: URLClassLoader reflection +public static Class<?> setupBootstrapLauncher(Class<?>)// line 31: returns mainClass +public static ClassLoader getPlatformClassLoader() // line 36: returns null +``` + +**Jigsaw ModuleUtil (Java 9 implementation):** +```java +public static void addModules(String modulePath) // Full JPMS module loading +public static void addExports(List<String> exports) // Module.implAddExports +public static void addOpens(List<String> opens) // Module.implAddOpens +public static void setupClassPath(Path, List<String>) // Module-aware loading +public static Class<?> setupBootstrapLauncher(Class<?>)// BootstrapLauncher setup +public static ClassLoader getPlatformClassLoader() // ClassLoader.getPlatformClassLoader() +``` + +### Call Sites + +`Bootstrap.java` calls these ModuleUtil methods (lines 70–76): + +```java +if (modulePath != null) { + ModuleUtil.addModules(modulePath); +} +ModuleUtil.addExports(addExports); +ModuleUtil.addOpens(addOpens); +``` + +`Main.java` calls (lines 49, 62, 63): + +```java +// line 49 (in URLClassLoader creation): +ModuleUtil.getPlatformClassLoader() + +// lines 62-63: +ModuleUtil.setupClassPath(detector.getLibraryDir(), ...); +Class<?> mainClass = ModuleUtil.setupBootstrapLauncher(Class.forName(...)); +``` + +At runtime, the JVM transparently loads the correct `ModuleUtil` class based on +the Java version, with no conditional logic needed in the calling code. + +--- + +## 17. Troubleshooting + +### Build Fails: "No compatible toolchains found" + +**Symptom:** +``` +> No locally installed toolchains match and toolchain download repositories + have not been configured. +``` + +**Cause:** Gradle is running on Java 8 and cannot find a Java 9+ JDK for the +jigsaw subproject. + +**Fix:** Install a Java 9+ JDK. On Linux: +```bash +# Ubuntu/Debian +sudo apt install openjdk-11-jdk + +# Fedora +sudo dnf install java-11-openjdk-devel +``` + +Or set `JAVA_HOME` to an existing Java 9+ installation: +```bash +export JAVA_HOME=/path/to/jdk-11 +./gradlew build +``` + +### Build Fails: "Could not resolve project :jigsaw" + +**Symptom:** +``` +> Could not resolve project :jigsaw. + > Incompatible because this component declares a component compatible + with Java 9 and the consumer needed a component compatible with Java 8 +``` + +**Cause:** The `TargetJvmVersion` attribute override in `jigsaw/build.gradle` +is missing or incorrect. + +**Fix:** Ensure lines 16–27 of `jigsaw/build.gradle` are present: +```groovy +configurations { + apiElements { + attributes { + attribute TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8 + } + } + runtimeElements { + attributes { + attribute TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8 + } + } +} +``` + +### Dependencies Not Found + +**Symptom:** +``` +> Could not find cpw.mods:modlauncher:8.0.9. +``` + +**Cause:** The Forge Maven repository is unreachable or not declared. + +**Fix:** Check network connectivity to `https://maven.minecraftforge.net/`. +Verify the `repositories` block in `build.gradle` includes the `forge` maven +repository (lines 24–29). + +### Wrong Version in JAR Name + +**Symptom:** JAR is named `ForgeWrapper-projt-LOCAL.jar` but you expected a +dated version. + +**Cause:** `GITHUB_ACTIONS` and `IS_PUBLICATION` are not set. + +**Fix:** +```bash +IS_PUBLICATION=1 ./gradlew build +``` + +### MultiRelease Attribute Missing + +**Symptom:** The JAR does not exhibit Multi-Release behavior on Java 9+. +`ModuleUtil` methods are no-ops even on Java 11. + +**Diagnosis:** Inspect the manifest: +```bash +unzip -p build/libs/ForgeWrapper-*.jar META-INF/MANIFEST.MF | grep Multi-Release +``` + +If `Multi-Release: true` is missing, check the `jar` block in `build.gradle` +(line 54). + +Also verify the jigsaw classes exist in the JAR: +```bash +jar tf build/libs/ForgeWrapper-*.jar | grep "META-INF/versions/9" +``` + +Expected output should show the `ModuleUtil.class` under `META-INF/versions/9/`. + +### Gradle Daemon Issues + +**Symptom:** Stale build state, phantom errors that disappear after restarting. + +**Note:** The daemon is disabled (`org.gradle.daemon = false` in +`gradle.properties`), so this should not normally occur. If a daemon is somehow +running from a previous configuration: + +```bash +./gradlew --stop +./gradlew clean build +``` + +### Java Version Mismatch in IDE + +**Symptom:** Eclipse or IntelliJ shows errors on jigsaw source files. + +**Cause:** The IDE is using Java 8 to compile the jigsaw source set. + +**Fix for Eclipse:** +```bash +./gradlew eclipse +``` +Then re-import the project. The Eclipse plugin generates `.classpath` with +correct JRE containers. + +**Fix for IntelliJ:** Import as Gradle project. IntelliJ reads the `compileJava` +block and sets the jigsaw module to use Java 9. + +### Verifying the MRJAR + +Full verification workflow: + +```bash +# 1. Build +./gradlew clean build + +# 2. Check JAR contents +jar tf build/libs/ForgeWrapper-projt-LOCAL.jar + +# 3. Verify manifest +unzip -p build/libs/ForgeWrapper-projt-LOCAL.jar META-INF/MANIFEST.MF + +# 4. Check bytecode versions +cd build/libs +mkdir -p _verify && cd _verify +jar xf ../ForgeWrapper-projt-LOCAL.jar + +# Root ModuleUtil → should be 52 (Java 8) +javap -v io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class \ + | grep "major version" + +# Jigsaw ModuleUtil → should be 53 (Java 9) +javap -v META-INF/versions/9/io/github/zekerzhayard/forgewrapper/installer/util/ModuleUtil.class \ + | grep "major version" + +# 5. Cleanup +cd .. && rm -rf _verify +``` + +--- + +## 18. Quick Reference Card + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ ForgeWrapper Build Cheat Sheet │ +├──────────────────────────────────────────────────────────────────┤ +│ │ +│ Build: ./gradlew build │ +│ Clean+Build: ./gradlew clean build │ +│ Publish: ./gradlew publish │ +│ JAR only: ./gradlew jar │ +│ Eclipse: ./gradlew eclipse │ +│ Tasks list: ./gradlew tasks --all │ +│ Dependencies: ./gradlew dependencies │ +│ │ +│ CI version: GITHUB_ACTIONS=true ./gradlew build │ +│ Pub version: IS_PUBLICATION=1 ./gradlew build │ +│ │ +│ Gradle: 7.3.3 (wrapper) │ +│ Root Java: 1.8 (source + target) │ +│ Jigsaw Java: 9 (source + target, toolchain auto-detect) │ +│ Group: io.github.zekerzhayard │ +│ Artifact: ForgeWrapper │ +│ Base Version: projt (from gradle.properties: fw_version) │ +│ │ +│ Output JAR: build/libs/ForgeWrapper-<ver>.jar │ +│ Sources JAR: build/libs/ForgeWrapper-<ver>-sources.jar │ +│ Maven output: build/maven/ │ +│ │ +│ Repos: Maven Central │ +│ https://maven.minecraftforge.net/ │ +│ │ +│ Dependencies (all compileOnly): │ +│ gson 2.8.7, modlauncher 8.0.9, │ +│ installer 2.2.7, jopt-simple 5.0.4 │ +│ │ +│ MRJAR overlay: META-INF/versions/9/ (from :jigsaw) │ +│ Manifest key: Multi-Release: true │ +│ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +--- + +*This document was generated from direct analysis of the ForgeWrapper build +configuration files: `build.gradle`, `jigsaw/build.gradle`, `settings.gradle`, +`gradle.properties`, and `gradle/wrapper/gradle-wrapper.properties`.* |
