Skip to content

Commit f770c88

Browse files
lukebemishshartte
andauthored
Implement generation of split-source Manifest entries (#50, updated) (#66)
This is a rebased version of #50 (made as a separate PR because I lack appropriate permissions to do otherwise); from a quick test, its behaviour seems to match that of neoforged/NeoGradle#257, putting the generated manifest info in the MANIFEST.MF of the client-extra jar. --------- Co-authored-by: Sebastian Hartte <[email protected]>
1 parent 5746aae commit f770c88

File tree

6 files changed

+216
-8
lines changed

6 files changed

+216
-8
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ idea {
163163
programParameters = "--help"
164164
moduleRef(project, sourceSets.main)
165165
}
166+
"Run Neoforge 1.21.6 (joined) + Parchment"(Application) {
167+
mainClass = mainClassName
168+
programParameters = "run --dist joined --neoforge net.neoforged:neoforge:21.6.1-beta:userdev --add-repository=https://maven.parchmentmc.org --parchment-data=org.parchmentmc.data:parchment-1.21.5:2025.06.15@zip --parchment-conflict-prefix=p_ --write-result=compiled:build/minecraft-1.21.6.jar --write-result=clientResources:build/client-extra-1.21.6.jar --write-result=sources:build/minecraft-sources-1.21.6.jar"
169+
moduleRef(project, sourceSets.main)
170+
}
166171
"Run Neoforge 1.21 (joined) + Parchment"(Application) {
167172
mainClass = mainClassName
168173
programParameters = "run --dist joined --neoforge net.neoforged:neoforge:21.0.0-beta:userdev --add-repository=https://maven.parchmentmc.org --parchment-data=org.parchmentmc.data:parchment-1.21:2024.06.23@zip --parchment-conflict-prefix=p_ --write-result=compiled:build/minecraft-1.21.jar --write-result=clientResources:build/client-extra-1.21.jar --write-result=sources:build/minecraft-sources-1.21.jar"

src/main/java/net/neoforged/neoform/runtime/actions/SplitResourcesFromClassesAction.java

Lines changed: 160 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,74 @@
22

33
import net.neoforged.neoform.runtime.cache.CacheKeyBuilder;
44
import net.neoforged.neoform.runtime.engine.ProcessingEnvironment;
5+
import net.neoforged.srgutils.IMappingFile;
6+
import org.jetbrains.annotations.Nullable;
57

6-
import java.io.BufferedInputStream;
78
import java.io.BufferedOutputStream;
89
import java.io.IOException;
910
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.time.LocalDateTime;
1013
import java.util.ArrayList;
14+
import java.util.HashSet;
1115
import java.util.List;
16+
import java.util.Objects;
17+
import java.util.Set;
1218
import java.util.function.Predicate;
13-
import java.util.jar.JarEntry;
14-
import java.util.jar.JarInputStream;
19+
import java.util.jar.Attributes;
20+
import java.util.jar.JarFile;
1521
import java.util.jar.JarOutputStream;
22+
import java.util.jar.Manifest;
1623
import java.util.regex.Pattern;
1724
import java.util.stream.Collectors;
25+
import java.util.zip.ZipEntry;
26+
import java.util.zip.ZipFile;
1827

1928
/**
2029
* Copies a Jar file while applying a filename filter.
30+
* <p>Optionally, this also {@link #generateSplitManifest creates and injects} a {@code MANIFEST.MF} file that details files that are exclusive
31+
* to the distribution of Minecraft being processed by this action.
2132
*/
2233
public final class SplitResourcesFromClassesAction extends BuiltInAction {
34+
35+
/**
36+
* @see #generateSplitManifest
37+
*/
38+
public static final String INPUT_OTHER_DIST_JAR = "otherDistJar";
39+
/**
40+
* @see #generateSplitManifest
41+
*/
42+
public static final String INPUT_MAPPINGS = "mappings";
43+
44+
/**
45+
* Use a fixed timestamp for the manifest entry.
46+
*/
47+
private static final LocalDateTime MANIFEST_TIME = LocalDateTime.of(2000, 1, 1, 0, 0, 0, 0);
48+
2349
/**
2450
* Patterns for filenames that should not be written to either output jar.
2551
*/
2652
private final List<Pattern> denyListPatterns = new ArrayList<>();
2753

54+
/**
55+
* When non-null, the action expects additional inputs ({@link #INPUT_OTHER_DIST_JAR} and {@link #INPUT_MAPPINGS})
56+
* pointing to the Jar file of the *other* distribution (i.e. this action processes the client resources,
57+
* then the other distribution jar is the server jar).
58+
* The mapping file is required to produce a Manifest using named file names instead of obfuscated names.
59+
*/
60+
@Nullable
61+
private GenerateDistManifestSettings generateDistManifestSettings;
62+
2863
@Override
2964
public void run(ProcessingEnvironment environment) throws IOException, InterruptedException {
3065
var inputJar = environment.getRequiredInputPath("input");
66+
Path otherDistJarPath = null;
67+
Path mappingsPath = null;
68+
if (generateDistManifestSettings != null) {
69+
otherDistJarPath = environment.getRequiredInputPath(INPUT_OTHER_DIST_JAR);
70+
mappingsPath = environment.getRequiredInputPath(INPUT_MAPPINGS);
71+
}
72+
3173
var classesJar = environment.getOutputPath("output");
3274
var resourcesJar = environment.getOutputPath("resourcesOutput");
3375

@@ -39,19 +81,35 @@ public void run(ProcessingEnvironment environment) throws IOException, Interrupt
3981
.asMatchPredicate();
4082
}
4183

42-
try (var is = new JarInputStream(new BufferedInputStream(Files.newInputStream(inputJar)));
84+
try (var jar = new ZipFile(inputJar.toFile());
4385
var classesFileOut = new BufferedOutputStream(Files.newOutputStream(classesJar));
4486
var resourcesFileOut = new BufferedOutputStream(Files.newOutputStream(resourcesJar));
4587
var classesJarOut = new JarOutputStream(classesFileOut);
4688
var resourcesJarOut = new JarOutputStream(resourcesFileOut);
4789
) {
48-
// Ignore any entry that's not allowed
49-
JarEntry entry;
50-
while ((entry = is.getNextJarEntry()) != null) {
90+
if (generateDistManifestSettings != null) {
91+
generateDistSourceManifest(
92+
generateDistManifestSettings.distId(),
93+
jar,
94+
generateDistManifestSettings.otherDistId(),
95+
otherDistJarPath,
96+
mappingsPath,
97+
resourcesJarOut
98+
);
99+
}
100+
101+
var entries = jar.entries();
102+
while (entries.hasMoreElements()) {
103+
var entry = entries.nextElement();
51104
if (entry.isDirectory()) {
52105
continue; // For simplicity, we ignore directories completely
53106
}
54107

108+
// If this task generates its own manifest, ignore any manifests found in the input jar
109+
if (generateDistManifestSettings != null && entry.getName().equals(JarFile.MANIFEST_NAME)) {
110+
continue;
111+
}
112+
55113
var filename = entry.getName();
56114

57115
// Skip anything that looks like a signature file
@@ -62,12 +120,77 @@ public void run(ProcessingEnvironment environment) throws IOException, Interrupt
62120
var destinationStream = filename.endsWith(".class") ? classesJarOut : resourcesJarOut;
63121

64122
destinationStream.putNextEntry(entry);
65-
is.transferTo(destinationStream);
123+
try (var is = jar.getInputStream(entry)) {
124+
is.transferTo(destinationStream);
125+
}
66126
destinationStream.closeEntry();
67127
}
68128
}
69129
}
70130

131+
private static void generateDistSourceManifest(String distId,
132+
ZipFile jar,
133+
String otherDistId,
134+
Path otherDistJarPath,
135+
Path mappingsPath,
136+
JarOutputStream resourcesJarOut) throws IOException {
137+
var mappings = mappingsPath != null ? IMappingFile.load(mappingsPath.toFile()) : null;
138+
139+
// Use the time-stamp of either of the two input files (whichever is newer)
140+
var ourFiles = getFileIndex(jar);
141+
ourFiles.remove(JarFile.MANIFEST_NAME);
142+
Set<String> theirFiles;
143+
try (var otherDistJar = new ZipFile(otherDistJarPath.toFile())) {
144+
theirFiles = getFileIndex(otherDistJar);
145+
}
146+
theirFiles.remove(JarFile.MANIFEST_NAME);
147+
148+
var manifest = new Manifest();
149+
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
150+
manifest.getMainAttributes().putValue("Minecraft-Dists", distId + " " + otherDistId);
151+
152+
addSourceDistEntries(ourFiles, theirFiles, distId, mappings, manifest);
153+
addSourceDistEntries(theirFiles, ourFiles, otherDistId, mappings, manifest);
154+
155+
var manifestEntry = new ZipEntry(JarFile.MANIFEST_NAME);
156+
manifestEntry.setTimeLocal(MANIFEST_TIME);
157+
resourcesJarOut.putNextEntry(manifestEntry);
158+
manifest.write(resourcesJarOut);
159+
resourcesJarOut.closeEntry();
160+
}
161+
162+
private static void addSourceDistEntries(Set<String> distFiles,
163+
Set<String> otherDistFiles,
164+
String dist,
165+
IMappingFile mappings,
166+
Manifest manifest) {
167+
for (var file : distFiles) {
168+
if (!otherDistFiles.contains(file)) {
169+
var fileAttr = new Attributes(1);
170+
fileAttr.putValue("Minecraft-Dist", dist);
171+
172+
if (mappings != null && file.endsWith(".class")) {
173+
file = mappings.remapClass(file.substring(0, file.length() - ".class".length())) + ".class";
174+
}
175+
manifest.getEntries().put(file, fileAttr);
176+
}
177+
}
178+
}
179+
180+
private static Set<String> getFileIndex(ZipFile zipFile) {
181+
var result = new HashSet<String>(zipFile.size());
182+
183+
var entries = zipFile.entries();
184+
while (entries.hasMoreElements()) {
185+
ZipEntry entry = entries.nextElement();
186+
if (!entry.isDirectory()) {
187+
result.add(entry.getName());
188+
}
189+
}
190+
191+
return result;
192+
}
193+
71194
/**
72195
* Adds a regular expression for filenames that should be filtered out completely.
73196
*/
@@ -77,9 +200,38 @@ public void addDenyPatterns(String... patterns) {
77200
}
78201
}
79202

203+
/**
204+
* Enable generation of a Jar manifest in the output resources jar which contains
205+
* entries detailing which distribution each file came from.
206+
* <p>This adds required inputs {@link #INPUT_MAPPINGS} and {@link #INPUT_OTHER_DIST_JAR} to this action.
207+
* <p>Common values for distributions are {@code client} and {@code server}.
208+
*
209+
* @param distId The name for the distribution that the main input file is from. It is used in the
210+
* generated manifest for files that are only present in the main input, but not in the
211+
* {@linkplain #INPUT_OTHER_DIST_JAR jar file of the other distribution}.
212+
* @param otherDistId The name for the Minecraft distribution for the jar file given in {@link #INPUT_OTHER_DIST_JAR}.
213+
* It is used in the generated manifest for files that are only present in that jar file.
214+
*/
215+
public void generateSplitManifest(String distId, String otherDistId) {
216+
generateDistManifestSettings = new GenerateDistManifestSettings(
217+
Objects.requireNonNull(distId, "distId"),
218+
Objects.requireNonNull(otherDistId, "otherDistId")
219+
);
220+
}
221+
80222
@Override
81223
public void computeCacheKey(CacheKeyBuilder ck) {
82224
super.computeCacheKey(ck);
83225
ck.addStrings("deny patterns", denyListPatterns.stream().map(Pattern::pattern).toList());
226+
if (generateDistManifestSettings != null) {
227+
ck.add("generate dist manifest - our dist", generateDistManifestSettings.distId);
228+
ck.add("generate dist manifest - other dist", generateDistManifestSettings.otherDistId);
229+
}
230+
}
231+
232+
private record GenerateDistManifestSettings(
233+
String distId,
234+
String otherDistId
235+
) {
84236
}
85237
}

src/main/java/net/neoforged/neoform/runtime/config/neoform/NeoFormDistConfig.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public NeoFormDistConfig(NeoFormConfig config, String dist) {
2121
this.dist = dist;
2222
}
2323

24+
public String dist() {
25+
return dist;
26+
}
27+
2428
public int javaVersion() {
2529
return config.javaVersion();
2630
}

src/main/java/net/neoforged/neoform/runtime/engine/NeoFormEngine.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,26 @@ private void addNodeForStep(ExecutionGraph graph, NeoFormDistConfig config, NeoF
324324
var action = new SplitResourcesFromClassesAction();
325325
// The Minecraft jar contains nothing of interest in META-INF, and the signature files are useless.
326326
action.addDenyPatterns("META-INF/.*");
327+
328+
// When generating Minecraft artifacts that join the client and server, we generate a MANIFEST.MF that
329+
// indicates files exclusive to one or the other. This started in Minecraft 1.21.6.
330+
if (processGeneration.generateDistSourceManifest() && config.dist().equals("joined")) {
331+
if ("stripClient".equals(step.getId())) {
332+
// Prefer the already extracted server, otherwise download it
333+
var serverJarInput = graph.hasOutput("extractServer", "output") ?
334+
graph.getRequiredOutput("extractServer", "output").asInput()
335+
: graph.getRequiredOutput("downloadServer", "output").asInput();
336+
337+
action.generateSplitManifest("client", "server");
338+
builder.input(SplitResourcesFromClassesAction.INPUT_OTHER_DIST_JAR, serverJarInput);
339+
builder.input(SplitResourcesFromClassesAction.INPUT_MAPPINGS, graph.getRequiredOutput("mergeMappings", "output").asInput());
340+
} else if ("stripServer".equals(step.getId())) {
341+
action.generateSplitManifest("server", "client");
342+
builder.input(SplitResourcesFromClassesAction.INPUT_OTHER_DIST_JAR, graph.getRequiredOutput("downloadClient", "output").asInput());
343+
builder.input(SplitResourcesFromClassesAction.INPUT_MAPPINGS, graph.getRequiredOutput("mergeMappings", "output").asInput());
344+
}
345+
}
346+
327347
processGeneration.getAdditionalDenyListForMinecraftJars().forEach(action::addDenyPatterns);
328348
builder.action(action);
329349
}
@@ -401,6 +421,7 @@ private void applyFunctionToNode(NeoFormStep step, NeoFormFunction function, Exe
401421
if ("output".equals(variable)) {
402422
var type = switch (step.type()) {
403423
case "mergeMappings" -> NodeOutputType.TSRG;
424+
case "generateSplitManifest" -> NodeOutputType.JAR_MANIFEST;
404425
default -> NodeOutputType.JAR;
405426
};
406427
if (!builder.hasOutput(variable)) {

src/main/java/net/neoforged/neoform/runtime/engine/ProcessGeneration.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public int compareTo(@NotNull ProcessGeneration.MinecraftReleaseVersion o) {
4343

4444
private static final MinecraftReleaseVersion MC_1_17_1 = new MinecraftReleaseVersion(1, 17, 1);
4545
private static final MinecraftReleaseVersion MC_1_20_1 = new MinecraftReleaseVersion(1, 20, 1);
46+
private static final MinecraftReleaseVersion MC_1_21_6 = new MinecraftReleaseVersion(1, 21, 6);
4647

4748
/**
4849
* Indicates whether the Minecraft server jar file contains third party
@@ -64,6 +65,12 @@ public int compareTo(@NotNull ProcessGeneration.MinecraftReleaseVersion o) {
6465
*/
6566
private boolean supportsSideAnnotationStripping;
6667

68+
/**
69+
* Enables generation of the MANIFEST.MF in the client and server resource files that
70+
* indicates which distribution each file came from. Only applies to joined distributions.
71+
*/
72+
private boolean generateDistSourceManifest;
73+
6774
/**
6875
* For (Neo)Forge 1.20.1 and below, we have to remap method and field names from
6976
* SRG to official names for development.
@@ -94,6 +101,9 @@ static ProcessGeneration fromMinecraftVersion(String minecraftVersion) {
94101
// In 1.20.2 and later, NeoForge switched to Mojmap at runtime and sources defined in Mojmap
95102
result.sourcesUseIntermediaryNames = isLessThanOrEqualTo(releaseVersion, MC_1_20_1);
96103

104+
// In 1.21.6 and later, manifest entries should be generated as they may be used instead of RuntimeDistCleaner
105+
result.generateDistSourceManifest = isGreaterThanOrEqualTo(releaseVersion, MC_1_21_6);
106+
97107
result.supportsSideAnnotationStripping = isLessThanOrEqualTo(releaseVersion, MC_1_20_1);
98108

99109
return result;
@@ -106,6 +116,13 @@ private static boolean isLessThanOrEqualTo(@Nullable MinecraftReleaseVersion rel
106116
return releaseVersion.compareTo(version) <= 0;
107117
}
108118

119+
private static boolean isGreaterThanOrEqualTo(@Nullable MinecraftReleaseVersion releaseVersion, MinecraftReleaseVersion version) {
120+
if (releaseVersion == null) {
121+
return true; // We're working with a snapshot version, which we always use the latest processes for
122+
}
123+
return releaseVersion.compareTo(version) >= 0;
124+
}
125+
109126
/**
110127
* Does the Minecraft source code that MCP/NeoForm creates use SRG names?
111128
*/
@@ -120,6 +137,14 @@ public boolean supportsSideAnnotationStripping() {
120137
return supportsSideAnnotationStripping;
121138
}
122139

140+
/**
141+
* Does the FML version on that MC generation support use of MANIFEST.MF entries
142+
* for filtering out dist-specific classes in dev? (When using the joined distribution)
143+
*/
144+
public boolean generateDistSourceManifest() {
145+
return generateDistSourceManifest;
146+
}
147+
123148
/**
124149
* Allows additional resources to be completely removed from Minecraft jars before processing them.
125150
*/

src/main/java/net/neoforged/neoform/runtime/graph/NodeOutputType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public enum NodeOutputType {
44
JAR(".jar"),
5+
JAR_MANIFEST(".MF"),
56
TXT(".txt"),
67
ZIP(".zip"),
78
JSON(".json"),

0 commit comments

Comments
 (0)