Skip to content

Commit 00f3f80

Browse files
authored
Add task to generate ats (#1719)
1 parent e59a37f commit 00f3f80

15 files changed

+823
-231
lines changed

Diff for: .github/workflows/check-local-changes.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ jobs:
4040
- name: Gen package infos
4141
run: ./gradlew generatePackageInfos
4242

43-
- name: Gen patches
44-
run: ./gradlew :neoforge:genPatches
43+
- name: Gen patches and ATs
44+
run: ./gradlew :neoforge:genPatches :neoforge:generateAccessTransformers
4545

4646
- name: Run datagen with Gradle
4747
run: ./gradlew :neoforge:runData :tests:runData

Diff for: buildSrc/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ dependencies {
2323

2424
implementation "com.google.code.gson:gson:${gradle.parent.ext.gson_version}"
2525
implementation "io.codechicken:DiffPatch:${gradle.parent.ext.diffpatch_version}"
26+
27+
implementation "org.ow2.asm:asm:${gradle.parent.ext.asm_version}"
2628
}

Diff for: buildSrc/src/main/java/net/neoforged/neodev/ApplyAccessTransformer.java

+19-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.gradle.api.tasks.Classpath;
88
import org.gradle.api.tasks.Input;
99
import org.gradle.api.tasks.InputFile;
10+
import org.gradle.api.tasks.InputFiles;
1011
import org.gradle.api.tasks.Internal;
1112
import org.gradle.api.tasks.JavaExec;
1213
import org.gradle.api.tasks.OutputFile;
@@ -17,6 +18,8 @@
1718
import java.io.IOException;
1819
import java.io.UncheckedIOException;
1920
import java.nio.charset.StandardCharsets;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
2023

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

31-
@InputFile
32-
public abstract RegularFileProperty getAccessTransformer();
34+
@InputFiles
35+
public abstract ConfigurableFileCollection getAccessTransformers();
3336

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

62-
args(
65+
var args = new ArrayList<>(Arrays.asList(
6366
"--enable-accesstransformers",
64-
"--access-transformer", getAccessTransformer().getAsFile().get().getAbsolutePath(),
6567
"--access-transformer-validation", getValidate().get() ? "error" : "log",
66-
"--libraries-list", getLibrariesFile().getAsFile().get().getAbsolutePath(),
68+
"--libraries-list", getLibrariesFile().getAsFile().get().getAbsolutePath()
69+
));
70+
71+
for (var file : getAccessTransformers().getFiles()) {
72+
args.addAll(Arrays.asList(
73+
"--access-transformer", file.getAbsolutePath()
74+
));
75+
}
76+
77+
args.addAll(Arrays.asList(
6778
getInputJar().getAsFile().get().getAbsolutePath(),
68-
getOutputJar().getAsFile().get().getAbsolutePath());
79+
getOutputJar().getAsFile().get().getAbsolutePath()));
80+
81+
args(args);
6982

7083
super.exec();
7184
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package net.neoforged.neodev;
2+
3+
import net.neoforged.neodev.utils.FileUtils;
4+
import net.neoforged.neodev.utils.SerializablePredicate;
5+
import net.neoforged.neodev.utils.structure.ClassInfo;
6+
import net.neoforged.neodev.utils.structure.ClassStructureVisitor;
7+
import net.neoforged.neodev.utils.structure.FieldInfo;
8+
import net.neoforged.neodev.utils.structure.MethodInfo;
9+
import org.gradle.api.DefaultTask;
10+
import org.gradle.api.Named;
11+
import org.gradle.api.file.RegularFileProperty;
12+
import org.gradle.api.provider.ListProperty;
13+
import org.gradle.api.tasks.Input;
14+
import org.gradle.api.tasks.InputFile;
15+
import org.gradle.api.tasks.OutputFile;
16+
import org.gradle.api.tasks.TaskAction;
17+
import org.objectweb.asm.Opcodes;
18+
19+
import javax.annotation.Nullable;
20+
import java.io.IOException;
21+
import java.io.Serializable;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.stream.Collectors;
27+
28+
/**
29+
* This task is used to generate access transformers based on a set of rules defined in the buildscript.
30+
*/
31+
public abstract class GenerateAccessTransformers extends DefaultTask {
32+
public static final Modifier PUBLIC = new Modifier("public", false, Opcodes.ACC_PUBLIC);
33+
public static final Modifier PROTECTED = new Modifier("protected", false, Opcodes.ACC_PUBLIC, Opcodes.ACC_PROTECTED);
34+
35+
@InputFile
36+
public abstract RegularFileProperty getInput();
37+
38+
@OutputFile
39+
public abstract RegularFileProperty getAccessTransformer();
40+
41+
@Input
42+
public abstract ListProperty<AtGroup> getGroups();
43+
44+
@TaskAction
45+
public void exec() throws IOException {
46+
// First we collect all classes
47+
var targets = ClassStructureVisitor.readJar(getInput().getAsFile().get());
48+
49+
var groupList = getGroups().get();
50+
51+
List<String>[] groups = new List[groupList.size()];
52+
for (int i = 0; i < groupList.size(); i++) {
53+
groups[i] = new ArrayList<>();
54+
}
55+
56+
// Now we check each class against each group and see if the group wants to handle it
57+
for (ClassInfo value : targets.values()) {
58+
for (int i = 0; i < groupList.size(); i++) {
59+
var group = groupList.get(i);
60+
if (group.classMatch.test(value)) {
61+
var lastInner = value.name().lastIndexOf("$");
62+
// Skip anonymous classes
63+
if (lastInner >= 0 && Character.isDigit(value.name().charAt(lastInner + 1))) {
64+
continue;
65+
}
66+
67+
// fieldMatch is non-null only for field ATs
68+
if (group.fieldMatch != null) {
69+
for (var field : value.fields()) {
70+
if (group.fieldMatch.test(field) && !group.modifier.test(field.access())) {
71+
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.') + " " + field.name());
72+
}
73+
}
74+
}
75+
// methodMatch is non-null only for group ATs
76+
else if (group.methodMatch != null) {
77+
for (var method : value.methods()) {
78+
if (group.methodMatch.test(method) && !group.modifier.test(method.access())) {
79+
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.') + " " + method.name() + method.descriptor());
80+
}
81+
}
82+
}
83+
// If there's neither a field nor a method predicate, this is a class AT
84+
else if (!group.modifier.test(value.access().intValue())) {
85+
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.'));
86+
87+
// If we AT a record we must ensure that its constructors have the same AT
88+
if (value.hasSuperclass("java/lang/Record")) {
89+
for (MethodInfo method : value.methods()) {
90+
if (method.name().equals("<init>")) {
91+
groups[i].add(group.modifier.name + " " + value.name().replace('/', '.') + " " + method.name() + method.descriptor());
92+
}
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
100+
// Dump the ATs
101+
var text = new StringBuilder();
102+
103+
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");
104+
105+
for (int i = 0; i < groups.length; i++) {
106+
// Check if the group found no targets. If it didn't, there's probably an error in the test and it should be reported
107+
if (groups[i].isEmpty()) {
108+
throw new IllegalStateException("Generated AT group '" + groupList.get(i).name + "' found no entries!");
109+
}
110+
text.append("# ").append(groupList.get(i).name).append('\n');
111+
text.append(groups[i].stream().sorted().collect(Collectors.joining("\n")));
112+
text.append('\n');
113+
114+
if (i < groups.length - 1) text.append('\n');
115+
}
116+
117+
var outFile = getAccessTransformer().getAsFile().get().toPath();
118+
if (!Files.exists(outFile.getParent())) {
119+
Files.createDirectories(outFile.getParent());
120+
}
121+
122+
FileUtils.writeStringSafe(outFile, text.toString(), StandardCharsets.UTF_8);
123+
}
124+
125+
public void classGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> match) {
126+
getGroups().add(new AtGroup(name, modifier, match, null, null));
127+
}
128+
129+
public void methodGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> targetTest, SerializablePredicate<MethodInfo> methodTest) {
130+
getGroups().add(new AtGroup(name, modifier, targetTest, methodTest, null));
131+
}
132+
133+
public void fieldGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> targetTest, SerializablePredicate<FieldInfo> fieldTest) {
134+
getGroups().add(new AtGroup(name, modifier, targetTest, null, fieldTest));
135+
}
136+
137+
public <T extends Named> SerializablePredicate<T> named(String name) {
138+
return target -> target.getName().equals(name);
139+
}
140+
141+
public SerializablePredicate<ClassInfo> classesWithSuperclass(String superClass) {
142+
return target -> target.hasSuperclass(superClass);
143+
}
144+
145+
public SerializablePredicate<ClassInfo> innerClassesOf(String parent) {
146+
var parentFullName = parent + "$";
147+
return target -> target.name().startsWith(parentFullName);
148+
}
149+
150+
public SerializablePredicate<MethodInfo> methodsReturning(String type) {
151+
var endMatch = ")L" + type + ";";
152+
return methodInfo -> methodInfo.descriptor().endsWith(endMatch);
153+
}
154+
155+
public SerializablePredicate<FieldInfo> fieldsOfType(SerializablePredicate<ClassInfo> type) {
156+
return value -> type.test(value.type());
157+
}
158+
159+
public <T> SerializablePredicate<T> matchAny() {
160+
return value -> true;
161+
}
162+
163+
public record AtGroup(String name, Modifier modifier, SerializablePredicate<ClassInfo> classMatch,
164+
@Nullable SerializablePredicate<MethodInfo> methodMatch, @Nullable SerializablePredicate<FieldInfo> fieldMatch) implements Serializable {
165+
}
166+
167+
public record Modifier(String name, boolean isFinal, int... validOpcodes) implements Serializable {
168+
public boolean test(int value) {
169+
if (isFinal && (value & Opcodes.ACC_FINAL) == 0) return false;
170+
171+
for (int validOpcode : validOpcodes) {
172+
if ((value & validOpcode) != 0) {
173+
return true;
174+
}
175+
}
176+
return false;
177+
}
178+
}
179+
}

Diff for: buildSrc/src/main/java/net/neoforged/neodev/NeoDevPlugin.java

+33-18
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,42 @@ public void apply(Project project) {
7070
// Task must run on sync to have MC resources available for IDEA nondelegated builds.
7171
NeoDevFacade.runTaskOnProjectSync(project, createSourceArtifacts);
7272

73+
// Obtain clean binary artifacts, needed to be able to generate ATs
74+
var createCleanArtifacts = tasks.register("createCleanArtifacts", CreateCleanArtifacts.class, task -> {
75+
task.setGroup(INTERNAL_GROUP);
76+
task.setDescription("This task retrieves various files for the Minecraft version without applying NeoForge patches to them");
77+
var cleanArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts/clean"));
78+
task.getRawClientJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-client.jar")));
79+
task.getCleanClientJar().set(cleanArtifactsDir.map(dir -> dir.file("client.jar")));
80+
task.getRawServerJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-server.jar")));
81+
task.getCleanServerJar().set(cleanArtifactsDir.map(dir -> dir.file("server.jar")));
82+
task.getCleanJoinedJar().set(cleanArtifactsDir.map(dir -> dir.file("joined.jar")));
83+
task.getMergedMappings().set(cleanArtifactsDir.map(dir -> dir.file("merged-mappings.txt")));
84+
task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
85+
});
86+
87+
var genAts = project.getRootProject().file("src/main/resources/META-INF/accesstransformergenerated.cfg");
88+
89+
var genAtsTask = tasks.register("generateAccessTransformers", GenerateAccessTransformers.class, task -> {
90+
task.setGroup(GROUP);
91+
task.setDescription("Generate access transformers based on a set of rules defined in the buildscript");
92+
task.getInput().set(createCleanArtifacts.flatMap(CreateCleanArtifacts::getCleanJoinedJar));
93+
task.getAccessTransformer().set(genAts);
94+
});
95+
7396
// 2. Apply AT to the source jar from 1.
74-
var atFile = project.getRootProject().file("src/main/resources/META-INF/accesstransformer.cfg");
97+
var atFiles = List.of(
98+
project.getRootProject().file("src/main/resources/META-INF/accesstransformer.cfg"),
99+
genAts
100+
);
75101
var applyAt = configureAccessTransformer(
76102
project,
77103
configurations,
78104
createSourceArtifacts,
79105
neoDevBuildDir,
80-
atFile);
106+
atFiles);
107+
108+
applyAt.configure(task -> task.mustRunAfter(genAtsTask));
81109

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

215-
var createCleanArtifacts = tasks.register("createCleanArtifacts", CreateCleanArtifacts.class, task -> {
216-
task.setGroup(INTERNAL_GROUP);
217-
task.setDescription("This task retrieves various files for the Minecraft version without applying NeoForge patches to them");
218-
var cleanArtifactsDir = neoDevBuildDir.map(dir -> dir.dir("artifacts/clean"));
219-
task.getRawClientJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-client.jar")));
220-
task.getCleanClientJar().set(cleanArtifactsDir.map(dir -> dir.file("client.jar")));
221-
task.getRawServerJar().set(cleanArtifactsDir.map(dir -> dir.file("raw-server.jar")));
222-
task.getCleanServerJar().set(cleanArtifactsDir.map(dir -> dir.file("server.jar")));
223-
task.getCleanJoinedJar().set(cleanArtifactsDir.map(dir -> dir.file("joined.jar")));
224-
task.getMergedMappings().set(cleanArtifactsDir.map(dir -> dir.file("merged-mappings.txt")));
225-
task.getNeoFormArtifact().set(mcAndNeoFormVersion.map(version -> "net.neoforged:neoform:" + version + "@zip"));
226-
});
227-
228243
var binaryPatchOutputs = configureBinaryPatchCreation(
229244
project,
230245
configurations,
@@ -390,7 +405,7 @@ public void apply(Project project) {
390405
task.from(writeUserDevConfig.flatMap(CreateUserDevConfig::getUserDevConfig), spec -> {
391406
spec.rename(s -> "config.json");
392407
});
393-
task.from(atFile, spec -> {
408+
task.from(atFiles, spec -> {
394409
spec.into("ats/");
395410
});
396411
task.from(binaryPatchOutputs.binaryPatchesForMerged(), spec -> {
@@ -434,15 +449,15 @@ private static TaskProvider<ApplyAccessTransformer> configureAccessTransformer(
434449
NeoDevConfigurations configurations,
435450
TaskProvider<CreateMinecraftArtifacts> createSourceArtifacts,
436451
Provider<Directory> neoDevBuildDir,
437-
File atFile) {
452+
List<File> atFiles) {
438453

439454
// Pass -PvalidateAccessTransformers to validate ATs.
440455
var validateAts = project.getProviders().gradleProperty("validateAccessTransformers").map(p -> true).orElse(false);
441456
return project.getTasks().register("applyAccessTransformer", ApplyAccessTransformer.class, task -> {
442457
task.setGroup(INTERNAL_GROUP);
443458
task.classpath(configurations.getExecutableTool(Tools.JST));
444459
task.getInputJar().set(createSourceArtifacts.flatMap(CreateMinecraftArtifacts::getSourcesArtifact));
445-
task.getAccessTransformer().set(atFile);
460+
task.getAccessTransformers().from(atFiles);
446461
task.getValidate().set(validateAts);
447462
task.getOutputJar().set(neoDevBuildDir.map(dir -> dir.file("artifacts/access-transformed-sources.jar")));
448463
task.getLibraries().from(configurations.neoFormClasspath);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package net.neoforged.neodev.utils;
2+
3+
import java.io.Serializable;
4+
import java.util.function.Predicate;
5+
6+
@FunctionalInterface
7+
public interface SerializablePredicate<T> extends Serializable, Predicate<T> {
8+
@Override
9+
boolean test(T value);
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package net.neoforged.neodev.utils.structure;
2+
3+
import org.apache.commons.lang3.mutable.MutableInt;
4+
import org.gradle.api.Named;
5+
6+
import java.util.List;
7+
8+
public record ClassInfo(String name, MutableInt access, List<ClassInfo> parents, List<MethodInfo> methods,
9+
List<FieldInfo> fields) implements Named {
10+
public void addMethod(String name, String desc, int access) {
11+
this.methods.add(new MethodInfo(name, desc, access));
12+
}
13+
14+
public boolean hasSuperclass(String name) {
15+
for (ClassInfo parent : parents) {
16+
if (parent.hasSuperclass(name)) {
17+
return true;
18+
}
19+
}
20+
return this.name.equals(name);
21+
}
22+
23+
@Override
24+
public String getName() {
25+
return name;
26+
}
27+
}

0 commit comments

Comments
 (0)