Skip to content

Commit e03ab9e

Browse files
committed
stage2/lw: Abstract away from always installing Essential
(Note that all changes talked about in this commit only apply to EssentialLoader for LaunchWrapper. Not to EssentialLoader for ModLauncher or Fabric, those continue operating as they used to.) This commit rewrites most of Essential Loader for LaunchWrapper to no longer always implicitly install Essential and instead provide a more general dependency loading/negotiating mechanism (similar to fabric-loader's Jar-in-Jar mechanism) useable by third-party mods via a `essential.mod.json` metadata file. To this end, this commit practically removes stage1 on LaunchWrapper because stage1 necessarily interacts with Essential infra to download/update stage2 and with the Essential mod to ask the user for permission to do so. Instead stage2 now has a self-update mechanism where if it finds a more recent stage2 in any of the discovered jars (e.g. the downloaded Essential jar), it will re-launch into that newer stage2. Additionally, this also removes the concept of a pinned stage2 and instead always includes the stage2 jar inside the stage1 jar. This also allows using the existing stage1 update mechansim (dropping a file in a specific location) to permanently update stage1 and therefore stage2 to a new version on next boot (e.g. if we want to update the Downloading gui). To support Essential's auto-updating feature (as well as similar third-party ones), in addition to plain inner jars, the `essenital.mod.json` metadata file also supports loading a specific class from the jar to generate additional dependency specifications. This class can then e.g. download a newer version of the mod and emit a dependency specification which points at it.
1 parent 84df279 commit e03ab9e

File tree

28 files changed

+1195
-457
lines changed

28 files changed

+1195
-457
lines changed

.github/workflows/publish-stage2.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
17
2121
2222
- name: Build
23-
run: ./gradlew :stage2:{launchwrapper,fabric,modlauncher{8,9}}:build --stacktrace
23+
run: ./gradlew :stage2:{launchwrapper-legacy,fabric,modlauncher{8,9}}:build --stacktrace
2424

2525
- name: Upload Artifacts
2626
uses: actions/upload-artifact@v4

docs/container-mods.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ overridePinnedVersion=1.2.0.12
109109
## Pinning stage2
110110

111111
Stage2 can be pinned as well.
112+
(But only on Fabric and ModLauncher. On LaunchWrapper the stage2 jar is already included in the stage1 jar and cannot
113+
be updated independently.)
112114

113115
Instead of an `essential-loader.properties` file at the root of the container mod, the file must be placed at
114116
`gg/essential/loader/stage1/stage2.properties`.

docs/stages.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,10 @@ download fails (assuming the host mod has no hard dependency on Essential).
7474

7575
### LaunchWrapper
7676

77-
On LaunchWrapper, it additionally:
78-
- instructs Forge to scan the host mod jar for regular mods (ordinarily Forge would not check for mods in a jar that
79-
already contained a Tweaker).
80-
- if the jar declares a CoreMod, loads it (Forge will not load CoreMods from jars that already contained a Tweaker)
81-
- chain load the Mixin Tweaker if the host mod declares a `MixinConfigs` attribute and the Mixin Tweaker is not yet
82-
loaded or scheduled (you can only have one Tweaker declared, you can't have Mixin and Essential Tweaker, so we load
83-
the Mixin one for you if you need it)
84-
- load the host mod's declared mixin configs (Mixin only automatically loads these for mods declaring the Mixin Tweaker)
85-
86-
Stage1 on LaunchWrapper currently does not need to ensure that only a single instance of stage2 is loaded because stage0
87-
actually puts it in the Launch class loader rather than its own isolated class loader for legacy reasons.
88-
So all stage0 instances are talking to the same stage1 class and a simple static field is enough to ensure uniqueness.
77+
On LaunchWrapper, it is even simpler: The stage2 jar file is embedded in the stage1 jar file, so stage1 simply extracts
78+
that (no update checks, downloads, or anything), and then jumps to it like stage0 did.
79+
80+
Stage1 used to do a lot more on LaunchWrapper, but all this functionality has since been moved to stage2.
8981

9082
### Fabric
9183

integrationTest/launchwrapper/build.gradle

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,15 @@ def includeMixin(AbstractArchiveTask task, Configuration mixin) {
133133
}
134134
}
135135

136-
def configureExampleModJar = { String tweaker, Configuration mixin = null -> return { AbstractArchiveTask task ->
136+
def configureExampleModJar = { String tweaker, Configuration mixin = null, excludes = null -> return { AbstractArchiveTask task ->
137137
archiveBaseName.set(task.name)
138138
from(sourceSets.exampleMod.output)
139139
dependsOn(configurations.exampleModRuntimeClasspath)
140-
from({ configurations.exampleModRuntimeClasspath.collect { zipTree(it) } })
140+
from({ configurations.exampleModRuntimeClasspath.collect { zipTree(it) } }) {
141+
if (excludes != null) {
142+
excludes()
143+
}
144+
}
141145

142146
manifest {
143147
attributes "FMLCorePlugin": "com.example.mod.ExampleCoreMod",
@@ -203,39 +207,49 @@ tasks.register("exampleRelocatedModJar", com.github.jengelman.gradle.plugins.sha
203207
def stage2Task = tasks.register("stage2V${i}Jar", Jar) {
204208
archiveBaseName.set(name)
205209
from(evaluationDependsOn(':stage2:launchwrapper').tasks.jar.archiveFile.map { zipTree(it) })
206-
// Dummy attribute so they all have different hashes
207210
manifest {
211+
attributes "Name": "gg/essential/loader/stage2/"
208212
attributes "Implementation-Version": "$i"
209213
}
210214
}
215+
def stage1Task = tasks.register("stage1V${i}Jar", Jar) {
216+
archiveBaseName.set(name)
217+
from(evaluationDependsOn(':stage1:launchwrapper').tasks.jar.archiveFile.map { zipTree(it) }) {
218+
exclude("gg/essential/loader/stage1/stage2.jar")
219+
}
220+
from(stage2Task.flatMap { it.archiveFile }) {
221+
into("gg/essential/loader/stage1")
222+
rename { "stage2.jar" }
223+
}
224+
manifest {
225+
attributes("Name": "gg/essential/loader/stage1/")
226+
attributes "Implementation-Version": "${i}"
227+
}
228+
}
211229
def stage3Task = tasks.register("stage3V${i}Jar", Jar) {
212230
archiveBaseName.set(name)
213231
from(tasks.essentialJar.archiveFile.map { zipTree(it) })
214-
manifest {
215-
// Dummy attribute so they all have different hashes
216-
attributes "Implementation-Version": "$i"
217-
// For stage3 version 4+, we add an explicit requirement on stage2 version 4
218-
if (i >= 4) {
219-
attributes "Requires-Essential-Stage2-Version": "4"
220-
}
232+
from(stage1Task.flatMap { it.archiveFile }) {
233+
into("gg/essential/loader/stage0")
234+
rename { "stage1.jar" }
221235
}
222236
}
223237
tasks.register("exampleBundledModJar$i", Jar) {
224-
def configure = configureExampleModJar("com.example.mod.tweaker.ExampleModTweaker")
238+
def configure = configureExampleModJar("com.example.mod.tweaker.ExampleModTweaker", null, {
239+
exclude("gg/essential/loader/stage0/stage1.jar")
240+
})
225241
configure.delegate = delegate
226242
configure(it)
227243

228-
def stage2Jar = stage2Task.get().archiveFile
229-
def stage3Jar = stage3Task.get().archiveFile
230-
from(stage2Jar) {
231-
rename { "bundled-stage2-${i}.jar" }
244+
from(stage1Task.flatMap { it.archiveFile }) {
245+
into("gg/essential/loader/stage0")
246+
rename { "stage1.jar" }
232247
}
248+
249+
def stage3Jar = stage3Task.get().archiveFile
233250
from(stage3Jar) {
234251
rename { "bundled-essential-${i}.jar" }
235252
}
236-
from(file("../essential-loader-stage2.properties")) {
237-
expand(["pinnedFileMd5": { stage2Jar.get().asFile.bytes.md5() }, version: i])
238-
}
239253
from(file("../essential-loader.properties")) {
240254
expand(["pinnedFileMd5": { stage3Jar.get().asFile.bytes.md5() }, "version": i])
241255
}

integrationTest/launchwrapper/src/main/java/gg/essential/loader/stage1/Stage1DevEnvTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private IsolatedLaunch newDevLaunch(Installation installation) throws Exception
1616
isolatedLaunch.addToClasspath(Paths.get("build", "classes", "java", "exampleMod").toUri().toURL());
1717
isolatedLaunch.addArg("--tweakClass", "gg.essential.loader.stage0.EssentialSetupTweaker");
1818
isolatedLaunch.setProperty("fml.coreMods.load", "com.example.mod.ExampleCoreMod");
19+
isolatedLaunch.setProperty("essential.loader.installEssentialMod", "true");
1920
return isolatedLaunch;
2021
}
2122

integrationTest/launchwrapper/src/main/java/gg/essential/loader/stage2/RelaunchTests.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public void testRelaunchOfInitPhaseMixin(Installation installation, String outer
5353
assertTrue(isolatedLaunch.getModLoadState("mixin"), "Example mixin plugin ran");
5454
assertTrue(isolatedLaunch.getModLoadState("coreMod"), "Example CoreMod ran");
5555
assertTrue(isolatedLaunch.getModLoadState("mod"), "Example Mod ran");
56-
assertTrue(isolatedLaunch.isEssentialLoaded(), "Essential loaded");
5756
}
5857

5958
private IsolatedLaunch newDevLaunch(Installation installation, String...javaArgs) throws IOException {
@@ -113,7 +112,6 @@ public void testRelaunchArgs_UnknownMain(Installation installation) throws Excep
113112
IsolatedLaunch isolatedLaunch = newDevLaunch(installation, "some.unknown.launcher.Main");
114113
isolatedLaunch.launch();
115114

116-
assertTrue(isolatedLaunch.isEssentialLoaded(), "Essential loaded");
117115
assertTrue(isolatedLaunch.getModLoadState("coreMod"), "Example CoreMod ran");
118116
assertTrue(isolatedLaunch.getModLoadState("mod"), "Example Mod ran");
119117
assertTrue(isolatedLaunch.getModLoadState("relaunched"), "Re-launched");
@@ -132,7 +130,6 @@ public void testRelaunchArgs_LaunchWrapper(Installation installation) throws Exc
132130
isolatedLaunch.launch();
133131

134132
installation.assertModLaunched(isolatedLaunch);
135-
assertTrue(isolatedLaunch.isEssentialLoaded(), "Essential loaded");
136133
assertTrue(isolatedLaunch.getModLoadState("relaunched"), "Re-launched");
137134

138135
// For LaunchWrapper we should be able to fully recover everything
@@ -149,7 +146,6 @@ public void testRelaunchArgs_GradleStart(Installation installation) throws Excep
149146
isolatedLaunch.launch();
150147

151148
installation.assertModLaunched(isolatedLaunch);
152-
assertTrue(isolatedLaunch.isEssentialLoaded(), "Essential loaded");
153149
assertTrue(isolatedLaunch.getModLoadState("relaunched"), "Re-launched");
154150

155151
// For GradleStart we should be able to effectively recover everything but not cleanly
@@ -175,7 +171,6 @@ public void testRelaunchArgs_DevLaunchInjector(Installation installation) throws
175171
isolatedLaunch.launch();
176172

177173
installation.assertModLaunched(isolatedLaunch);
178-
assertTrue(isolatedLaunch.isEssentialLoaded(), "Essential loaded");
179174
assertTrue(isolatedLaunch.getModLoadState("relaunched"), "Re-launched");
180175

181176
// For DLI we should be able to fully recover everything
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package gg.essential.loader.stage1;
1+
package gg.essential.loader.stage2;
22

33
import gg.essential.loader.fixtures.Installation;
44
import gg.essential.loader.fixtures.IsolatedLaunch;
@@ -8,7 +8,7 @@
88
import static org.junit.jupiter.api.Assertions.assertFalse;
99
import static org.junit.jupiter.api.Assertions.assertTrue;
1010

11-
public class Stage1MixinTests {
11+
public class Stage2MixinTests {
1212
@Test
1313
public void testMultipleCustomTweakerModsWithMixin07(Installation installation) throws Exception {
1414
testMultipleCustomTweakerModsWithMixin(installation, "07");

integrationTest/launchwrapper/src/main/java/gg/essential/loader/stage2/Stage2Tests.java

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
1515
import static org.apache.commons.codec.digest.DigestUtils.md5Hex;
1616
import static org.junit.jupiter.api.Assertions.assertEquals;
17-
import static org.junit.jupiter.api.Assertions.assertNull;
18-
import static org.junit.jupiter.api.Assertions.assertThrows;
1917
import static org.junit.jupiter.api.Assertions.assertTrue;
2018

2119
public class Stage2Tests {
@@ -118,17 +116,7 @@ public void testUpdateRequiringNewerStage2(Installation installation) throws Exc
118116
Files.copy(withBranch(installation.stage3Meta, "4"), installation.stage3Meta, REPLACE_EXISTING);
119117
writeProps(installation.stage2ConfigFile, props("pendingUpdateVersion=4", "pendingUpdateResolution=true"));
120118
IsolatedLaunch secondLaunch = installation.launchFML();
121-
assertEquals("2", secondLaunch.getProperty("essential.stage2.version"));
122-
assertNull(secondLaunch.getProperty("essential.version"));
123-
assertThrows(ClassNotFoundException.class, () -> secondLaunch.getClass("sun.gg.essential.LoadState"));
124-
125-
// Make available a newer stage2 version
126-
// We make available version 5 even though stage3 only requires version 4; we expect it to upgrade straight to 5
127-
Files.copy(withBranch(installation.stage2Meta, "5"), installation.stage2Meta, REPLACE_EXISTING);
128-
129-
// Restart to complete upgrade
130-
IsolatedLaunch thirdLaunch = installation.launchFML();
131-
assertEquals("5", thirdLaunch.getProperty("essential.stage2.version"));
132-
assertEquals("4", thirdLaunch.getProperty("essential.version"));
119+
assertEquals("4", secondLaunch.getProperty("essential.stage2.version"));
120+
assertEquals("4", secondLaunch.getProperty("essential.version"));
133121
}
134122
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ include(":stage1:modlauncher9")
1919
include(":stage2:common")
2020
include(":stage2:fabric")
2121
include(":stage2:launchwrapper")
22+
include(":stage2:launchwrapper-legacy")
2223
include(":stage2:modlauncher")
2324
include(":stage2:modlauncher8")
2425
include(":stage2:modlauncher9")

stage0/launchwrapper/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
dependencies {
22
compileOnly("net.minecraft:launchwrapper:1.12")
33
}
4+
5+
jar {
6+
manifest {
7+
// See Loader#isRawStage0
8+
attributes("ImplicitlyDependsOnEssential": "false")
9+
}
10+
}

0 commit comments

Comments
 (0)