diff --git a/README.md b/README.md index 6f284b0f..7d16ea34 100644 --- a/README.md +++ b/README.md @@ -227,24 +227,6 @@ neoForge { To embed external Jar-files into your mod file, you can use the `jarJar` configuration added by the plugin. -#### Subprojects - -For example, if you have a coremod in a subproject and want to embed its jar file, you can use the following syntax. - -```groovy -dependencies { - jarJar project(":coremod") -} -``` - -When starting the game, FML will use the group and artifact id of an embedded Jar-file to determine if the same file -has been embedded in other mods. -For subprojects, the group id is the root project name, while the artifact id is the name of the subproject. -Besides the group and artifact id, the Java module name of an embedded Jar also has to be unique across all loaded -Jar files. -To decrease the likelihood of conflicts if no explicit module name is set, -we prefix the filename of embedded subprojects with the group id. - #### External Dependencies When you want to bundle external dependencies, Jar-in-Jar has to be able to select a single copy of that dependency @@ -279,7 +261,68 @@ the [Maven version range format](https://cwiki.apache.org/confluence/display/MAV | (,1.0],[1.2,) | x <= 1.0 or x >= 1.2. Multiple sets are comma-separated | | (,1.1),(1.1,) | This excludes 1.1 if it is known not to work in combination with this library | -#### External Dependencies: Runs +#### Local Files + +You can also include files built by other tasks in your project, for example, jar tasks of other source sets. + +When wanting to build a secondary jar for a coremod or plugin, you could define a separate source set `plugin`, +add a jar task to package it and then include the output of that jar like this: + +```groovy +sourceSets { + plugin +} + + +neoForge { + // ... + mods { + // ... + // To make the plugin load in dev + 'plugin' { + sourceSet sourceSets.plugin + } + } +} + +def pluginJar = tasks.register("pluginJar", Jar) { + from(sourceSets.plugin.output) + archiveClassifier = "plugin" + manifest { + attributes( + 'FMLModType': "LIBRARY", + "Automatic-Module-Name": project.name + "-plugin" + ) + } +} + +dependencies { + jarJar files(pluginJar) +} +``` + +When you include a jar file like this, we use its filename as the artifact-id and its MD5 hash as the version. +It will never be swapped out with embedded libraries of the same name, unless their content matches. + +#### Subprojects + +For example, if you have a coremod in a subproject and want to embed its jar file, you can use the following syntax. + +```groovy +dependencies { + jarJar project(":coremod") +} +``` + +When starting the game, FML will use the group and artifact id of an embedded Jar-file to determine if the same file +has been embedded in other mods. +For subprojects, the group id is the root project name, while the artifact id is the name of the subproject. +Besides the group and artifact id, the Java module name of an embedded Jar also has to be unique across all loaded +Jar files. +To decrease the likelihood of conflicts if no explicit module name is set, +we prefix the filename of embedded subprojects with the group id. + +### External Dependencies: Runs External dependencies will only be loaded in your runs if they are mods (with a `META-INF/neoforge.mods.toml` file), or if they have the `FMLModType` entry set in their `META-INF/MANIFEST.MF` file. Usually, Java libraries do not fit either of these requirements, diff --git a/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java b/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java index 0ec89de4..5340c967 100644 --- a/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java +++ b/src/main/java/net/neoforged/moddevgradle/internal/utils/FileUtils.java @@ -1,17 +1,27 @@ package net.neoforged.moddevgradle.internal.utils; +import org.gradle.api.GradleException; import org.jetbrains.annotations.ApiStatus; +import java.io.File; +import java.io.FileInputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.lang.module.ModuleDescriptor; import java.nio.charset.Charset; import java.nio.file.AccessDeniedException; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.HexFormat; import java.util.List; +import java.util.Optional; +import java.util.jar.JarFile; +import java.util.zip.ZipFile; @ApiStatus.Internal public final class FileUtils { @@ -23,6 +33,47 @@ public final class FileUtils { private FileUtils() { } + /** + * Finds an explicitly defined Java module name in the given Jar file. + */ + public static Optional getExplicitJavaModuleName(File file) throws IOException { + try (var jf = new JarFile(file, false, ZipFile.OPEN_READ, JarFile.runtimeVersion())) { + var moduleInfoEntry = jf.getJarEntry("module-info.class"); + if (moduleInfoEntry != null) { + try (var in = jf.getInputStream(moduleInfoEntry)) { + return Optional.of(ModuleDescriptor.read(in).name()); + } + } + + var manifest = jf.getManifest(); + if (manifest == null) { + return Optional.empty(); + } + + var automaticModuleName = manifest.getMainAttributes().getValue("Automatic-Module-Name"); + if (automaticModuleName == null) { + return Optional.empty(); + } + + return Optional.of(automaticModuleName); + } catch (Exception e) { + throw new IOException("Failed to determine the Java module name of " + file + ": " + e, e); + } + + } + + public static String hashFile(File file, String algorithm) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + try (var input = new DigestInputStream(new FileInputStream(file), digest)) { + input.transferTo(OutputStream.nullOutputStream()); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (Exception e) { + throw new GradleException("Failed to hash file " + file, e); + } + } + public static void writeStringSafe(Path destination, String content, Charset charset) throws IOException { if (!charset.newEncoder().canEncode(content)) { throw new IllegalArgumentException("The given character set " + charset diff --git a/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java b/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java index 977cbb6a..4218ae6f 100644 --- a/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java +++ b/src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java @@ -4,7 +4,9 @@ import net.neoforged.jarjar.metadata.MetadataIOHandler; import net.neoforged.moddevgradle.internal.jarjar.JarJarArtifacts; import net.neoforged.moddevgradle.internal.jarjar.ResolvedJarJarArtifact; +import net.neoforged.moddevgradle.internal.utils.FileUtils; import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.attributes.Bundling; @@ -18,6 +20,7 @@ import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.SkipWhenEmpty; @@ -31,6 +34,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; @@ -57,12 +61,16 @@ public abstract class JarJar extends DefaultTask { @OutputDirectory public abstract DirectoryProperty getOutputDirectory(); + @Internal + public abstract DirectoryProperty getBuildDirectory(); + private final FileSystemOperations fileSystemOperations; @Inject public JarJar(FileSystemOperations fileSystemOperations) { this.fileSystemOperations = fileSystemOperations; this.getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir("generated/" + getName())); + this.getBuildDirectory().convention(getProject().getLayout().getBuildDirectory()); setGroup(DEFAULT_GROUP); } @@ -100,16 +108,60 @@ public static TaskProvider registerWithConfiguration(Project project, St } @TaskAction - protected void run() { - List includedJars = getJarJarArtifacts().getResolvedArtifacts().get(); + protected void run() throws IOException { + List includedJars = new ArrayList<>(getJarJarArtifacts().getResolvedArtifacts().get()); fileSystemOperations.delete(spec -> spec.delete(getOutputDirectory())); + var artifactFiles = new ArrayList<>(includedJars.stream().map(ResolvedJarJarArtifact::getFile).toList()); + // Now we have to handle pure file collection dependencies that do not have artifact ids + for (var file : getInputFiles()) { + if (!artifactFiles.contains(file)) { + // Determine the module-name of the file, which is also what Java will use as the unique key + // when it tries to load the file. No two files can have the same module name, so it seems + // like a fitting key for conflict resolution by JiJ. + var moduleName = FileUtils.getExplicitJavaModuleName(file); + if (moduleName.isEmpty()) { + throw new GradleException("Cannot embed local file dependency " + file + " because it has no explicit Java module name.\n" + + "Please set either 'Automatic-Module-Name' in the Jar manifest, or make it an explicit Java module.\n" + + "This ensures that your file does not conflict with another mods library that has the same or a similar filename."); + } + + // Create a hashcode to use as a version + var hashCode = FileUtils.hashFile(file, "MD5"); + includedJars.add(new ResolvedJarJarArtifact( + file, + file.getName(), + hashCode, + "[" + hashCode + "]", + "", + moduleName.get() + )); + artifactFiles.add(file); + } + } + // Only copy metadata if not empty, always delete if (!includedJars.isEmpty()) { fileSystemOperations.copy(spec -> { spec.into(getOutputDirectory().dir("META-INF/jarjar")); - spec.from(includedJars.stream().map(ResolvedJarJarArtifact::getFile).toArray()); + spec.from(artifactFiles.toArray()); for (var includedJar : includedJars) { + // Warn if any included jar is using the cursemaven group. + // We know that cursemaven versions are not comparable, and the same artifact might also be + // available under a "normal" group and artifact from another Maven repository. + // JIJ will not correctly detect the conflicting file at runtime if another mod uses the normal Maven dependency. + // For a description of Curse Maven, see https://www.cursemaven.com/ + if ("curse.maven".equals(includedJar.getGroup())) { + getLogger().warn("Embedding dependency {}:{}:{} from cursemaven using JiJ is likely to cause conflicts at runtime when other mods include the same library from a normal Maven repository.", + includedJar.getGroup(), includedJar.getArtifact(), includedJar.getVersion()); + } + // Same with the Modrinth official maven (see https://support.modrinth.com/en/articles/8801191-modrinth-maven) + // While actual versions can be used, version IDs (which are random strings) can also be used + else if ("maven.modrinth".equals(includedJar.getGroup())) { + getLogger().warn("Embedding dependency {}:{}:{} from Modrinth Maven using JiJ is likely to cause conflicts at runtime when other mods include the same library from a normal Maven repository.", + includedJar.getGroup(), includedJar.getArtifact(), includedJar.getVersion()); + } + var originalName = includedJar.getFile().getName(); var embeddedName = includedJar.getEmbeddedFilename(); if (!originalName.equals(embeddedName)) { @@ -123,8 +175,8 @@ protected void run() { @SuppressWarnings("ResultOfMethodCallIgnored") private Path writeMetadata(List includedJars) { - final Path metadataPath = getJarJarMetadataPath(); - final Metadata metadata = createMetadata(includedJars); + var metadataPath = getJarJarMetadataPath(); + var metadata = createMetadata(includedJars); try { metadataPath.toFile().getParentFile().mkdirs(); diff --git a/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java b/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java index 5a0a1441..1427611e 100644 --- a/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java +++ b/src/test/java/net/neoforged/moddevgradle/functional/AbstractFunctionalTest.java @@ -12,7 +12,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -abstract class AbstractFunctionalTest { +public abstract class AbstractFunctionalTest { static final String DEFAULT_NEOFORGE_VERSION = "21.0.133-beta"; static final Map DEFAULT_PLACEHOLDERS = Map.of( @@ -20,9 +20,9 @@ abstract class AbstractFunctionalTest { ); @TempDir - File testProjectDir; - File settingsFile; - File buildFile; + protected File testProjectDir; + protected File settingsFile; + protected File buildFile; @BeforeEach final void setBaseFiles() { diff --git a/src/test/java/net/neoforged/moddevgradle/tasks/JarJarTest.java b/src/test/java/net/neoforged/moddevgradle/tasks/JarJarTest.java index 14ef408c..7b4e4355 100644 --- a/src/test/java/net/neoforged/moddevgradle/tasks/JarJarTest.java +++ b/src/test/java/net/neoforged/moddevgradle/tasks/JarJarTest.java @@ -5,18 +5,21 @@ import net.neoforged.jarjar.metadata.ContainedVersion; import net.neoforged.jarjar.metadata.Metadata; import net.neoforged.jarjar.metadata.MetadataIOHandler; +import net.neoforged.moddevgradle.functional.AbstractFunctionalTest; +import net.neoforged.moddevgradle.internal.utils.FileUtils; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; +import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import static org.assertj.core.api.Assertions.assertThat; import static org.gradle.testkit.runner.TaskOutcome.NO_SOURCE; @@ -24,16 +27,214 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -class JarJarTest { - @TempDir - Path tempDir; - +class JarJarTest extends AbstractFunctionalTest { @Test public void testNoSourceWhenNoDependenciesAreDefined() throws IOException { var result = runWithSource(""); assertEquals(NO_SOURCE, result.task(":jarJar").getOutcome()); } + @Test + void testEmbeddingCurseMavenDependencyProducesWarning() throws IOException { + var result = runWithSource(""" + repositories { + maven { + url "https://www.cursemaven.com" + content { + includeGroup "curse.maven" + } + } + } + dependencies { + jarJar(implementation("curse.maven:jade-324717:5444008")) + } + """); + assertEquals(SUCCESS, result.task(":jarJar").getOutcome()); + assertThat(result.getOutput()).contains("Embedding dependency curse.maven:jade-324717:5444008 from cursemaven using JiJ is likely to cause conflicts at runtime when other mods include the same library from a normal Maven repository."); + } + + @Test + void testEmbeddingmODRINTHMavenDependencyProducesWarning() throws IOException { + var result = runWithSource(""" + repositories { + maven { + url = "https://api.modrinth.com/maven" + content { + includeGroup "maven.modrinth" + } + } + } + dependencies { + jarJar(implementation("maven.modrinth:lithium:mc1.19.2-0.10.0")) + } + """); + assertEquals(SUCCESS, result.task(":jarJar").getOutcome()); + assertThat(result.getOutput()).contains("Embedding dependency maven.modrinth:lithium:mc1.19.2-0.10.0 from Modrinth Maven using JiJ is likely to cause conflicts at runtime when other mods include the same library from a normal Maven repository."); + } + + @Test + void testCannotEmbedLocalFileWithoutExplicitJavaModuleName() throws IOException { + var localFile = testProjectDir.toPath().resolve("file.jar"); + new JarOutputStream(Files.newOutputStream(localFile), new Manifest()).close(); + + var e = assertThrows(UnexpectedBuildFailure.class, () -> runWithSource(""" + dependencies { + jarJar(files("file.jar")) + } + """)); + assertThat(e).hasMessageFindingMatch("Cannot embed local file dependency .*file.jar because it has no explicit Java module name.\\s*" + + "Please set either 'Automatic-Module-Name' in the Jar manifest, or make it an explicit Java module.\\s*" + + "This ensures that your file does not conflict with another mods library that has the same or a similar filename."); + } + + @Test + void testCanEmbedLocalFileWithAutomaticModuleName() throws Exception { + var localFile = testProjectDir.toPath().resolve("file.jar"); + var manifest = new Manifest(); + manifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + manifest.getMainAttributes().putValue("Automatic-Module-Name", "super_duper_module"); + new JarOutputStream(Files.newOutputStream(localFile), manifest).close(); + var md5Hash = FileUtils.hashFile(localFile.toFile(), "MD5"); + + var result = runWithSource(""" + dependencies { + jarJar(files("file.jar")) + } + """); + assertEquals(SUCCESS, result.task(":jarJar").getOutcome()); + assertEquals(new Metadata( + List.of( + new ContainedJarMetadata( + new ContainedJarIdentifier("", "super_duper_module"), + new ContainedVersion(VersionRange.createFromVersionSpec("[" + md5Hash + "]"), new DefaultArtifactVersion(md5Hash)), + "META-INF/jarjar/file.jar", + false + ) + ) + ), readMetadata()); + } + + /** + * The default capability of a subproject uses group=name of the root project + */ + @Test + void testEmbeddingSubprojectUsesDefaultCapabilityCoordinate() throws Exception { + writeProjectFile("settings.gradle", """ + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + } + rootProject.name = 'root_project_name' + + include ':plugin' + """); + writeProjectFile("build.gradle", """ + plugins { + id "net.neoforged.moddev" + } + dependencies { + jarJar(project(":plugin")) + } + """); + writeProjectFile("plugin/build.gradle", """ + plugins { + id 'java' + } + version = "9.0.0" + """); + + var result = run(); + assertEquals(SUCCESS, result.task(":jarJar").getOutcome()); + assertEquals(new Metadata( + List.of( + new ContainedJarMetadata( + new ContainedJarIdentifier("root_project_name", "plugin"), + new ContainedVersion(VersionRange.createFromVersionSpec("[9.0.0,)"), new DefaultArtifactVersion("9.0.0")), + "META-INF/jarjar/root_project_name.plugin-9.0.0.jar", + false + ) + ) + ), readMetadata()); + } + + /** + * The default capability of a subproject uses group=name of the root project + */ + @Test + void testEmbeddingSubprojectWithExplicitGroupIdSet() throws Exception { + writeProjectFile("settings.gradle", """ + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + } + rootProject.name = 'root_project_name' + + include ':plugin' + """); + writeProjectFile("build.gradle", """ + plugins { + id "net.neoforged.moddev" + } + dependencies { + jarJar(project(":plugin")) + } + """); + writeProjectFile("plugin/build.gradle", """ + plugins { + id 'java' + } + version = "9.0.0" + group = "net.somegroup" + """); + + var result = run(); + assertEquals(SUCCESS, result.task(":jarJar").getOutcome()); + assertEquals(new Metadata( + List.of( + new ContainedJarMetadata( + new ContainedJarIdentifier("net.somegroup", "plugin"), + new ContainedVersion(VersionRange.createFromVersionSpec("[9.0.0,)"), new DefaultArtifactVersion("9.0.0")), + "META-INF/jarjar/net.somegroup.plugin-9.0.0.jar", + false + ) + ) + ), readMetadata()); + } + + @Test + void testCanEmbedLocalFileWithModuleInfo() throws Exception { + var moduleInfoJava = testProjectDir.toPath().resolve("src/plugin/java/module-info.java"); + Files.createDirectories(moduleInfoJava.getParent()); + Files.writeString(moduleInfoJava, "module super_duper_module {}"); + + var result = runWithSource(""" + sourceSets { + plugin + } + compilePluginJava { + // otherwise testkit needs to run with J21 + options.release = 17 + } + var pluginJar = tasks.register(sourceSets.plugin.jarTaskName, Jar) { + from sourceSets.plugin.output + archiveClassifier = "plugin" + } + dependencies { + jarJar(files(pluginJar)) + } + """); + assertEquals(SUCCESS, result.task(":jarJar").getOutcome()); + var md5Hash = FileUtils.hashFile(new File(testProjectDir, "build/libs/jijtest-plugin.jar"), "MD5"); + assertEquals(new Metadata( + List.of( + new ContainedJarMetadata( + new ContainedJarIdentifier("", "super_duper_module"), + new ContainedVersion(VersionRange.createFromVersionSpec("[" + md5Hash + "]"), new DefaultArtifactVersion(md5Hash)), + "META-INF/jarjar/jijtest-plugin.jar", + false + ) + ) + ), readMetadata()); + } + @Test public void testSuccessfulEmbed() throws Exception { var result = runWithSource(""" @@ -176,26 +377,35 @@ public void testUnsupportedDynamicVersion() { } private BuildResult runWithSource(String source) throws IOException { - Files.writeString(tempDir.resolve("settings.gradle"), ""); - Files.writeString(tempDir.resolve("build.gradle"), """ - plugins { - id "net.neoforged.moddev" - } - repositories { - mavenCentral() - } - """ + source); + writeProjectFile("settings.gradle", """ + plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" + } + rootProject.name = 'jijtest' + """); + writeProjectFile("build.gradle", """ + plugins { + id "net.neoforged.moddev" + } + repositories { + mavenCentral() + } + """ + source); + + return run(); + } + private BuildResult run() { return GradleRunner.create() .withPluginClasspath() - .withProjectDir(tempDir.toFile()) - .withArguments("jarjar") + .withProjectDir(testProjectDir) + .withArguments("jarjar", "--stacktrace") .withDebug(true) .build(); } private List listFiles() throws IOException { - var path = tempDir.resolve("build/generated/jarJar"); + var path = testProjectDir.toPath().resolve("build/generated/jarJar"); if (!Files.isDirectory(path)) { return List.of(); } @@ -207,7 +417,7 @@ private List listFiles() throws IOException { } private Metadata readMetadata() throws IOException { - try (var in = Files.newInputStream(tempDir.resolve("build/generated/jarJar/META-INF/jarjar/metadata.json"))) { + try (var in = Files.newInputStream(testProjectDir.toPath().resolve("build/generated/jarJar/META-INF/jarjar/metadata.json"))) { return MetadataIOHandler.fromStream(in).orElseThrow(); } } diff --git a/testproject/build.gradle b/testproject/build.gradle index 35c47e8f..b5cdd805 100644 --- a/testproject/build.gradle +++ b/testproject/build.gradle @@ -9,7 +9,8 @@ sourceSets { } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testImplementation(enforcedPlatform("org.junit:junit-bom:5.10.2")) + testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation "net.neoforged:testframework:${project.neoforge_version}" diff --git a/testproject/jijtest/build.gradle b/testproject/jijtest/build.gradle index e89a1bbd..028146e3 100644 --- a/testproject/jijtest/build.gradle +++ b/testproject/jijtest/build.gradle @@ -2,14 +2,30 @@ plugins { id 'net.neoforged.moddev' } +sourceSets { + plugin +} + +def pluginJar = tasks.register("pluginJar", Jar) { + from(sourceSets.plugin.output) + archiveClassifier = "plugin" + manifest { + attributes( + 'FMLModType': 'LIBRARY', + 'Automatic-Module-Name': 'jij.plugin' + ) + } +} + evaluationDependsOn(":coremod") // Because of the sourceset reference dependencies { implementation project(":coremod") - + implementation files(sourceSets.plugin.output) testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' jarJar(project(":coremod")) + jarJar(files(pluginJar)) jarJar("org.commonmark:commonmark") { version { @@ -39,6 +55,9 @@ neoForge { coremod { sourceSet project(":coremod").sourceSets.main } + plugin { + sourceSet sourceSets.plugin + } } unitTest { diff --git a/testproject/jijtest/src/main/java/jijtest/AccessPluginClass.java b/testproject/jijtest/src/main/java/jijtest/AccessPluginClass.java new file mode 100644 index 00000000..a1aaae59 --- /dev/null +++ b/testproject/jijtest/src/main/java/jijtest/AccessPluginClass.java @@ -0,0 +1,15 @@ +package jijtest; + +import cpw.mods.modlauncher.TransformingClassLoader; +import jijtestplugin.Plugin; +import net.neoforged.fml.common.Mod; + +@Mod("jijtest") +public class AccessPluginClass { + public AccessPluginClass() { + // Validate that Plugin.class is not loaded via the transforming classloader + if (Plugin.class.getClassLoader() instanceof TransformingClassLoader) { + throw new IllegalStateException("Expected Plugin to be loaded as a plugin!"); + } + } +} diff --git a/testproject/jijtest/src/plugin/java/jijtestplugin/Plugin.java b/testproject/jijtest/src/plugin/java/jijtestplugin/Plugin.java new file mode 100644 index 00000000..33e3f011 --- /dev/null +++ b/testproject/jijtest/src/plugin/java/jijtestplugin/Plugin.java @@ -0,0 +1,4 @@ +package jijtestplugin; + +public class Plugin { +} diff --git a/testproject/jijtest/src/plugin/resources/META-INF/MANIFEST.MF b/testproject/jijtest/src/plugin/resources/META-INF/MANIFEST.MF new file mode 100644 index 00000000..eb6b1cfa --- /dev/null +++ b/testproject/jijtest/src/plugin/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Note: Dummy Manifest file only for in-dev runtime +FMLModType: LIBRARY diff --git a/testproject/jijtest/src/test/java/jijtest/CoreModTest.java b/testproject/jijtest/src/test/java/jijtest/CoreModTest.java index a7cb4800..047dde26 100644 --- a/testproject/jijtest/src/test/java/jijtest/CoreModTest.java +++ b/testproject/jijtest/src/test/java/jijtest/CoreModTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class CoreModTest { @@ -11,5 +12,10 @@ public class CoreModTest { void testPresenceOfCoreMod() throws Exception { var field = assertDoesNotThrow(() -> ItemStack.class.getField("CORE_MOD_MARKER")); assertTrue(field.getBoolean(null)); + + var obj = new jijtestplugin.Plugin(); + // ensures it is *not* a transforming classloader, meaning it was loaded in our parent layer + assertEquals("cpw.mods.cl.ModuleClassLoader", obj.getClass().getClassLoader().getClass().getName()); + assertEquals("cpw.mods.modlauncher.TransformingClassLoader", getClass().getClassLoader().getClass().getName()); } }