Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement generation of split-source Manifest entries #257

Merged
merged 5 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,17 @@ private ReplacementResult generateReplacement(final Project project, final Depen
return replacements.computeIfAbsent(minecraftVersion, (v) -> {
final MinecraftArtifactCache minecraftArtifactCacheExtension = project.getExtensions().getByType(MinecraftArtifactCache.class);

Map<GameArtifact, TaskProvider<? extends WithOutput>> tasks = minecraftArtifactCacheExtension.cacheGameVersionTasks(project, minecraftVersion, DistributionType.CLIENT);
Map<GameArtifact, TaskProvider<? extends WithOutput>> tasks = minecraftArtifactCacheExtension.cacheGameVersionTasks(project, minecraftVersion, DistributionType.JOINED);

final TaskProvider<GenerateExtraJar> extraJarTaskProvider = project.getTasks().register("create" + minecraftVersion + StringUtils.capitalize(dependency.getName()) + "ExtraJar", GenerateExtraJar.class, task -> {
task.getOriginalJar().set(tasks.get(GameArtifact.CLIENT_JAR).flatMap(WithOutput::getOutput));
task.getServerJar().set(tasks.get(GameArtifact.SERVER_JAR).flatMap(WithOutput::getOutput));
task.getMappings().set(tasks.get(GameArtifact.CLIENT_MAPPINGS).flatMap(WithOutput::getOutput));
task.getOutput().set(project.getLayout().getBuildDirectory().dir("jars/extra/" + dependency.getName()).map(cacheDir -> cacheDir.dir(Objects.requireNonNull(minecraftVersion)).file( dependency.getName() + "-extra.jar")));

task.dependsOn(tasks.get(GameArtifact.CLIENT_JAR));
task.dependsOn(tasks.get(GameArtifact.SERVER_JAR));
task.dependsOn(tasks.get(GameArtifact.CLIENT_MAPPINGS));
});

return new ReplacementResult(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package net.neoforged.gradle.common.runtime.tasks;

import net.minecraftforge.srgutils.IMappingFile;
import net.neoforged.gradle.common.services.caching.CachedExecutionService;
import net.neoforged.gradle.common.services.caching.jobs.ICacheableJob;
import net.neoforged.gradle.dsl.common.tasks.NeoGradleBase;
import net.neoforged.gradle.dsl.common.tasks.WithOutput;
import net.neoforged.gradle.dsl.common.tasks.WithWorkspace;
import net.neoforged.gradle.util.ZipBuildingFileTreeVisitor;
import org.gradle.api.file.FileTree;
import net.neoforged.gradle.util.FileUtils;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.services.ServiceReference;
Expand All @@ -15,9 +15,22 @@
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import org.jetbrains.annotations.Nullable;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

@CacheableTask
Expand All @@ -34,28 +47,161 @@ public GenerateExtraJar() {
@TaskAction
public void run() throws Throwable {
getCacheService().get()
.cached(
this,
ICacheableJob.Default.file(getOutput(), this::doRun)
).execute();
.cached(
this,
ICacheableJob.Default.file(getOutput(), this::doRun)
).execute();
}

private void doRun() throws Exception {
final File originalJar = getOriginalJar().get().getAsFile();
final File outputJar = ensureFileWorkspaceReady(getOutput());

// Official mappings are Named -> Obf and need to be reversed
var mappings = IMappingFile.load(getMappings().getAsFile().get()).reverse();
try (var clientZip = new JarFile(getOriginalJar().getAsFile().get());
var serverZip = new JarFile(getServerJar().getAsFile().get())) {
var clientFiles = getFileIndex(clientZip);
clientFiles.remove(JarFile.MANIFEST_NAME);
var serverFiles = getFileIndex(serverZip);
serverFiles.remove(JarFile.MANIFEST_NAME);

final FileTree inputTree = getArchiveOperations().zipTree(originalJar);
final FileTree filteredInput = inputTree.matching(filter -> {
filter.exclude("**/*.class");
});
var manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().putValue("Minecraft-Dists", "server client");

try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outputJar))) {
filteredInput.visit(new ZipBuildingFileTreeVisitor(zos));
addSourceDistEntries(clientFiles, serverFiles, "client", mappings, manifest);
addSourceDistEntries(serverFiles, clientFiles, "server", mappings, manifest);

try (var zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(outputJar)))) {
zos.putNextEntry(FileUtils.getStableEntry(JarFile.MANIFEST_NAME));
manifest.write(zos);
zos.closeEntry();

// Generally ignore directories, manifests and class files
var clientEntries = clientZip.entries();
while (clientEntries.hasMoreElements()) {
var clientEntry = clientEntries.nextElement();
if (isResourceEntry(clientEntry)) {
zos.putNextEntry(clientEntry);
try (var clientIn = clientZip.getInputStream(clientEntry)) {
clientIn.transferTo(zos);
}
zos.closeEntry();
}
}
}
}
}

private static void addSourceDistEntries(Set<String> distFiles,
Set<String> otherDistFiles,
String dist,
IMappingFile mappings,
Manifest manifest) {
for (var file : distFiles) {
if (!otherDistFiles.contains(file)) {
var fileAttr = new Attributes(1);
fileAttr.putValue("Minecraft-Dist", dist);

if (mappings != null && file.endsWith(".class")) {
file = mappings.remapClass(file.substring(0, file.length() - ".class".length())) + ".class";
}
manifest.getEntries().put(file, fileAttr);
}
}
}

private Set<String> getFileIndex(ZipFile zipFile) throws IOException {
// Support "nested" ZIP-Files as in recent server jars
var embeddedVersionPath = readEmbeddedVersionPath(zipFile);
if (embeddedVersionPath != null) {
// Extract the embedded zip and instead index that
var versionJarEntry = zipFile.getEntry(embeddedVersionPath);
if (versionJarEntry == null) {
throw new IOException("version list in jar file " + zipFile + " refers to missing entry " + embeddedVersionPath);
}
var unbundledFile = new File(getTemporaryDir(), "unpacked.jar");
try (var in = zipFile.getInputStream(versionJarEntry)) {
Files.copy(in, unbundledFile.toPath());
}
try (ZipFile zf = new ZipFile(unbundledFile)) {
return getFileIndex(zf);
}
}

var result = new HashSet<String>(zipFile.size());

var entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory()) {
result.add(entry.getName());
}
}

return result;
}

private static boolean isResourceEntry(JarEntry entry) {
return !entry.getName().endsWith(".class")
&& !entry.isDirectory()
&& !entry.getName().equals(JarFile.MANIFEST_NAME)
&& !isSignatureFile(entry.getName());
}

private static boolean isSignatureFile(String name) {
return name.startsWith("META-INF/")
&& (
name.endsWith(".SF")
|| name.endsWith(".RSA")
|| name.endsWith(".EC")
|| name.endsWith(".DSA")
);
}

/**
* Server jars support embedding the actual jar file using an indirection via a version listing at
* META-INF/versions.list
* This method will try to read that list and return the path to the actual jar embedded in the bundle jar.
*/
@Nullable
private static String readEmbeddedVersionPath(ZipFile zipFile) throws IOException {
var entry = zipFile.getEntry("META-INF/versions.list");
if (entry == null) {
return null;
}

var entries = new ArrayList<String>();
try (var in = zipFile.getInputStream(entry)) {
for (var line : new String(in.readAllBytes()).split("\n")) {
if (line.isBlank()) {
continue;
}
String[] pts = line.split("\t");
if (pts.length != 3)
throw new IOException("Invalid file list line: " + line + " in " + zipFile);
entries.add(pts[2]);
}
}

if (entries.isEmpty()) {
return null;
} else if (entries.size() == 1) {
return "META-INF/versions/" + entries.get(0);
} else {
throw new IOException("Version file list contains more than one entry in " + zipFile);
}
}

@InputFile
@PathSensitive(PathSensitivity.NONE)
public abstract RegularFileProperty getOriginalJar();

@InputFile
@PathSensitive(PathSensitivity.NONE)
public abstract RegularFileProperty getServerJar();

@InputFile
@PathSensitive(PathSensitivity.NONE)
public abstract RegularFileProperty getMappings();
}
Loading