Skip to content
2 changes: 2 additions & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
dependencies {
implementation 'net.neoforged.gradleutils:net.neoforged.gradleutils.gradle.plugin:5.0.0'
implementation 'org.ow2.asm:asm:9.7'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't asm on 9.8 already?

Copy link
Contributor Author

@shartte shartte Jul 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but we're compiling with JDK8 here. (I didn't actually check what the minimum java version for apache compress is hmmm).

edit Okay, Apache Compress also says "The current release requires Java 8 or above."

implementation 'org.ow2.asm:asm-commons:9.7'
}

repositories {
Expand Down
7 changes: 1 addition & 6 deletions buildSrc/src/main/java/CliToolPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,15 @@
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaApplication;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;
import org.gradle.api.tasks.bundling.Jar;

import java.util.Map;

public class CliToolPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(ShadowDefaultsPlugin.class);
project.getPlugins().apply("application");
project.getPlugins().apply("com.gradleup.shadow");

project.getTasks().named("shadowJar", AbstractArchiveTask.class, task -> {
task.getArchiveClassifier().set("fatjar");
});

var applicationExtension = project.getExtensions().getByType(JavaApplication.class);
project.getTasks().named("jar", Jar.class, task -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Set;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
* For the Zip Transform library, we let ProGuard obfuscate all third-party dependencies and move them
* into the same package as the library implementation.
* Since the library only has a single package, we can make all third party classes package-private,
* but sadly ProGuard has no optimization for this.
*/
public abstract class MakeObfuscatedClassesPackageVisibleTask extends DefaultTask {
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public abstract ConfigurableFileCollection getInputFiles();

@Input
public abstract SetProperty<String> getClassWhitelist();

@OutputFile
public abstract RegularFileProperty getOutputFile();

@TaskAction
public void run() throws IOException {

Set<String> classWhitelist = getClassWhitelist().get();

try (var zf = new ZipFile(getInputFiles().getSingleFile());
var out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(getOutputFile().getAsFile().get())))) {
var entries = zf.entries();
while (entries.hasMoreElements()) {
var entry = entries.nextElement();

if (entry.isDirectory()) {
out.putNextEntry(entry);
out.closeEntry();
continue;
}

byte[] entryData;
try (var entryIn = zf.getInputStream(entry)) {
entryData = entryIn.readAllBytes();
}

// Clear the access flags from classes that don't match our whitelist.
if (entry.getName().endsWith(".class") && !classWhitelist.contains(entry.getName())) {
ClassReader classReader = new ClassReader(entryData);
ClassWriter classWriter = new ClassWriter(0);

ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9, classWriter) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
access &= ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE);
super.visit(version, access, name, signature, superName, interfaces);
}
};

classReader.accept(classVisitor, 0);
entryData = classWriter.toByteArray();
}

out.putNextEntry(entry);
out.write(entryData);
out.closeEntry();
}
}

}
}
14 changes: 14 additions & 0 deletions buildSrc/src/main/java/ShadowDefaultsPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.tasks.bundling.AbstractArchiveTask;

public class ShadowDefaultsPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply("com.gradleup.shadow");

project.getTasks().named("shadowJar", AbstractArchiveTask.class, task -> {
task.getArchiveClassifier().set("fatjar");
});
}
}
3 changes: 3 additions & 0 deletions jarsplitter/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ application {
mainClass = 'net.neoforged.jarsplitter.ConsoleTool'
}

evaluationDependsOn(":ziptransform")

dependencies {
implementation(libs.jopt)
implementation(libs.srgutils)
implementation(project(path: ":ziptransform", configuration: "proguard"))
implementation project(':cli-utils')
}

Expand Down
102 changes: 49 additions & 53 deletions jarsplitter/src/main/java/net/neoforged/jarsplitter/ConsoleTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@
package net.neoforged.jarsplitter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import joptsimple.OptionException;
import joptsimple.OptionParser;
Expand All @@ -28,12 +24,12 @@
import net.neoforged.cliutils.JarUtils;
import net.neoforged.cliutils.progress.ProgressManager;
import net.neoforged.cliutils.progress.ProgressReporter;
import net.neoforged.jartransform.ZipInput;
import net.neoforged.jartransform.ZipOutput;
import net.neoforged.jartransform.ZipTransformEntry;
import net.neoforged.srgutils.IMappingFile;

public class ConsoleTool {
private static final OutputStream NULL_OUTPUT = new OutputStream() {
@Override public void write(int b) {}
};
private static final boolean DEBUG = Boolean.getBoolean("net.neoforged.jarsplitter.debug");
private static final ProgressManager PROGRESS = ProgressReporter.getDefault();

Expand All @@ -51,8 +47,8 @@ public static void main(String[] args) throws IOException {
OptionSet options = parser.parse(args);

File input = options.valueOf(inputO);
File slim = options.valueOf(slimO);
File data = options.has(dataO) ? options.valueOf(dataO) : null;
File slim = options.valueOf(slimO);
File data = options.has(dataO) ? options.valueOf(dataO) : null;
File extra = options.has(extraO) ? options.valueOf(extraO) : null;
boolean merge = data == null;

Expand All @@ -67,7 +63,7 @@ public static void main(String[] args) throws IOException {
Set<String> whitelist = new HashSet<>();

if (options.has(srgO)) {
for(File dir : options.valuesOf(srgO)) {
for (File dir : options.valuesOf(srgO)) {
log(" SRG: " + dir);
IMappingFile srg = IMappingFile.load(dir);
srg.getClasses().forEach(c -> whitelist.add(c.getOriginal()));
Expand All @@ -82,7 +78,8 @@ public static void main(String[] args) throws IOException {
slim = checkOutput("Slim", slim, inputSha, srgSha);
data = checkOutput("Data", data, inputSha, srgSha);
if (extra != null) {
if (whitelist.isEmpty()) throw new IllegalArgumentException("--extra argument specified with no --srg class list");
if (whitelist.isEmpty())
throw new IllegalArgumentException("--extra argument specified with no --srg class list");
extra = checkOutput("Extra", extra, inputSha, srgSha, merge ? "\nMerge: true" : null);
} else if (merge) {
throw new IllegalArgumentException("You must specify --extra if you do not specify --data");
Expand All @@ -98,33 +95,34 @@ public static void main(String[] args) throws IOException {
log("Splitting " + fileAmount + " files:");

int amount = 0;
try (ZipInputStream zinput = new ZipInputStream(Files.newInputStream(input.toPath()));
ZipOutputStream zslim = new ZipOutputStream(slim == null ? NULL_OUTPUT : new FileOutputStream(slim));
ZipOutputStream zdata = new ZipOutputStream(data == null ? NULL_OUTPUT : new FileOutputStream(data));
ZipOutputStream zextra = new ZipOutputStream(extra == null ? NULL_OUTPUT : new FileOutputStream(extra))) {

ZipEntry entry;
while ((entry = zinput.getNextEntry()) != null) {
if (entry.getName().endsWith(".class")) {
String key = entry.getName().substring(0, entry.getName().length() - 6); //String .class

if (whitelist.isEmpty() || whitelist.contains(key)) {
debug(" Slim " + entry.getName());
copy(entry, zinput, zslim);
} else {
debug(" Extra " + entry.getName());
copy(entry, zinput, zextra);
}
} else {
debug(" Data " + entry.getName());
copy(entry, zinput, merge ? zextra : zdata);
}

// To avoid spam, only change the progress every 10 files processed
if ((++amount) % 10 == 0) {
PROGRESS.setProgress(amount);
}
}
try (ZipInput zinput = new ZipInput(input);
ZipOutput zslim = slim != null ? new ZipOutput(slim) : null;
ZipOutput zdata = data != null ? new ZipOutput(data) : null;
ZipOutput zextra = extra != null ? new ZipOutput(extra) : null) {

Iterator<ZipTransformEntry> entries = zinput.getEntries();
while (entries.hasNext()) {
ZipTransformEntry entry = entries.next();
if (entry.getName().endsWith(".class")) {
String key = entry.getName().substring(0, entry.getName().length() - 6); //String .class

if (whitelist.isEmpty() || whitelist.contains(key)) {
debug(" Slim " + entry.getName());
copy(entry, zinput, zslim);
} else {
debug(" Extra " + entry.getName());
copy(entry, zinput, zextra);
}
} else {
debug(" Data " + entry.getName());
copy(entry, zinput, merge ? zextra : zdata);
}

// To avoid spam, only change the progress every 10 files processed
if ((++amount) % 10 == 0) {
PROGRESS.setProgress(amount);
}
}
}
PROGRESS.setProgress(fileAmount);

Expand All @@ -137,24 +135,20 @@ public static void main(String[] args) throws IOException {
}
}

private static byte[] BUFFER = new byte[1024];
private static void copy(ZipEntry entry, InputStream input, ZipOutputStream output) throws IOException {
ZipEntry _new = new ZipEntry(entry.getName());
_new.setTime(628041600000L); //Java8 screws up on 0 time, so use another static time.
output.putNextEntry(_new);

int read = -1;
while ((read = input.read(BUFFER)) != -1)
output.write(BUFFER, 0, read);
private static void copy(ZipTransformEntry entry, ZipInput input, ZipOutput output) throws IOException {
if (output == null) {
return;
}
input.transferEntry(entry, output);
}

private static void writeCache(File file, String inputSha, String srgSha) throws IOException {
if (file == null) return;

File cacheFile = new File(file.getAbsolutePath() + ".cache");
byte[] cache = ("Input: " + inputSha + "\n" +
"Srg: " + srgSha + "\n" +
"Output: " + sha1(file, false)).getBytes();
"Srg: " + srgSha + "\n" +
"Output: " + sha1(file, false)).getBytes();
Files.write(cacheFile.toPath(), cache);
}

Expand All @@ -170,9 +164,9 @@ private static File checkOutput(String name, File file, String inputSha, String
if (cacheFile.exists()) {
byte[] data = Files.readAllBytes(cacheFile.toPath());
byte[] cache = ("Input: " + inputSha + "\n" +
"Srg: " + srgSha + "\n" +
"Output: " + sha1(file, false) +
(extra == null ? "" : extra)).getBytes(); // Reading from disc is less costly/destructive then writing. So we can verify the output hasn't changed.
"Srg: " + srgSha + "\n" +
"Output: " + sha1(file, false) +
(extra == null ? "" : extra)).getBytes(); // Reading from disc is less costly/destructive then writing. So we can verify the output hasn't changed.

if (Arrays.equals(cache, data) && file.exists()) {
log(" " + name + " Cache Hit");
Expand Down Expand Up @@ -211,6 +205,8 @@ private static String sha1(Set<String> data) {
}
}

private static final byte[] BUFFER = new byte[8192];

private static String sha1(File path, boolean allowCache) throws IOException {
if (!path.exists())
return "missing";
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencyResolutionManagement {
library('assert4j', 'org.assertj:assertj-core:3.27.3')
bundle('junit', ['junit-engine', 'junit-platform-launcher', 'junit-api', 'assert4j'])

library('commons-compress', 'org.apache.commons:commons-compress:1.27.1')
library('srgutils', 'net.neoforged:srgutils:1.0.0')
library('jopt', 'net.sf.jopt-simple:jopt-simple:5.0.4')
library('xz', 'org.tukaani:xz:1.10')
Expand All @@ -57,5 +58,6 @@ include(':jarsplitter')
include(':binarypatcher')
include(':zipinject')
include(':problems-api')
include(':ziptransform')

rootProject.name = 'installertools'
Loading
Loading