使用 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 文件的解决方案引入了我们希望避免的连锁反应,包括:

$ 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 内容