使用 zipTree 作为构建和静态编译的源集
Use zipTree as source set for building and static compiling
背景
Alice 项目生成 Java 源代码,将其存储在 sources.jar
中,然后将其上传到 Maven 存储库。 Bob项目把sources.jar
拉下来,编译的时候需要用到。 Bob不知道Alice的存在,只知道去哪里找sources.jar
.
版本:JDK11,Gradle7.3.1,IntelliJIDEA 2021.3.1
问题
使用嵌入在 JAR 文件中的源文件进行 gradle(和 IntelliJ 的 IDEA)构建。明确地说,JAR 文件内容类似于:
$ 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
从 .jar
文件中提取 .java
文件的解决方案引入了我们希望避免的连锁反应,包括:
- 可编辑。 提取的源文件可以在 IDE 中编辑。我们希望它们是只读的。我们可以添加一个将文件设置为只读的任务,但这感觉就像解决了错误的问题(增加了复杂性)。
- 同步。 当新生成的
sources.jar
被拉下时,我们必须删除提取目录以删除任何陈旧的 .java 文件被保存下来。如果有办法避免提取,那么下拉新 sources.jar
文件的行为将确保正确性(不会增加复杂性)。通过取消存档 .java 文件,可能会进入不一致状态:
$ jar -tvf sources.jar | grep java$ | wc -l
61
$ find src/gen -name "*java" | wc -l
65
如果有一种方法可以在不提取文件的情况下将 sources.jar
视为源目录,那么这些连锁反应就会消失。
尝试次数
许多方法都失败了。
源集
更改 sourceSets
无效:
sourceSets.main.java.srcDirs += "jar:${projectDir}/sources.jar!/"
错误是:
Cannot convert URL 'jar:/home/user/dev/project/sources.jar!/' to a file.
使用带 sourceSets
的 zipTree 不起作用,尽管错误消息告诉:
sourceSets.main.java.srcDirs += zipTree(file: "${projectDir}/sources.jar")
错误:
Cannot convert the provided notation to a File or URI.
The following types/formats are supported:
- A URI or URL instance.
这是预料之中的。出乎意料的是 URL 个实例是允许的,但如果嵌入到 JAR 文件中似乎就不行了。
以下允许构建 Bob,但 IDE 无法找到 SourceCode.java
:
sourceSets.main.java.srcDirs += zipTree("${projectDir}/sources.jar").matching {
include "com"
}
构建任务
修改构建任务以首先提取生成的代码部分有效:
task codeGen {
copy {
from( zipTree( "${projectDir}/sources.jar" ) )
into( "${buildDir}/src/gen/java" )
}
sourceSets.main.java.srcDirs += ["${buildDir}/src/gen/java"]
}
build { doFirst { codeGen } }
问题是删除 build
目录会阻止静态编译(因为 IDEA 无法找到生成的源文件)。无论如何,我们不想因为所有连锁问题而提取源文件。
编译任务
以下代码段也无法编译:
tasks.withType(JavaCompile) {
source = zipTree(file: "${projectDir}/sources.jar")
}
不更新sourceSets
意味着IDE无法发现源文件。
同步任务
我们可以将文件提取到主源目录中,例如:
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)
虽然这解决了干净的问题,但我们留下了我们想要避免的原始问题。
内容根目录
设置内容根目录并在 IntelliJ 中标记源文件夹 IDEA 有效。 IDE 更新 .idea/misc.xml
以包括:
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
<file type="web" url="jar://$PROJECT_DIR$/project/sources.jar!/" />
</component>
理论上,idea
插件可以设置这个值。
问题
在编译项目时,您如何指示 Gradle 使用存储在外部 Java 存档文件中的源文件进行引用和构建(没有解压缩存档)这样 IDEA 也可以静态解析源文件?
工作解决方案
我最初认为不可能使用包含未编译 Java 代码的 JAR 文件作为 IntelliJ 中的附加源。不过,经过几次尝试,我最终可以在 UI 中配置它,这要感谢“内容根目录”部分的指针。稍后对 IDEA plugin 进行了一些摆弄,我终于想出了一个完全可行的解决方案:
plugins {
id 'java'
id 'idea'
}
tasks.withType(JavaCompile) {
source(zipTree("${projectDir}/sources.jar"))
}
idea.module.iml {
withXml {
def baseUrl = 'jar://$MODULE_DIR$/sources.jar!/'
def component = it.asNode().component[0]
def jarContent = component.appendNode('content', [url: baseUrl])
jarContent.appendNode('sourceFolder', [
url: "${baseUrl}com",
isTestSource: false,
])
}
}
在 IntelliJ 中打开项目之前,您必须 运行 ./gradlew idea
然后用 IntelliJ 打开生成的 .ipr
文件。如果 IntelliJ 说“Gradle 找到构建脚本”,那么 不要 尝试加载 Gradle 项目,而是“跳过”这个项目。您现在有一个由 Gradle 生成但仍然独立于 Gradle 的 IntelliJ 项目——因此您可以安全地调用 ./gradlew clean
而不会影响 IntelliJ。
其他想法
这是从 Gradle 配置编译器的另一种方法,使用 javac
的(很少使用的)-sourcepath
option:
tasks.withType(JavaCompile) {
options.sourcepath = files("${projectDir}/sources.jar")
}
我个人仍然更喜欢一种方法,我不必让 Gradle 生成 IntelliJ 项目,而是让 IntelliJ 使用 Gradle。但是,这需要提取 JAR 文件。一个很好的 Gradle 解决方案如下:
plugins {
id 'java'
}
def unzipAlice = tasks.register('unzipAlice', Sync) {
from(zipTree("${projectDir}/sources.jar"))
into(temporaryDir)
}
sourceSets.main.java.srcDir(unzipAlice)
此解决方案至少解决了您问题中提到的“同步”问题:Sync
task 确保不会忽略 sources.jar
的任何更新(包括删除)。
关于“可编辑”的问题,我想知道你为什么不信任你的开发人员?无论如何,他们可以在其余代码中产生各种废话。或者即使未提取 JAR,他们仍然可以替换 JAR 中的文件。使用此提取解决方案,IntelliJ 至少会在源代码被编辑时发出警告:
Generated source files should not be edited. The changes will be lost when sources are regenerated.
好的,如果你 运行 ./gradlew clean
那么 IntelliJ 确实不会再找到源代码了。但这可以通过在 UI 中调用“Build” → “Build Project” 或 运行ning “./gradlew unzipAlice” 轻松解决。如果“./gradlew clean”问题确实是提取方法的一个破坏因素,那么您仍然可以考虑将源提取到 build
目录之外……
我们决定提取 jar
文件毕竟是最好的方法:
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)
这个:
- 在
clean
之后保留生成的源
- 修改生成的源时在 IDE 中发出警告
- 将构建过程与提取源文件相结合
- 重复使用
build/generated/sources
路径
- 保持
.jar
个文件和 .java
个文件同步
实际上,以下行为按预期工作:
./gradlew clean
-- 完整保留提取的 .java
文件,有效
./gradlew build
-- re-synchronizes .java
个文件 .jar
内容
背景
Alice 项目生成 Java 源代码,将其存储在 sources.jar
中,然后将其上传到 Maven 存储库。 Bob项目把sources.jar
拉下来,编译的时候需要用到。 Bob不知道Alice的存在,只知道去哪里找sources.jar
.
版本:JDK11,Gradle7.3.1,IntelliJIDEA 2021.3.1
问题
使用嵌入在 JAR 文件中的源文件进行 gradle(和 IntelliJ 的 IDEA)构建。明确地说,JAR 文件内容类似于:
$ 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
从 .jar
文件中提取 .java
文件的解决方案引入了我们希望避免的连锁反应,包括:
- 可编辑。 提取的源文件可以在 IDE 中编辑。我们希望它们是只读的。我们可以添加一个将文件设置为只读的任务,但这感觉就像解决了错误的问题(增加了复杂性)。
- 同步。 当新生成的
sources.jar
被拉下时,我们必须删除提取目录以删除任何陈旧的 .java 文件被保存下来。如果有办法避免提取,那么下拉新sources.jar
文件的行为将确保正确性(不会增加复杂性)。通过取消存档 .java 文件,可能会进入不一致状态:
$ jar -tvf sources.jar | grep java$ | wc -l
61
$ find src/gen -name "*java" | wc -l
65
如果有一种方法可以在不提取文件的情况下将 sources.jar
视为源目录,那么这些连锁反应就会消失。
尝试次数
许多方法都失败了。
源集
更改 sourceSets
无效:
sourceSets.main.java.srcDirs += "jar:${projectDir}/sources.jar!/"
错误是:
Cannot convert URL 'jar:/home/user/dev/project/sources.jar!/' to a file.
使用带 sourceSets
的 zipTree 不起作用,尽管错误消息告诉:
sourceSets.main.java.srcDirs += zipTree(file: "${projectDir}/sources.jar")
错误:
Cannot convert the provided notation to a File or URI.
The following types/formats are supported:
- A URI or URL instance.
这是预料之中的。出乎意料的是 URL 个实例是允许的,但如果嵌入到 JAR 文件中似乎就不行了。
以下允许构建 Bob,但 IDE 无法找到 SourceCode.java
:
sourceSets.main.java.srcDirs += zipTree("${projectDir}/sources.jar").matching {
include "com"
}
构建任务
修改构建任务以首先提取生成的代码部分有效:
task codeGen {
copy {
from( zipTree( "${projectDir}/sources.jar" ) )
into( "${buildDir}/src/gen/java" )
}
sourceSets.main.java.srcDirs += ["${buildDir}/src/gen/java"]
}
build { doFirst { codeGen } }
问题是删除 build
目录会阻止静态编译(因为 IDEA 无法找到生成的源文件)。无论如何,我们不想因为所有连锁问题而提取源文件。
编译任务
以下代码段也无法编译:
tasks.withType(JavaCompile) {
source = zipTree(file: "${projectDir}/sources.jar")
}
不更新sourceSets
意味着IDE无法发现源文件。
同步任务
我们可以将文件提取到主源目录中,例如:
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)
虽然这解决了干净的问题,但我们留下了我们想要避免的原始问题。
内容根目录
设置内容根目录并在 IntelliJ 中标记源文件夹 IDEA 有效。 IDE 更新 .idea/misc.xml
以包括:
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
<file type="web" url="jar://$PROJECT_DIR$/project/sources.jar!/" />
</component>
理论上,idea
插件可以设置这个值。
问题
在编译项目时,您如何指示 Gradle 使用存储在外部 Java 存档文件中的源文件进行引用和构建(没有解压缩存档)这样 IDEA 也可以静态解析源文件?
工作解决方案
我最初认为不可能使用包含未编译 Java 代码的 JAR 文件作为 IntelliJ 中的附加源。不过,经过几次尝试,我最终可以在 UI 中配置它,这要感谢“内容根目录”部分的指针。稍后对 IDEA plugin 进行了一些摆弄,我终于想出了一个完全可行的解决方案:
plugins {
id 'java'
id 'idea'
}
tasks.withType(JavaCompile) {
source(zipTree("${projectDir}/sources.jar"))
}
idea.module.iml {
withXml {
def baseUrl = 'jar://$MODULE_DIR$/sources.jar!/'
def component = it.asNode().component[0]
def jarContent = component.appendNode('content', [url: baseUrl])
jarContent.appendNode('sourceFolder', [
url: "${baseUrl}com",
isTestSource: false,
])
}
}
在 IntelliJ 中打开项目之前,您必须 运行 ./gradlew idea
然后用 IntelliJ 打开生成的 .ipr
文件。如果 IntelliJ 说“Gradle 找到构建脚本”,那么 不要 尝试加载 Gradle 项目,而是“跳过”这个项目。您现在有一个由 Gradle 生成但仍然独立于 Gradle 的 IntelliJ 项目——因此您可以安全地调用 ./gradlew clean
而不会影响 IntelliJ。
其他想法
这是从 Gradle 配置编译器的另一种方法,使用 javac
的(很少使用的)-sourcepath
option:
tasks.withType(JavaCompile) {
options.sourcepath = files("${projectDir}/sources.jar")
}
我个人仍然更喜欢一种方法,我不必让 Gradle 生成 IntelliJ 项目,而是让 IntelliJ 使用 Gradle。但是,这需要提取 JAR 文件。一个很好的 Gradle 解决方案如下:
plugins {
id 'java'
}
def unzipAlice = tasks.register('unzipAlice', Sync) {
from(zipTree("${projectDir}/sources.jar"))
into(temporaryDir)
}
sourceSets.main.java.srcDir(unzipAlice)
此解决方案至少解决了您问题中提到的“同步”问题:Sync
task 确保不会忽略 sources.jar
的任何更新(包括删除)。
关于“可编辑”的问题,我想知道你为什么不信任你的开发人员?无论如何,他们可以在其余代码中产生各种废话。或者即使未提取 JAR,他们仍然可以替换 JAR 中的文件。使用此提取解决方案,IntelliJ 至少会在源代码被编辑时发出警告:
Generated source files should not be edited. The changes will be lost when sources are regenerated.
好的,如果你 运行 ./gradlew clean
那么 IntelliJ 确实不会再找到源代码了。但这可以通过在 UI 中调用“Build” → “Build Project” 或 运行ning “./gradlew unzipAlice” 轻松解决。如果“./gradlew clean”问题确实是提取方法的一个破坏因素,那么您仍然可以考虑将源提取到 build
目录之外……
我们决定提取 jar
文件毕竟是最好的方法:
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)
这个:
- 在
clean
之后保留生成的源
- 修改生成的源时在 IDE 中发出警告
- 将构建过程与提取源文件相结合
- 重复使用
build/generated/sources
路径 - 保持
.jar
个文件和.java
个文件同步
实际上,以下行为按预期工作:
./gradlew clean
-- 完整保留提取的.java
文件,有效./gradlew build
-- re-synchronizes.java
个文件.jar
内容