Project Alice generates Java source code, stores it in sources.jar, then uploads it to a Maven repository. Project Bob pulls sources.jar down and needs to use it when compiling. Bob does not know that Alice exists, only where to find sources.jar.
Versions: JDK 11, Gradle 7.3.1, IntelliJ IDEA 2021.3.1
Making gradle (and IntelliJ's IDEA) build using source files embedded in a JAR file. To be clear, the JAR file contents resemble:
$ jar -tvf sources.jar
0 Thu Feb 03 08:38:56 PST 2022 META-INF/
52 Thu Feb 03 08:38:56 PST 2022 META-INF/MANIFEST.MF
0 Thu Feb 03 08:38:30 PST 2022 com/
0 Thu Feb 03 08:38:32 PST 2022 com/domain/
0 Thu Feb 03 08:38:30 PST 2022 com/domain/package/
938 Thu Feb 03 08:38:32 PST 2022 com/domain/package/SourceCode.java
Solutions that extract the .java files from the .jar file introduce knock-on effects we'd like to avoid, including:
sources.jar is pulled down, we'll have to delete the extraction directory to remove any stale .java files that were preserved. If there was a way to avoid extraction, then the act of pulling down the new sources.jar file would ensure correctness (no added complexity). By unarchiving the .java files, it's possible to enter an inconsistent state:$ jar -tvf sources.jar | grep java$ | wc -l
61
$ find src/gen -name "*java" | wc -l
65
If there was a way to treat sources.jar as a source directory without extracting the files, these knock-on effects disappear.
A number of approaches have failed.
Changing sourceSets doesn't work:
sourceSets.main.java.srcDirs += "jar:${projectDir}/sources.jar!/"
The error is:
Cannot convert URL 'jar:/home/user/dev/project/sources.jar!/' to a file.
Using a zipTree with sourceSets doesn't work, although the error message is telling:
sourceSets.main.java.srcDirs += zipTree(file: "${projectDir}/sources.jar")
Error:
Cannot convert the provided notation to a File or URI.
The following types/formats are supported:
- A URI or URL instance.
This was expected. What was unexpected was that URL instances are allowed, but seemingly not if embedded within a JAR file.
The following allows building Bob, but the IDE is unable to find SourceCode.java:
sourceSets.main.java.srcDirs += zipTree("${projectDir}/sources.jar").matching {
include "com"
}
Modifying the build task to extract the generated code first partially works:
task codeGen {
copy {
from( zipTree( "${projectDir}/sources.jar" ) )
into( "${buildDir}/src/gen/java" )
}
sourceSets.main.java.srcDirs += ["${buildDir}/src/gen/java"]
}
build { doFirst { codeGen } }
The issue is that removing the build directory then prevents static compiles (because IDEA cannot find the generated source files). In any case, we don't want to extract the source files because of all the knock-on problems.
The following snippet also does not compile:
tasks.withType(JavaCompile) {
source = zipTree(file: "${projectDir}/sources.jar")
}
Not updating sourceSets means that the IDE cannot discover the source files.
We could extract the files into the main source directory, instead, such as:
def syncTask = task sync(type: Sync) {
from zipTree("${projectDir}/sources.jar")
into "${projectDir}/src/gen/java"
preserve {
include 'com/**'
exclude 'META-INF/**'
}
}
sourceSets.main.java.srcDir(syncTask)
While that addresses the clean issue, we’re left with the original problems that we’d like to avoid.
Setting the Content Root and marking the Source Folder from within IntelliJ IDEA works. The IDE updates .idea/misc.xml to include:
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
<file type="web" url="jar://$PROJECT_DIR$/project/sources.jar!/" />
</component>
In theory, the idea plugin may have the ability to set this value.
How would you instruct Gradle to reference and build using source files that are stored in an external Java archive file when compiling a project (without extracting the archive) such that IDEA can statically resolve the source files, as well?
We decided that extracting the jar file would be the best approach after all:
apply plugin: 'idea'
final GENERATED_JAR = "${projectDir}/sources.jar"
final GENERATED_DIR = "${buildDir}/generated/sources"
task extractSources(type: Sync) {
from zipTree(GENERATED_JAR)
into GENERATED_DIR
}
sourceSets.main.java.srcDir extractSources
clean.finalizedBy extractSources
idea.module.generatedSourceDirs += file(GENERATED_DIR)
This:
cleanbuild/generated/sources path.jar file and .java files in synchronizationIn effect, the following behaviours work as expected:
./gradlew clean -- leaves the extracted .java files intact, effectively./gradlew build -- re-synchronizes .java files with .jar contents