Skip to content

Commit

Permalink
Merge pull request #17 from mk868/maven-plugin
Browse files Browse the repository at this point in the history
Add cabe-maven-plugin
  • Loading branch information
xzel23 authored Dec 21, 2024
2 parents 9318be2 + 721243e commit a414cd4
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 3 deletions.
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

0 comments on commit a414cd4

Please sign in to comment.