22
33import net .neoforged .neoform .runtime .cache .CacheKeyBuilder ;
44import net .neoforged .neoform .runtime .engine .ProcessingEnvironment ;
5+ import net .neoforged .srgutils .IMappingFile ;
6+ import org .jetbrains .annotations .Nullable ;
57
6- import java .io .BufferedInputStream ;
78import java .io .BufferedOutputStream ;
89import java .io .IOException ;
910import java .nio .file .Files ;
11+ import java .nio .file .Path ;
12+ import java .time .LocalDateTime ;
1013import java .util .ArrayList ;
14+ import java .util .HashSet ;
1115import java .util .List ;
16+ import java .util .Objects ;
17+ import java .util .Set ;
1218import 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 ;
1521import java .util .jar .JarOutputStream ;
22+ import java .util .jar .Manifest ;
1623import java .util .regex .Pattern ;
1724import 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 */
2233public 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}
0 commit comments