Skip to content

Commit

Permalink
Rewrite build logic in Java
Browse files Browse the repository at this point in the history
  • Loading branch information
Juuxel committed Jul 23, 2024
1 parent a034c63 commit 61d137c
Show file tree
Hide file tree
Showing 28 changed files with 644 additions and 533 deletions.
25 changes: 16 additions & 9 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
plugins {
// The Kotlin DSL plugin sets up Kotlin with the correct version
// and lets me do .gradle.kts files as plugins.
// See https://docs.gradle.org/current/userguide/kotlin_dsl.html#sec:kotlin-dsl_plugin
`kotlin-dsl`
id("org.jmailen.kotlinter") version "3.2.0"
`java-gradle-plugin`
}

repositories {
Expand All @@ -20,10 +16,21 @@ java {
targetCompatibility = JavaVersion.VERSION_17
}

tasks {
compileKotlin {
kotlinOptions {
jvmTarget = "17"
gradlePlugin {
plugins {
register("adorn-data-generator") {
id = "adorn-data-generator"
implementationClass = "juuxel.adorn.gradle.DataGeneratorPlugin"
}

register("adorn-data-generator.emi") {
id = "adorn-data-generator.emi"
implementationClass = "juuxel.adorn.gradle.EmiDataGeneratorPlugin"
}

register("adorn-service-inline") {
id = "adorn-service-inline"
implementationClass = "juuxel.adorn.gradle.ServiceInlinePlugin"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package juuxel.adorn.gradle;

import juuxel.adorn.gradle.action.MinifyJson;
import juuxel.adorn.gradle.datagen.DataGeneratorExtension;
import juuxel.adorn.gradle.datagen.DeleteDuplicates;
import juuxel.adorn.gradle.datagen.GenerateData;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPluginExtension;

public final class DataGeneratorPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
var java = project.getExtensions().getByType(JavaPluginExtension.class);
var extension = project.getExtensions().create("dataGenerator", DataGeneratorExtension.class, project);
extension.getConfigs().register("main", config -> {
config.getFiles().from(project.fileTree("src/data").filter(file -> file.getName().endsWith(".xml")));
});

var generatedResources = project.getLayout().getProjectDirectory().dir("src/main/generatedResources");
var generateMainData = project.getTasks().register("generateMainData", GenerateData.class, task -> {
task.getConfigs().set(extension.getConfigs());
task.getGenerateTags().set(extension.getGenerateTags());
task.getOutput().convention(generatedResources);
});
var generateData = project.getTasks().register("generateData", task -> task.dependsOn(generateMainData));
project.getTasks().named("processResources", task -> task.mustRunAfter(generateData));
project.getTasks().register("deleteDuplicateResources", DeleteDuplicates.class, task -> {
task.getGenerated().convention(generatedResources);
task.getMain().convention(project.getLayout().dir(
java.getSourceSets()
.named("main")
.map(main -> main.getResources().getSrcDirs().iterator().next())
));
});
java.getSourceSets().named("main", main -> {
main.getResources().srcDir(generatedResources);
main.getResources().exclude(".cache");
});
project.afterEvaluate(p -> {
p.getTasks().named("remapJar", task -> task.doLast(new MinifyJson()));
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package juuxel.adorn.gradle;

import juuxel.adorn.gradle.datagen.EmiDataGeneratorExtension;
import juuxel.adorn.gradle.datagen.GenerateData;
import juuxel.adorn.gradle.datagen.GenerateEmi;
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public final class EmiDataGeneratorPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getExtensions().create("emiDataGenerator", EmiDataGeneratorExtension.class);
var generateEmi = project.getTasks().register("generateEmi", GenerateEmi.class, task -> {
var generateMainData = project.getTasks().named("generateMainData", GenerateData.class);
task.mustRunAfter(generateMainData);
task.getOutput().convention(generateMainData.flatMap(GenerateData::getOutput));
});
project.getTasks().named("generateData", task -> task.dependsOn(generateEmi));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package juuxel.adorn.gradle;

import juuxel.adorn.gradle.action.InlineServiceLoader;
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public final class ServiceInlinePlugin implements Plugin<Project> {
@Override
public void apply(Project target) {
target.getTasks().named("remapJar", task -> {
task.doLast(new InlineServiceLoader());
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package juuxel.adorn.gradle.action;

import juuxel.adorn.gradle.asm.Asm;
import juuxel.adorn.gradle.asm.InsnPattern;
import juuxel.adorn.gradle.asm.MethodBodyPattern;
import org.gradle.api.Action;
import org.gradle.api.Task;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;

public final class InlineServiceLoader implements Action<Task> {
private static final MethodBodyPattern INLINE_PATTERN = new MethodBodyPattern(
List.of(
new InsnPattern<>(LdcInsnNode.class, ldc -> ldc.cst instanceof Type),
new InsnPattern<>(
MethodInsnNode.class,
method -> method.getOpcode() == Opcodes.INVOKESTATIC
&& "java/util/ServiceLoader".equals(method.owner)
&& "load".equals(method.name)
&& "(Ljava/lang/Class;)Ljava/util/ServiceLoader;".equals(method.desc)
),
new InsnPattern<>(
MethodInsnNode.class,
method -> method.getOpcode() == Opcodes.INVOKEVIRTUAL
&& "java/util/ServiceLoader".equals(method.owner)
&& "findFirst".equals(method.name)
&& "()Ljava/util/Optional;".equals(method.desc)
)
)
);

@Override
public void execute(Task task) {
try {
run(task);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private void run(Task task) throws IOException {
Asm.transformJar(task.getOutputs().getFiles().getSingleFile().toPath(), (filer, classNode) -> {
List<AnnotationNode> invisibleAnnotations = Objects.requireNonNullElse(classNode.invisibleAnnotations, List.of());
if (invisibleAnnotations.stream().noneMatch(node -> "Ljuuxel/adorn/util/InlineServices;".equals(node.desc))) {
// Skip classes without the anno
return;
}

for (MethodNode method : classNode.methods) {
boolean success = INLINE_PATTERN.match(method, ctx -> {
var ldc = (LdcInsnNode) ctx.instructions().get(0);
var type = ((Type) ldc.cst).getInternalName().replace('/', '.');
var serviceFile = filer.apply("META-INF/services/" + type);
if (Files.exists(serviceFile)) {
try {
var serviceImpls = Files.readAllLines(serviceFile);
if (serviceImpls.size() != 1) throw new IllegalArgumentException("Service file " + type + " must have exactly one provider");
var implType = serviceImpls.get(0).replace('.', '/');
ctx.replaceWith(
List.of(
new TypeInsnNode(Opcodes.NEW, implType),
new InsnNode(Opcodes.DUP),
new MethodInsnNode(Opcodes.INVOKESPECIAL, implType, "<init>", "()V"),
new MethodInsnNode(
Opcodes.INVOKESTATIC,
"java/util/Optional",
"of",
"(Ljava/lang/Object;)Ljava/util/Optional;"
)
)
);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
});

if (success) {
method.maxStack++;
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package juuxel.adorn.gradle.action;

import groovy.json.JsonOutput;
import groovy.json.JsonSlurper;
import org.gradle.api.Action;
import org.gradle.api.Task;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

/**
* A simple action that you can add to a {@code Jar} task
* that minifies all JSON files using {@link JsonSlurper} and {@link JsonOutput}.
*/
public final class MinifyJson implements Action<Task> {
@Override
public void execute(Task task) {
var jar = task.getOutputs().getFiles().getSingleFile().toPath();
try (var fs = FileSystems.newFileSystem(URI.create("jar:" + jar.toUri()), Map.of("create", false))) {
for (Path root : fs.getRootDirectories()) {
try (var jsons = Files.walk(root).filter(it -> Files.isRegularFile(it) && it.toString().endsWith(".json"))) {
var iter = jsons.iterator();
while (iter.hasNext()) {
var json = iter.next();
Files.writeString(json, JsonOutput.toJson(new JsonSlurper().parse(json)));
}
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
49 changes: 49 additions & 0 deletions build-logic/src/main/java/juuxel/adorn/gradle/asm/Asm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package juuxel.adorn.gradle.asm;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;

import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;

public final class Asm {
public static void transformJar(Path jar, Transformer transformer) throws IOException {
try (var fs = FileSystems.newFileSystem(URI.create("jar:" + jar.toUri()), Map.of("create", false))) {
for (Path root : fs.getRootDirectories()) {
try (var classes = Files.walk(root).filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".class"))) {
var iter = classes.iterator();
while (iter.hasNext()) {
var c = iter.next();

// Read the existing class
ClassReader cr;
try (var in = Files.newInputStream(c)) {
cr = new ClassReader(in);
}
var node = new ClassNode();
cr.accept(node, 0);

// Transform
transformer.transform(fs::getPath, node);

// Write out the new class
var cw = new ClassWriter(cr, 0);
node.accept(cw);
Files.write(c, cw.toByteArray());
}
}
}
}
}

@FunctionalInterface
public interface Transformer {
void transform(Function<String, Path> filer, ClassNode classNode) throws IOException;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package juuxel.adorn.gradle.asm;

import org.objectweb.asm.tree.AbstractInsnNode;

import java.util.function.Predicate;

public record InsnPattern<T extends AbstractInsnNode>(Class<T> type, Predicate<T> filter) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package juuxel.adorn.gradle.asm;

import org.objectweb.asm.tree.AbstractInsnNode;

import java.util.List;

public interface MatchContext {
List<AbstractInsnNode> instructions();
void replaceWith(List<AbstractInsnNode> instructions);
}
Loading

0 comments on commit 61d137c

Please sign in to comment.