Skip to content

Commit

Permalink
Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte committed Jul 21, 2024
1 parent 4eb2a55 commit 7a2f1f5
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,8 @@ private static void configureEclipseModel(Project project,
return;
}

eclipseModel.getJdt().setJavaRuntimeName(ECLIPSE_DEFAULT_JRE);

// Make sure our post-sync task runs on Eclipse
eclipseModel.synchronizationTasks(ideSyncTask);

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<String> 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
Expand Down
53 changes: 24 additions & 29 deletions src/main/java/net/neoforged/moddevgradle/tasks/JarJar.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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.artifacts.Configuration;
Expand All @@ -20,20 +21,13 @@
import org.jetbrains.annotations.ApiStatus;

import javax.inject.Inject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HexFormat;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -70,30 +64,33 @@ public JarJar(FileSystemOperations fileSystemOperations) {
}

@TaskAction
protected void run() {
protected void run() throws IOException {
List<ResolvedJarJarArtifact> includedJars = new ArrayList<>(getJarJarArtifacts().getResolvedArtifacts().get());
fileSystemOperations.delete(spec -> spec.delete(getOutputDirectory()));

var buildDir = getBuildDirectory().getAsFile().get().toPath();

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)) {
// This is only intended for libraries built by this project, so this is a shoddy check
if (!file.toPath().startsWith(buildDir)) {
throw new GradleException("Cannot embed file dependencies in the jar if they aren't in this projects build directory.");
// 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 = hashFile(file);
var hashCode = FileUtils.hashFile(file, "MD5");
includedJars.add(new ResolvedJarJarArtifact(
file,
file.getName(),
hashCode,
"[" + hashCode + "]",
"",
file.getName().toLowerCase(Locale.ROOT)
moduleName.get()
));
artifactFiles.add(file);
}
Expand All @@ -105,6 +102,16 @@ protected void run() {
spec.into(getOutputDirectory().dir("META-INF/jarjar"));
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());
}

var originalName = includedJar.getFile().getName();
var embeddedName = includedJar.getEmbeddedFilename();
if (!originalName.equals(embeddedName)) {
Expand All @@ -116,22 +123,10 @@ protected void run() {
}
}

private static String hashFile(File file) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
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);
}
}

@SuppressWarnings("ResultOfMethodCallIgnored")
private Path writeMetadata(List<ResolvedJarJarArtifact> includedJars) {
final Path metadataPath = getJarJarMetadataPath();
final Metadata metadata = createMetadata(includedJars);
var metadataPath = getJarJarMetadataPath();
var metadata = createMetadata(includedJars);

try {
metadataPath.toFile().getParentFile().mkdirs();
Expand Down
109 changes: 107 additions & 2 deletions src/test/java/net/neoforged/moddevgradle/tasks/JarJarTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import net.neoforged.jarjar.metadata.ContainedVersion;
import net.neoforged.jarjar.metadata.Metadata;
import net.neoforged.jarjar.metadata.MetadataIOHandler;
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;
Expand All @@ -17,6 +18,8 @@
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;
Expand All @@ -34,6 +37,103 @@ public void testNoSourceWhenNoDependenciesAreDefined() throws IOException {
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 testCannotEmbedLocalFileWithoutExplicitJavaModuleName() throws IOException {
var localFile = tempDir.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 = tempDir.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());
}

@Test
void testCanEmbedLocalFileWithModuleInfo() throws Exception {
var moduleInfoJava = tempDir.resolve("src/service/java/module-info.java");
Files.createDirectories(moduleInfoJava.getParent());
Files.writeString(moduleInfoJava, "module super_duper_module {}");

var result = runWithSource("""
sourceSets {
service
}
compileServiceJava {
// otherwise testkit needs to run with J21
options.release = 9
}
var serviceJar = tasks.register(sourceSets.service.jarTaskName, Jar) {
from sourceSets.service.output
archiveClassifier = "service"
}
dependencies {
jarJar(files(serviceJar))
}
""");
assertEquals(SUCCESS, result.task(":jarJar").getOutcome());
var md5Hash = FileUtils.hashFile(moduleInfoJava.toFile(), "MD5");
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());
}

@Test
public void testSuccessfulEmbed() throws Exception {
var result = runWithSource("""
Expand Down Expand Up @@ -176,7 +276,12 @@ public void testUnsupportedDynamicVersion() {
}

private BuildResult runWithSource(String source) throws IOException {
Files.writeString(tempDir.resolve("settings.gradle"), "");
Files.writeString(tempDir.resolve("settings.gradle"), """
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = 'jijtest'
""");
Files.writeString(tempDir.resolve("build.gradle"), """
plugins {
id "net.neoforged.moddev"
Expand All @@ -189,7 +294,7 @@ private BuildResult runWithSource(String source) throws IOException {
return GradleRunner.create()
.withPluginClasspath()
.withProjectDir(tempDir.toFile())
.withArguments("jarjar")
.withArguments("jarjar", "--stacktrace")
.withDebug(true)
.build();
}
Expand Down
3 changes: 2 additions & 1 deletion testproject/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,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}"

Expand Down

0 comments on commit 7a2f1f5

Please sign in to comment.