Skip to content

Commit

Permalink
Add task to generate ats (#1719)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt authored Dec 7, 2024
1 parent e59a37f commit 00f3f80
Show file tree
Hide file tree
Showing 15 changed files with 823 additions and 231 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/check-local-changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ jobs:
- name: Gen package infos
run: ./gradlew generatePackageInfos

- name: Gen patches
run: ./gradlew :neoforge:genPatches
- name: Gen patches and ATs
run: ./gradlew :neoforge:genPatches :neoforge:generateAccessTransformers

- name: Run datagen with Gradle
run: ./gradlew :neoforge:runData :tests:runData
Expand Down
2 changes: 2 additions & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ dependencies {

implementation "com.google.code.gson:gson:${gradle.parent.ext.gson_version}"
implementation "io.codechicken:DiffPatch:${gradle.parent.ext.diffpatch_version}"

implementation "org.ow2.asm:asm:${gradle.parent.ext.asm_version}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.OutputFile;
Expand All @@ -17,6 +18,8 @@
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;

/**
* Runs <a href="https://github.com/neoforged/JavaSourceTransformer">JavaSourceTransformer</a> to apply
Expand All @@ -28,8 +31,8 @@ abstract class ApplyAccessTransformer extends JavaExec {
@InputFile
public abstract RegularFileProperty getInputJar();

@InputFile
public abstract RegularFileProperty getAccessTransformer();
@InputFiles
public abstract ConfigurableFileCollection getAccessTransformers();

@Input
public abstract Property<Boolean> getValidate();
Expand Down Expand Up @@ -59,13 +62,23 @@ public void exec() {
throw new UncheckedIOException("Failed to write libraries for JST.", exception);
}

args(
var args = new ArrayList<>(Arrays.asList(
"--enable-accesstransformers",
"--access-transformer", getAccessTransformer().getAsFile().get().getAbsolutePath(),
"--access-transformer-validation", getValidate().get() ? "error" : "log",
"--libraries-list", getLibrariesFile().getAsFile().get().getAbsolutePath(),
"--libraries-list", getLibrariesFile().getAsFile().get().getAbsolutePath()
));

for (var file : getAccessTransformers().getFiles()) {
args.addAll(Arrays.asList(
"--access-transformer", file.getAbsolutePath()
));
}

args.addAll(Arrays.asList(
getInputJar().getAsFile().get().getAbsolutePath(),
getOutputJar().getAsFile().get().getAbsolutePath());
getOutputJar().getAsFile().get().getAbsolutePath()));

args(args);

super.exec();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package net.neoforged.neodev;

import net.neoforged.neodev.utils.FileUtils;
import net.neoforged.neodev.utils.SerializablePredicate;
import net.neoforged.neodev.utils.structure.ClassInfo;
import net.neoforged.neodev.utils.structure.ClassStructureVisitor;
import net.neoforged.neodev.utils.structure.FieldInfo;
import net.neoforged.neodev.utils.structure.MethodInfo;
import org.gradle.api.DefaultTask;
import org.gradle.api.Named;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.objectweb.asm.Opcodes;

import javax.annotation.Nullable;
import java.io.IOException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
* This task is used to generate access transformers based on a set of rules defined in the buildscript.
*/
public abstract class GenerateAccessTransformers extends DefaultTask {
public static final Modifier PUBLIC = new Modifier("public", false, Opcodes.ACC_PUBLIC);
public static final Modifier PROTECTED = new Modifier("protected", false, Opcodes.ACC_PUBLIC, Opcodes.ACC_PROTECTED);

@InputFile
public abstract RegularFileProperty getInput();

@OutputFile
public abstract RegularFileProperty getAccessTransformer();

@Input
public abstract ListProperty<AtGroup> getGroups();

@TaskAction
public void exec() throws IOException {
// First we collect all classes
var targets = ClassStructureVisitor.readJar(getInput().getAsFile().get());

var groupList = getGroups().get();

List<String>[] groups = new List[groupList.size()];
for (int i = 0; i < groupList.size(); i++) {
groups[i] = new ArrayList<>();
}

// Now we check each class against each group and see if the group wants to handle it
for (ClassInfo value : targets.values()) {
for (int i = 0; i < groupList.size(); i++) {
var group = groupList.get(i);
if (group.classMatch.test(value)) {
var lastInner = value.name().lastIndexOf("$");
// Skip anonymous classes
if (lastInner >= 0 && Character.isDigit(value.name().charAt(lastInner + 1))) {
continue;
}

// fieldMatch is non-null only for field ATs
if (group.fieldMatch != null) {
for (var field : value.fields()) {
if (group.fieldMatch.test(field) && !group.modifier.test(field.access())) {
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.') + " " + field.name());
}
}
}
// methodMatch is non-null only for group ATs
else if (group.methodMatch != null) {
for (var method : value.methods()) {
if (group.methodMatch.test(method) && !group.modifier.test(method.access())) {
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.') + " " + method.name() + method.descriptor());
}
}
}
// If there's neither a field nor a method predicate, this is a class AT
else if (!group.modifier.test(value.access().intValue())) {
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.'));

// If we AT a record we must ensure that its constructors have the same AT
if (value.hasSuperclass("java/lang/Record")) {
for (MethodInfo method : value.methods()) {
if (method.name().equals("<init>")) {
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.') + " " + method.name() + method.descriptor());
}
}
}
}
}
}
}

// Dump the ATs
var text = new StringBuilder();

text.append("# This file is generated based on the rules defined in the buildscript. DO NOT modify it manually.\n# Add more rules in the buildscript and then run the generateAccessTransformers task to update this file.\n\n");

for (int i = 0; i < groups.length; i++) {
// Check if the group found no targets. If it didn't, there's probably an error in the test and it should be reported
if (groups[i].isEmpty()) {
throw new IllegalStateException("Generated AT group '" + groupList.get(i).name + "' found no entries!");
}
text.append("# ").append(groupList.get(i).name).append('\n');
text.append(groups[i].stream().sorted().collect(Collectors.joining("\n")));
text.append('\n');

if (i < groups.length - 1) text.append('\n');
}

var outFile = getAccessTransformer().getAsFile().get().toPath();
if (!Files.exists(outFile.getParent())) {
Files.createDirectories(outFile.getParent());
}

FileUtils.writeStringSafe(outFile, text.toString(), StandardCharsets.UTF_8);
}

public void classGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> match) {
getGroups().add(new AtGroup(name, modifier, match, null, null));
}

public void methodGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> targetTest, SerializablePredicate<MethodInfo> methodTest) {
getGroups().add(new AtGroup(name, modifier, targetTest, methodTest, null));
}

public void fieldGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> targetTest, SerializablePredicate<FieldInfo> fieldTest) {
getGroups().add(new AtGroup(name, modifier, targetTest, null, fieldTest));
}

public <T extends Named> SerializablePredicate<T> named(String name) {
return target -> target.getName().equals(name);
}

public SerializablePredicate<ClassInfo> classesWithSuperclass(String superClass) {
return target -> target.hasSuperclass(superClass);
}

public SerializablePredicate<ClassInfo> innerClassesOf(String parent) {
var parentFullName = parent + "$";
return target -> target.name().startsWith(parentFullName);
}

public SerializablePredicate<MethodInfo> methodsReturning(String type) {
var endMatch = ")L" + type + ";";
return methodInfo -> methodInfo.descriptor().endsWith(endMatch);
}

public SerializablePredicate<FieldInfo> fieldsOfType(SerializablePredicate<ClassInfo> type) {
return value -> type.test(value.type());
}

public <T> SerializablePredicate<T> matchAny() {
return value -> true;
}

public record AtGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> classMatch,
@Nullable SerializablePredicate<MethodInfo> methodMatch, @Nullable SerializablePredicate<FieldInfo> fieldMatch) implements Serializable {
}

public record Modifier(String name, boolean isFinal, int... validOpcodes) implements Serializable {
public boolean test(int value) {
if (isFinal && (value & Opcodes.ACC_FINAL) == 0) return false;

for (int validOpcode : validOpcodes) {
if ((value & validOpcode) != 0) {
return true;
}
}
return false;
}
}
}
51 changes: 33 additions & 18 deletions buildSrc/src/main/java/net/neoforged/neodev/NeoDevPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,42 @@ public void apply(Project project) {
// Task must run on sync to have MC resources available for IDEA nondelegated builds.
NeoDevFacade.runTaskOnProjectSync(project, createSourceArtifacts);

// Obtain clean binary artifacts, needed to be able to generate ATs
var createCleanArtifacts = tasks.register("createCleanArtifacts", CreateCleanArtifacts.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.setDescription("This task retrieves various files for the Minecraft version without applying NeoForge patches to them");
var cleanArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts/clean"));
task.getRawClientJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-client.jar")));
task.getCleanClientJar().set(cleanArtifactsDir.map(dir -> dir.file("client.jar")));
task.getRawServerJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-server.jar")));
task.getCleanServerJar().set(cleanArtifactsDir.map(dir -> dir.file("server.jar")));
task.getCleanJoinedJar().set(cleanArtifactsDir.map(dir -> dir.file("joined.jar")));
task.getMergedMappings().set(cleanArtifactsDir.map(dir -> dir.file("merged-mappings.txt")));
task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
});

var genAts = project.getRootProject().file("src/main/resources/META-INF/accesstransformergenerated.cfg");

var genAtsTask = tasks.register("generateAccessTransformers", GenerateAccessTransformers.class, task -> {
task.setGroup(GROUP);
task.setDescription("Generate access transformers based on a set of rules defined in the buildscript");
task.getInput().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanJoinedJar));
task.getAccessTransformer().set(genAts);
});

// 2. Apply AT to the source jar from 1.
var atFile = project.getRootProject().file("src/main/resources/META-INF/accesstransformer.cfg");
var atFiles = List.of(
project.getRootProject().file("src/main/resources/META-INF/accesstransformer.cfg"),
genAts
);
var applyAt = configureAccessTransformer(
project,
configurations,
createSourceArtifacts,
neoDevBuildDir,
atFile);
atFiles);

applyAt.configure(task -> task.mustRunAfter(genAtsTask));

// 3. Apply patches to the source jar from 2.
var patchesFolder = project.getRootProject().file("patches");
Expand Down Expand Up @@ -212,19 +240,6 @@ public void apply(Project project) {
jarJarTask.configure(task -> task.setGroup(INTERNAL_GROUP));
universalJar.configure(task -> task.from(jarJarTask));

var createCleanArtifacts = tasks.register("createCleanArtifacts", CreateCleanArtifacts.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.setDescription("This task retrieves various files for the Minecraft version without applying NeoForge patches to them");
var cleanArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts/clean"));
task.getRawClientJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-client.jar")));
task.getCleanClientJar().set(cleanArtifactsDir.map(dir -> dir.file("client.jar")));
task.getRawServerJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-server.jar")));
task.getCleanServerJar().set(cleanArtifactsDir.map(dir -> dir.file("server.jar")));
task.getCleanJoinedJar().set(cleanArtifactsDir.map(dir -> dir.file("joined.jar")));
task.getMergedMappings().set(cleanArtifactsDir.map(dir -> dir.file("merged-mappings.txt")));
task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
});

var binaryPatchOutputs = configureBinaryPatchCreation(
project,
configurations,
Expand Down Expand Up @@ -390,7 +405,7 @@ public void apply(Project project) {
task.from(writeUserDevConfig.flatMap(CreateUserDevConfig::getUserDevConfig), spec -> {
spec.rename(s -> "config.json");
});
task.from(atFile, spec -> {
task.from(atFiles, spec -> {
spec.into("ats/");
});
task.from(binaryPatchOutputs.binaryPatchesForMerged(), spec -> {
Expand Down Expand Up @@ -434,15 +449,15 @@ private static TaskProvider<ApplyAccessTransformer> configureAccessTransformer(
NeoDevConfigurations configurations,
TaskProvider<CreateMinecraftArtifacts> createSourceArtifacts,
Provider<Directory> neoDevBuildDir,
File atFile) {
List<File> atFiles) {

// Pass -PvalidateAccessTransformers to validate ATs.
var validateAts = project.getProviders().gradleProperty("validateAccessTransformers").map(p -> true).orElse(false);
return project.getTasks().register("applyAccessTransformer", ApplyAccessTransformer.class, task -> {
task.setGroup(INTERNAL_GROUP);
task.classpath(configurations.getExecutableTool(Tools.JST));
task.getInputJar().set(createSourceArtifacts.flatMap(CreateMinecraftArtifacts::getSourcesArtifact));
task.getAccessTransformer().set(atFile);
task.getAccessTransformers().from(atFiles);
task.getValidate().set(validateAts);
task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("artifacts/access-transformed-sources.jar")));
task.getLibraries().from(configurations.neoFormClasspath);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.neoforged.neodev.utils;

import java.io.Serializable;
import java.util.function.Predicate;

@FunctionalInterface
public interface SerializablePredicate<T> extends Serializable, Predicate<T> {
@Override
boolean test(T value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.neoforged.neodev.utils.structure;

import org.apache.commons.lang3.mutable.MutableInt;
import org.gradle.api.Named;

import java.util.List;

public record ClassInfo(String name, MutableInt access, List<ClassInfo> parents, List<MethodInfo> methods,
List<FieldInfo> fields) implements Named {
public void addMethod(String name, String desc, int access) {
this.methods.add(new MethodInfo(name, desc, access));
}

public boolean hasSuperclass(String name) {
for (ClassInfo parent : parents) {
if (parent.hasSuperclass(name)) {
return true;
}
}
return this.name.equals(name);
}

@Override
public String getName() {
return name;
}
}
Loading

0 comments on commit 00f3f80

Please sign in to comment.