Omit class file from jar in gradle - java

I'm writing a gradle plugin that modifies class files. I want the task to run on all the generated class files and replace them. Here's what I have so far:
public class InstrumenterTask extends DefaultTask {
private File outputDir;
private SourceSet sourceSet;
public void setOutputDir(File outputDir) {
this.outputDir = outputDir;
}
public void setSourceSet(SourceSet sourceSet) {
this.sourceSet = sourceSet;
}
#TaskAction
public void processSourceSet() {
File classesDir = sourceSet.getOutput().getClassesDir();
Path classesPath = classesDir.toPath();
Files.walk(classesPath).forEach(this::processClassFile);
}
private void processClassFile(File inputFile) {
// Omitted for brevity. Result is in outputDir
}
}
and my buildscript
apply plugin: 'java'
apply plugin: 'application'
mainClassName = 'user.Main'
jar {
manifest {
attributes 'Main-Class': 'user.Main'
}
}
task(mainInstrumenter, type: InstrumenterTask) {
outputDir = new File(buildDir, 'instrumentedClasses')
sourceSet = sourceSets.main
}
mainInstrumenter.dependsOn classes
jar.dependsOn mainInstrumenter
sourceSets.main.output.dir new File(buildDir, 'instrumentedClasses')
The problem now is that in the compiled jar, the class is there two times. How can I tell gradle not to include the classes in the default classes dir?
Alternatively, can I tell gradle to compile the classes to a different directory and put my classes in the default location?

You can simply, at the end of your task action, call
sourceSet.getOutput().setClassesDir(outputDir);
and store your outputs in outputDir. All the following, dependant tasks will pick up on the newly set classesDir, especially jar and run when the task is a dependency of them.

Related

How can I use Gradle to configure different environments using a shared source set(main)?

I am trying to use Gradle Source Sets to configure different files for different environments, which is inspired by Gradles integration testing setup documentation.
My goal is to have both fake and prod source sets use interfaces defined in main and provide their own implementations. This is similar if not exactly what you can accomplish with the Android Gradle Plugin's Product Flavors.
For example, here is my build.gradle.kts file:
...
sourceSets {
create("prod") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
java.srcDirs("src/main/kotlin", "src/prod/kotlin")
}
create("fake") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
java.srcDirs("src/main/kotlin", "src/fake/kotlin")
}
}
val fakeImplementation by configurations.getting {
extendsFrom(configurations.implementation.get())
}
val prodImplementation by configurations.getting {
extendsFrom(configurations.implementation.get())
}
configurations["fakeRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())
configurations["prodRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())
val prodTest = task<Test>("prodTest") {
description = "Runs prod tests."
group = "verification"
testClassesDirs = sourceSets["prod"].output.classesDirs
classpath = sourceSets["prod"].runtimeClasspath
shouldRunAfter("test")
}
val fakeTest = task<Test>("fakeTest") {
description = "Runs fake tests."
group = "verification"
testClassesDirs = sourceSets["fake"].output.classesDirs
classpath = sourceSets["fake"].runtimeClasspath
shouldRunAfter("test")
}
tasks.check { dependsOn(prodTest) }
tasks.check { dependsOn(fakeTest) }
...
compileFakeKotlin doesn't recognize files in the fake source set, particularly on the kaptKotlin task which is defined like so in my dependencies block:
dependencies {
// Dagger
implementation("com.google.dagger:dagger:2.41")
kapt("com.google.dagger:dagger-compiler:2.41")
"kaptProd"("com.google.dagger:dagger-compiler:2.41")
"kaptFake"("com.google.dagger:dagger-compiler:2.41")
}
And here is the error:
> Task :server:compileKotlinCheckIncrementalCompilationAnvil UP-TO-DATE
> Task :server:kaptGenerateStubsKotlin UP-TO-DATE
> Task :server:processResources UP-TO-DATE
> Task :server:compileFakeKotlinCheckIncrementalCompilationAnvil UP-TO-DATE
> Task :server:kaptKotlin FAILED
...[Dagger/MissingBinding] my.package.name.RecipeController cannot be provided without an #Provides-annotated method.
public abstract interface AppComponent { ...
When I put the binding class(FakeRecipeController.kt) in the main source set, it works perfectly. But when it's in src/fake/kotlin, I get this error which leads me to believe the code in that source set isn't being used and I'm unsure how to proceed from here. Thank you.

Trying to run a test for a java class in Spock framework but I get this weird error about the constructor

This is the error I get when I try to create the class under test
Could not find matching constructor for: com.pittacode.apihelper.json.JsonObjectFlattener()
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.pittacode.apihelper.json.JsonObjectFlattener() at com.pittacode.apihelper.json.JsonObjectFlattenerTest.flatten json object with one nested object(JsonObjectFlattenerTest.groovy:12)
This is my test class
package com.pittacode.apihelper.json
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import spock.lang.Specification
class JsonObjectFlattenerTest extends Specification {
def classUnderTest = new JsonObjectFlattener()
def "flatten json object with one nested object"() {
given:
def jsonString = """
{
"1-1": 11,
"1-2": {
"2-1": "21"
},
"1-3": 13
}
"""
def jsonObject = JsonParser.parseString(json).getAsJsonObject()
when:
JsonObject result = classUnderTest.flatten(jsonObject)
then:
result.keySet().containsAll(["1-1", "1-2", "1-3", "2-1"])
}
}
I have a gradle project with one subproject and a module-info.java
plugins {
id "groovy"
id "application"
id "org.beryx.jlink" version "2.25.0"
id "org.javamodularity.moduleplugin" version "1.8.10"
}
repositories {
mavenCentral()
}
ext {
log4jVersion = "2.17.2"
}
dependencies {
implementation("com.jayway.jsonpath:json-path:2.7.0") {
exclude group: "com.fasterxml.jackson.core"
exclude group: "com.google.gson"
}
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.2")
implementation("com.google.code.gson:gson:2.9.0")
implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}")
runtimeOnly("org.apache.logging.log4j:log4j-core:${log4jVersion}")
annotationProcessor("org.apache.logging.log4j:log4j-core:${log4jVersion}")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}")
testImplementation("org.codehaus.groovy:groovy:3.0.9")
testImplementation("org.spockframework:spock-core:2.0-groovy-3.0")
}
application {
mainClass = "com.pittacode.apihelper.Runner"
mainModule = "com.pittacode.apihelper"
}
tasks.named("test") {
useJUnitPlatform()
}
jlink {
forceMerge "log4j", "jackson"
// options = ["--bind-services"] // makes jre bigger but has everything, good way to test stuff
launcher {
name = "apihelper"
jvmArgs = ["-Dlog4j.configurationFile=./log4j2.xml", "-Dlog4j2.debug=false"]
}
jpackage {
if (org.gradle.internal.os.OperatingSystem.current().windows) {
installerOptions += ["--win-per-user-install", "--win-dir-chooser", "--win-menu", "--win-shortcut"]
imageOptions += ["--win-console"]
}
}
}
tasks.jlink.doLast {
copy {
from("src/main/resources")
into("$buildDir/image/bin")
}
}
This is the class
package com.pittacode.apihelper.json;
import com.google.gson.JsonObject;
public final class JsonObjectFlattener {
public JsonObjectFlattener() {
}
public JsonObject flatten(JsonObject o) {
return null;
}
}
The weird thing is that in another specification class if I try to initiate another object (unrelated) it seems to create it just fine. That one has parameters so I tried adding some in the flattener as well but didn't seem to make a difference
Well this was silly,
Like I mentioned I am using java modules and it seems that I need to export the packages that contain the classes I want to test.
module com.pittacode.apihelper {
requires jdk.crypto.ec; // needed for ssl communication
requires org.slf4j;
requires java.net.http;
requires java.sql;
requires com.google.gson;
requires json.path;
requires org.apache.logging.log4j;
requires com.fasterxml.jackson.databind;
exports com.pittacode.apihelper;
exports com.pittacode.apihelper.json; // <-- this is the missing line
}

Is it possible to swap out the JavaExecAction Gradle is using to run Java?

Java fails to launch when the classpath is too long. The length limit is particularly short on Windows.
Gradle seem uninterested in fixing the issue on their side (even though it's sort of their responsibility since they're the ones launching Java), so we ended up substituting the JavaExec task out with our own alternative.
The alternative works like this:
public class WorkingJavaExec extends JavaExec {
private static final String MATCH_CHUNKS_OF_70_CHARACTERS =
"(?<=\\G.{70})";
private final Logger logger = LoggerFactory.getLogger(getClass());
#Override
public void exec() {
FileCollection oldClasspath = getClasspath();
File jarFile = null;
try {
if (!oldClasspath.isEmpty()) {
try {
jarFile =
toJarWithClasspath(oldClasspath.getFiles());
setClasspath(getProject().files(jarFile));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
super.exec();
} finally {
setClasspath(oldClasspath);
if (jarFile != null) {
try {
Files.delete(jarFile.toPath());
} catch (Exception e) {
logger.warn("Couldn't delete: " + jarFile, e);
}
}
}
}
public static File toJarWithClasspath(Set<File> files)
throws IOException {
File jarFile = File.createTempFile("long-classpath", ".jar");
try (ZipOutputStream zip =
new ZipOutputStream(new FileOutputStream(jarFile))) {
zip.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
try (PrintWriter writer =
new PrintWriter(
new OutputStreamWriter(
zip, StandardCharsets.UTF_8))) {
writer.println("Manifest-Version: 1.0");
String classPath = files.stream().map(
file -> file.toURI().toString())
.collect(Collectors.joining(" "));
String classPathEntry = "Class-Path: " + classPath;
writer.println(Arrays.stream(
classPathEntry.split(MATCH_CHUNKS_OF_70_CHARACTERS))
.collect(Collectors.joining("\n ")));
}
}
return jarFile;
}
}
Using this is cumbersome, though, because everywhere someone might run JavaExec, I have to replace it with WorkingJavaExec. New developers also don't know that there is this pitfall in Gradle in the first place, so they don't even know it's something they have to work around.
In reading the internals of Gradle, I saw that JavaExec internally uses a JavaExecAction to do the actual exec.
I thought that maybe by replacing this, we could fix the problem as if Gradle had fixed it themselves, and maybe it would then also apply to other tasks, such as Test. But I haven't been able to find any examples anywhere. (Even in other large projects, which you would expect to have hit the same issue!)
Is it possible to substitute JavaExecAction, and if so, how?
I'm not sure you can "substitute" JavaExecAction because it is set during JavaExec task instanciation, but I think you can solve this problem in a nicer way, using a custom Plugin as follow:
class FixClasspathLimitPlugin implements Plugin<Project> {
#Override
void apply(Project project) {
// after project has been evaluated, hack into all tasks of type JavaExec declared.
project.afterEvaluate {
project.tasks.stream().filter { task -> task instanceof JavaExec }.forEach {
println "Reconfiguring classpath for : $it"
JavaExec javaExec = (JavaExec) it;
FileCollection oldClasspath = javaExec.getClasspath()
// insert an Action at first position, that will change classpath
javaExec.doFirst { task ->
((JavaExec) task).setClasspath(getProject().files(toJarWithClasspath(oldClasspath.getFiles())));
}
// optional - reset old classpath
javaExec.doLast { task ->
((JavaExec) task).setClasspath(oldClasspath)
}
}
}
}
public static File toJarWithClasspath(Set<File> files)
throws Exception {
// same method implementation as given in your question
}
This way, you won't have to replace JavaExec in all build scripts written by your team, you will only have to ensure that these scripts apply your plugin.
And if you use a custom distribution of Gradle and use wrapper in you enterprise, you can even include this plugin in this distribution as an Init Script, as explained here: https://docs.gradle.org/current/userguide/init_scripts.html#sec:using_an_init_script
Put a file that ends with .gradle in the GRADLE_HOME/init.d/ directory, in the Gradle distribution. This allows you to package up a custom Gradle distribution containing some custom build logic and plugins. You can combine this with the Gradle wrapper as a way to make custom logic available to all builds in your enterprise.
This way, the plugin will be applied in a "transparent" way.
Concerning the Test task: it does not use JavaExecAction, I think, but a similar solution could be applied, using a similar plugin.
You can use the jar task to add the class path to the manifest for you:
jar {
baseName = "my-app"
version = "1.0.0"
manifest {
attributes("Class-Path": configurations.compile.collect { it.getName() }.join(' '))
}
}
And then you can reference that jar when launching:
task run(type:JavaExec) {
classpath = jar.outputs.files
main = "myapp.MainClass"
}
That works around the command line path limit. You might also want to copy the dependency JARs to the output folder, so they will be available at runtime.
task copyDependencies(type: Copy, dependsOn: [ "build" ]) {
from configurations.runtime
into "./build/libs"
}
build.finalizedBy(copyDependencies)
Helpful?

How to get the Files that have changed since the last run of a Gradle Task?

I have the following gradle task:
class MyTranslateTask extends DefaultTask {
#InputFiles FileCollection srcFiles
#OutputDirectory File destDir
#TaskAction
def run() {
...
}
}
How can I get all the files from my srcFiles which have changed since the last run of this task?
Gradle 1.6 introduced an incubating feature called IncrementalTasksInputs that allows you access to files that were changed or removed since last task run.
ref: https://gradle.org/docs/current/dsl/org.gradle.api.tasks.incremental.IncrementalTaskInputs.html
class IncrementalReverseTask extends DefaultTask {
#InputDirectory
def File inputDir
#OutputDirectory
def File outputDir
#TaskAction
void execute(IncrementalTaskInputs inputs) {
inputs.outOfDate { change ->
def targetFile = project.file("$outputDir/${change.file.name}")
targetFile.text = change.file.text.reverse()
}
inputs.removed { change ->
def targetFile = project.file("$outputDir/${change.file.name}")
if (targetFile.exists()) {
targetFile.delete()
}
}
}
}

Gradle: How to create multiple jar?

I'm new to Gradle and Groovy, and I'd hope there would be something to solve my problem.
I have several packages, each of which needs to be compiled into one jar.
One solution I've found is to create multiple tasks that are of Jar type, but I'd like to avoid copy/paste and end up with a giant gradle file whenever I add a new package to my project.
My current implementation is to have multiple jar tasks, like this one :
task jarFoo(type: Jar) {
baseName = "foo"
version = "1.0.0"
String className = baseName.capitalize()
from(sourceSets.main.output) {
include "$baseName/**"
}
from {
configurations.compile.collect {
it.isDirectory() ? it : zipTree(it)
}
}
manifest {
attributes "Implementation-Title": "$className",
"Implementation-Version": "$version",
"Main-Class": "$baseName.$className"
}
}
It works like a charm, however I add packages very often and I will end up with a lot of packages, therefore a lot of tasks and a lot of copied/pasted code.
After fiddling with build.gradle file, I've found that I needed to extend from Jar in order to get a jar created.
So here's the code for the class so far :
class JarTask extends Jar {
String jarName = "default"
String jarVersion = "1.0.0"
#TaskAction
def jar() {
baseName = jarName
version = jarVersion
String className = baseName.capitalize()
// Thanks Opal for reminding that sourceSets
// is defined in project.
from(project.sourceSets.main.output) {
include "$baseName/**"
}
from {
configurations.compile.collect {
it.isDirectory() ? it : zipTree(it)
}
}
manifest {
attributes "Implementation-Title": "$className",
"Implementation-Version": "$version",
"Main-Class": "$baseName.$className"
}
}
}
task jarFoo(type: JarTask) {
jarName = "foo"
}
task jarBar(type: JarTask) {
jarName = "bar"
jarVersion = "1.2.42"
}
The problem is that the jar that is created ignores basically everything in the method: it contains only a MANIFEST.MF containing one line with the manifest version and is given the name of the project, not the name given in task. Nothing else.
If needed, you can find the code online in my GitHub repo (mainly in French).
Any idea would be truly appreciated!
Here is another easier option that allows to pass parameters. I found the inspiration on this topic : https://discuss.gradle.org/t/passing-arguments-to-a-task/8427/20, which sounds exactly like what I was trying to do.
Here we basically define a method that returns a task, given some parameters. The rest is just testing if a version is given, or the code already given in question and adapted with #Opal great help.
It is sufficient to include the new builds in the artifacts block to make tasks available. Then, just run gradle jarqcm to build a single package or gradle assemble to compile everything.
apply plugin: "idea"
apply plugin: "java"
repositories {
mavenCentral()
}
dependencies {
compile "com.intellij:forms_rt:7.0.3"
runtime "com.intellij:forms_rt:7.0.3"
}
def jarPackage(artifactName, artifactVersion) {
if (artifactVersion == "" || artifactVersion == null) {
artifactVersion = "1.0.0"
}
return tasks.create("jar${artifactName}", Jar) {
baseName = artifactName
version = artifactVersion
String className = baseName.capitalize()
from(sourceSets.main.output) {
include "$baseName/**"
}
from {
configurations.compile.collect {
it.isDirectory() ? it : zipTree(it)
}
}
manifest {
attributes "Implementation-Title": "$className",
"Implementation-Version": "$version",
"Main-Class": "$baseName.$className"
}
}
}
artifacts {
archives jarPackage("aleatoire", ""), jarPackage("calculatrice", "1.2.3"), jarPackage("copier", ""),
jarPackage("qcm", "1.0.0")
}
After you edited the question is easy. There's no property sourceSets for the given task (Jar in this case). sourceSets are defined on Project and every task that extends DefaultTask inherits project field from it's parent. So you just need:
from(project.sourceSets.main.output) {
include "$baseName/**"
}
ANSWER
I hope you understand the difference between task configuration and execution phase. This is the problem that occurs here. Basically you extended Jar task which as all tasks of type Copy is not designed to be extended - see here. In task action you configure the artifacts to be copied but.. there's too late for configuration - it's execution phase. To solve the problem task rules may be used. I've modified the script and it's:
apply plugin: "idea"
apply plugin: "java"
repositories {
mavenCentral()
}
dependencies {
compile "com.intellij:forms_rt:7.0.3"
runtime "com.intellij:forms_rt:7.0.3"
}
tasks.addRule('Pattern: build<ID>') { String taskName ->
if (taskName.startsWith('build')) {
task(taskName, type: Jar) {
baseName = taskName - 'build'
version = '1.0.0'
String className = baseName.capitalize()
from(sourceSets.main.output) {
include "$baseName/**"
}
from {
configurations.compile.collect {
it.isDirectory() ? it : zipTree(it)
}
}
manifest {
attributes "Implementation-Title": "$className",
"Implementation-Version": "$version",
"Main-Class": "$baseName.$className"
}
}
}
}
artifacts {
archives project.tasks['buildqcm'], project.tasks['buildlistage'] //etc
}
and should invoked simply with gradle buildlistage buildqcm. You can make additional validation to check if <ID> passed is on the list of packages e.g. Hope that helps and sorry for having to wait so long :/

Categories