Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cabe-maven-plugin #17

Merged
merged 1 commit into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Writerside/topics/cabe.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ The instrumentation is done by the `ClassPatcher` class. A precompiled runnable
<verbosity> : 0 - show warnings and errors only (default)
: 1 - show basic processing information
: 2 - show detailed information
: 1 - show all information
: 3 - show all information
```

## What Java version is Cabe compatible with?
Expand Down
107 changes: 107 additions & 0 deletions cabe-maven-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import groovy.util.Node
import groovy.util.NodeList

plugins {
id("java-library")
id("maven-publish")
id("com.github.ben-manes.versions") version "0.50.0"
id("de.benediktritter.maven-plugin-development") version "0.4.3"
}

group = "com.dua3.cabe"
version = project.findProperty("plugin_version") as String? ?: project.version
description = "A plugin that adds assertions for annotated method parameters at compile time."

repositories {
mavenLocal()
mavenCentral()
}

dependencies {
var processor_version = rootProject.extra["processor_version"] as String
implementation("com.dua3.cabe:cabe-processor-all:${processor_version}")

compileOnlyApi("org.apache.maven:maven-plugin-api:3.9.9")
compileOnlyApi("org.apache.maven.plugin-tools:maven-plugin-annotations:3.15.1")
compileOnlyApi("org.apache.maven:maven-core:3.9.9")
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

/////////////////////////////////////////////////////////////////////////////
object Meta {
const val SCM = "https://github.com/xzel23/cabe.git"
const val REPO = "public"
const val LICENSE_NAME = "Apache License 2.0"
const val LICENSE_URL = "https://www.apache.org/licenses/LICENSE-2.0"
const val DEVELOPER_ID = "axh"
const val DEVELOPER_NAME = "Axel Howind"
const val DEVELOPER_EMAIL = "axh@dua3.com"
const val ORGANIZATION_NAME = "dua3"
const val ORGANIZATION_URL = "https://www.dua3.com"
}
/////////////////////////////////////////////////////////////////////////////

publishing {
publications {
create<MavenPublication>("maven") {
groupId = project.group as String?
artifactId = project.name
version = project.version.toString()

from(components["java"])

pom {
description.set(project.description)
packaging = "maven-plugin"

withXml {
val root = asNode()
val dependenciesNode = (root.get("dependencies") as NodeList)[0] as Node

// set maven dependencies with provided scope
dependenciesNode.children().forEach { dep ->
val dependencyNode = dep as Node
val artifactId =
((dependencyNode.get("artifactId") as NodeList)[0] as Node).text()
if (artifactId == "maven-plugin-api"
|| artifactId == "maven-plugin-annotations"
|| artifactId == "maven-core"
) {
val scopeNodes = (dependencyNode.get("scope") as NodeList)
if (scopeNodes.isEmpty()) {
dependencyNode.appendNode("scope", "provided")
} else {
(scopeNodes[0] as Node).setValue("provided")
}
}
}
}

licenses {
license {
name.set(Meta.LICENSE_NAME)
url.set(Meta.LICENSE_URL)
}
}
developers {
developer {
id.set(Meta.DEVELOPER_ID)
name.set(Meta.DEVELOPER_NAME)
email.set(Meta.DEVELOPER_EMAIL)
organization.set(Meta.ORGANIZATION_NAME)
organizationUrl.set(Meta.ORGANIZATION_URL)
}
}

scm {
url.set(Meta.SCM)
}
}
}
}
}

175 changes: 175 additions & 0 deletions cabe-maven-plugin/src/main/java/com/dua3/cabe/maven/CabeMojo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.dua3.cabe.maven;

import com.dua3.cabe.processor.ClassPatcher;
import java.io.BufferedReader;
import java.io.File;
import java.io.Reader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;

/**
* Cabe Maven goal definition
*/
@Mojo(name = "cabe", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class CabeMojo extends AbstractMojo {

@Parameter(defaultValue = "${project}", required = true, readonly = true)
private MavenProject project;
/**
* The verbosity level.
* <ul>
* <li> <b>0</b> - show warnings and errors only (default)
* <li> <b>1</b> - show basic processing information
* <li> <b>2</b> - show detailed information
* <li> <b>3</b> - show all information
* </ul>
*/
@Parameter(property = "cabe.verbosity")
private Integer verbosity;
/**
* The input directory for the Cabe processing
*/
@Parameter(property = "cabe.inputDirectory", defaultValue = "${project.build.outputDirectory}")
private Path inputDirectory;
/**
* The output directory for the Cabe processing
*/
@Parameter(property = "cabe.outputDirectory", defaultValue = "${project.build.outputDirectory}")
public Path outputDirectory;
/**
* The configuration string for the Cabe
* <ul>
* <li> <b>STANDARD</b> - use standard assertions for private API methods, throw NullPointerException for public API methods
* <li> <b>DEVELOPMENT</b> - failed checks will always throw an AssertionError, also checks return values
* <li> <b>NO_CHECKS</b> - do not add any null checks (class files are copied unchanged)
* <li> &lt;configstr&gt; - custom configuration string, please check documentation for details
* </ul>
*/
@Parameter(property = "cabe.configurationString", defaultValue = "STANDARD")
public String configurationString;

/**
* Default constructor
*/
public CabeMojo() {
}

@Override
public void execute() throws MojoExecutionException, MojoFailureException {
try {
String jarLocation = Paths.get(
ClassPatcher.class.getProtectionDomain().getCodeSource().getLocation().toURI())
.toString();
String systemClassPath = System.getProperty("java.class.path");

String classpath = project.getArtifacts().stream()
.map(Artifact::getFile)
.map(File::toString)
.distinct()
.collect(Collectors.joining(File.pathSeparator));

String javaExec = Path.of(System.getProperty("java.home"), "bin", "java").toString();
getLog().info("Java executable: %s".formatted(javaExec));

int v = Objects.requireNonNullElse(verbosity, 0);
String[] args = {
javaExec,
"-classpath", systemClassPath,
"-jar", jarLocation,
"-i", inputDirectory.toString(),
"-o", outputDirectory.toString(),
"-c", configurationString,
"-cp", classpath,
"-v", Integer.toString(v)
};

if (v > 0) {
getLog().debug("Instrumenting class files: %s".formatted(String.join(" ", args)));
}

getLog().info(String.join(" ", args));
ProcessBuilder pb = new ProcessBuilder(args);

Process process = pb.start();

try (CopyOutput copyStdErr = new CopyOutput(process.errorReader(), System.err::println);
CopyOutput ignored = new CopyOutput(process.inputReader(),
v > 1 ? System.out::println : s -> {
})) {
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new MojoFailureException("Instrumenting class files failed\n\n" + copyStdErr);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
throw new MojoFailureException(
"An error occurred while instrumenting classes: " + e.getMessage(), e);
}
}

/**
* This class is responsible for copying the output of a Reader to a specified Consumer. The first
* 10 lines are stored.
*/
private class CopyOutput implements AutoCloseable {

public static final int MAX_LINES = 10;
Thread thread;
List<String> firstLines = new ArrayList<>();

CopyOutput(Reader reader, Consumer<String> printer) {
thread = new Thread(() -> {
try (BufferedReader r = new BufferedReader(reader)) {
String line;
while ((line = r.readLine()) != null) {
printer.accept(line);
if (firstLines.size() < MAX_LINES) {
firstLines.add(line);
} else if (firstLines.size() == MAX_LINES) {
firstLines.add("...");
}
}
} catch (Exception e) {
getLog().warn("exception reading ClassPatcher error output");
}
});
thread.start();
}

@Override
public void close() {
try {
thread.join(5000); // Wait 5000ms for the thread to die.
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

if (thread.isAlive()) {
getLog().warn("output thread did not stop");
thread.interrupt();
}
}

@Override
public String toString() {
return String.join("\n", firstLines);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ private static void help() {
<verbosity> : 0 - show warnings and errors only (default)
: 1 - show basic processing information
: 2 - show detailed information
: 1 - show all information
: 3 - show all information
""";
System.out.println(msg);
}
Expand Down
13 changes: 13 additions & 0 deletions examples/hello-maven/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Examples: hello-maven

To build the project, use:

```
mvn package
```

To run the compiled example with instrumentation applied, use:

```
java -jar target/hello-maven.jar
```
76 changes: 76 additions & 0 deletions examples/hello-maven/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>hello-maven</artifactId>
<version>1.0.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

<build>
<finalName>hello-maven</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArguments>
<d>${project.build.directory}/unprocessed-classes</d>
</compilerArguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.dua3.cabe</groupId>
<artifactId>cabe-maven-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<inputDirectory>${project.build.directory}/unprocessed-classes</inputDirectory>
<verbosity>1</verbosity>
<configurationString>STANDARD</configurationString>
</configuration>
<executions>
<execution>
<goals>
<goal>cabe</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
<addClasspath>true</addClasspath>
<mainClass>hello.Hello</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Loading