From 30937a4d96d86c6d61533a68a379ca304e68b6bc Mon Sep 17 00:00:00 2001 From: Geert Bevin Date: Mon, 20 Jul 2026 19:52:25 -0400 Subject: [PATCH] Add an instrument operation for ahead-of-time RIFE2 instrumentation --- src/main/java/rife/bld/Project.java | 24 +++ .../java/rife/bld/help/InstrumentHelp.java | 31 ++++ .../bld/operations/InstrumentOperation.java | 140 ++++++++++++++++++ .../operations/TestInstrumentOperation.java | 58 ++++++++ 4 files changed, 253 insertions(+) create mode 100644 src/main/java/rife/bld/help/InstrumentHelp.java create mode 100644 src/main/java/rife/bld/operations/InstrumentOperation.java create mode 100644 src/test/java/rife/bld/operations/TestInstrumentOperation.java diff --git a/src/main/java/rife/bld/Project.java b/src/main/java/rife/bld/Project.java index 4d49a37..1a25289 100644 --- a/src/main/java/rife/bld/Project.java +++ b/src/main/java/rife/bld/Project.java @@ -25,6 +25,7 @@ public class Project extends BaseProject { private final JavadocOperation javadocOperation_ = new JavadocOperation(); private final PrecompileOperation precompileOperation_ = new PrecompileOperation(); + private final InstrumentOperation instrumentOperation_ = new InstrumentOperation(); private final JarOperation jarOperation_ = new JarOperation(); private final JarOperation jarSourcesOperation_ = new JarOperation(); private final JarOperation jarJavadocOperation_ = new JarOperation(); @@ -56,6 +57,16 @@ public class Project extends BaseProject { return precompileOperation_; } + /** + * Retrieves the project's default instrument operation. + * + * @return the default instrument operation instance + * @since 2.4 + */ + public InstrumentOperation instrumentOperation() { + return instrumentOperation_; + } + /** * Retrieves the project's default jar operation. * @@ -125,6 +136,19 @@ public class Project extends BaseProject { precompileOperation().executeOnce(() -> precompileOperation().fromProject(this)); } + /** + * Standard build command, instruments the compiled classes ahead of + * time, as an alternative to the java agent. + * + * @since 2.4 + */ + @BuildCommand(help = InstrumentHelp.class) + public void instrument() + throws Exception { + compile(); + instrumentOperation().executeOnce(() -> instrumentOperation().fromProject(this)); + } + /** * Standard build command, creates a jar archive for the project. * diff --git a/src/main/java/rife/bld/help/InstrumentHelp.java b/src/main/java/rife/bld/help/InstrumentHelp.java new file mode 100644 index 0000000..2d33400 --- /dev/null +++ b/src/main/java/rife/bld/help/InstrumentHelp.java @@ -0,0 +1,31 @@ +/* + * Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com) + * Licensed under the Apache License, Version 2.0 (the "License") + */ +package rife.bld.help; + +import rife.bld.CommandHelp; +import rife.tools.StringUtils; + +/** + * Provides help for the instrument command. + * + * @author Geert Bevin (gbevin[remove] at uwyn dot com) + * @since 2.4 + */ +public class InstrumentHelp implements CommandHelp { + public String getSummary() { + return "Instruments compiled classes ahead of time, as an alternative to the java agent"; + } + + public String getDescription(String topic) { + return StringUtils.replace(""" + Instruments the compiled classes ahead of time with RIFE2's bytecode + transformations, as an alternative to the java agent: web engine + continuations, workflow continuations, meta-data merging and + lazy-loading. This makes the agent unnecessary at run time and + enables these capabilities inside a GraalVM native image. + + Usage : ${topic}""", "${topic}", topic); + } +} diff --git a/src/main/java/rife/bld/operations/InstrumentOperation.java b/src/main/java/rife/bld/operations/InstrumentOperation.java new file mode 100644 index 0000000..fe4f249 --- /dev/null +++ b/src/main/java/rife/bld/operations/InstrumentOperation.java @@ -0,0 +1,140 @@ +/* + * Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com) + * Licensed under the Apache License, Version 2.0 (the "License") + */ +package rife.bld.operations; + +import rife.bld.BaseProject; +import rife.tools.FileUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Instruments compiled classes ahead of time with RIFE2's bytecode + * transformations, as an alternative to the java agent. + *

The instrumentation is performed by + * {@code rife.instrument.InstrumentationDeployer} from the RIFE2 + * dependency of the project that is being built, so the same + * transformations are applied as the ones the agent of that RIFE2 version + * performs at class loading time: web engine continuations, workflow + * continuations, meta-data merging and lazy-loading. Classes that don't + * use any of these capabilities are left untouched, and instrumenting + * already instrumented classes makes no changes. + *

Ahead-of-time instrumented classes make the agent unnecessary at run + * time and are the way to use these capabilities inside a GraalVM native + * image. + * + * @author Geert Bevin (gbevin[remove] at uwyn dot com) + * @since 2.4 + */ +public class InstrumentOperation extends AbstractProcessOperation { + public static final String DEPLOYER_CLASS = "rife.instrument.InstrumentationDeployer"; + + private final List sourceDirectories_ = new ArrayList<>(); + private File destinationDirectory_; + + /** + * Part of the {@link #execute} operation, constructs the command list + * to use for building the process. + * + * @since 2.4 + */ + protected List executeConstructProcessCommandList() { + var args = new ArrayList(); + args.add(javaTool()); + args.addAll(javaOptions()); + + if (!classpath().isEmpty()) { + args.add("-cp"); + args.add(FileUtils.joinPaths(classpath())); + } + + args.add(DEPLOYER_CLASS); + args.add("-verbose"); + args.add("-d"); + args.add(destinationDirectory().getAbsolutePath()); + for (var directory : sourceDirectories()) { + args.add(directory.getAbsolutePath()); + } + + return args; + } + + /** + * Configures an instrument operation from a {@link BaseProject}. + *

The project's main build directory is instrumented in place, with + * the runtime classpath providing the RIFE2 dependency that performs + * the instrumentation. + * + * @param project the project to configure the instrument operation from + * @since 2.4 + */ + public InstrumentOperation fromProject(BaseProject project) { + return classpath(project.runClasspath()) + .sourceDirectories(project.buildMainDirectory()) + .destinationDirectory(project.buildMainDirectory()); + } + + /** + * Provides source directories that will be instrumented. + * + * @param directories source directories + * @return this operation instance + * @since 2.4 + */ + public InstrumentOperation sourceDirectories(File... directories) { + sourceDirectories_.addAll(List.of(directories)); + return this; + } + + /** + * Provides a list of source directories that will be instrumented. + *

A copy will be created to allow this list to be independently modifiable. + * + * @param directories a list of source directories + * @return this operation instance + * @since 2.4 + */ + public InstrumentOperation sourceDirectories(Collection directories) { + sourceDirectories_.addAll(directories); + return this; + } + + /** + * Provides the destination directory in which the instrumented classes + * will be stored. + * + * @param directory the instrumentation destination directory + * @return this operation instance + * @since 2.4 + */ + public InstrumentOperation destinationDirectory(File directory) { + destinationDirectory_ = directory; + return this; + } + + /** + * Retrieves the source directories that will be instrumented. + *

This is a modifiable list that can be retrieved and changed. + * + * @return the instrumentation source directories + * @since 2.4 + */ + public List sourceDirectories() { + return sourceDirectories_; + } + + /** + * Retrieves the destination directory in which the instrumented classes + * will be stored. + * + * @return the instrumentation destination directory + * @since 2.4 + */ + public File destinationDirectory() { + return destinationDirectory_; + } +} diff --git a/src/test/java/rife/bld/operations/TestInstrumentOperation.java b/src/test/java/rife/bld/operations/TestInstrumentOperation.java new file mode 100644 index 0000000..8d7e53e --- /dev/null +++ b/src/test/java/rife/bld/operations/TestInstrumentOperation.java @@ -0,0 +1,58 @@ +/* + * Copyright 2001-2026 Geert Bevin (gbevin[remove] at uwyn dot com) + * Licensed under the Apache License, Version 2.0 (the "License") + */ +package rife.bld.operations; + +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestInstrumentOperation { + @Test + void testInstantiation() { + var operation = new InstrumentOperation(); + assertEquals("java", operation.javaTool()); + assertTrue(operation.javaOptions().isEmpty()); + assertTrue(operation.classpath().isEmpty()); + assertTrue(operation.sourceDirectories().isEmpty()); + assertNull(operation.destinationDirectory()); + } + + @Test + void testPopulation() { + var source1 = new File("source1"); + var source2 = new File("source2"); + var destination = new File("destination"); + + var operation = new InstrumentOperation() + .classpath("classpath1", "classpath2") + .sourceDirectories(source1, source2) + .destinationDirectory(destination); + assertEquals(2, operation.classpath().size()); + assertEquals(2, operation.sourceDirectories().size()); + assertEquals(destination, operation.destinationDirectory()); + } + + @Test + void testProcessCommandList() { + var operation = new InstrumentOperation() + .classpath("cp1", "cp2") + .sourceDirectories(new File("classes"), new File("more-classes")) + .destinationDirectory(new File("instrumented")); + + var command = operation.executeConstructProcessCommandList(); + assertEquals("java", command.get(0)); + assertTrue(command.contains("-cp")); + assertTrue(command.contains("cp1" + File.pathSeparator + "cp2")); + assertTrue(command.contains(InstrumentOperation.DEPLOYER_CLASS)); + assertTrue(command.contains("-verbose")); + var d_index = command.indexOf("-d"); + assertTrue(d_index > 0); + assertEquals(new File("instrumented").getAbsolutePath(), command.get(d_index + 1)); + assertEquals(new File("classes").getAbsolutePath(), command.get(command.size() - 2)); + assertEquals(new File("more-classes").getAbsolutePath(), command.get(command.size() - 1)); + } +}