Kotlin 21:构建脚本里的 Kotlin 和应用 Kotlin 是同一个吗
在已经能跑通 Java 消费者测试 的工程中执行 ./gradlew --version,看到的 Kotlin 是 2.0.21;build.gradle.kts 中的 Kotlin Gradle Plugin 和应用的 stdlib 却是 2.2.21。两个数字出现在同一条构建链上,分别约束什么?如果要把任务状态和存储放进领域模块,哪一处依赖决定应用能否找到这些类?
这里仍用冻结的 Gradle Wrapper 8.14、Ubuntu OpenJDK 21.0.12.1、Kotlin Gradle Plugin/stdlib 2.2.21,语言与 API 版本为 2.2,目标字节码为 JVM 21。完整版本、Wrapper 校验和已固定的上游源码 tag SHA 在下载工程的 VERSIONS.md;下文只描述这一组合已经运行的结果,不用官方 Kotlin Gradle 配置文档中的滚动兼容表冒充本机运行证据。
一个工程里为什么能有三处 Kotlin 版本
./gradlew --version 的 Kotlin 2.0.21 是 Gradle 8.14 自带的构建逻辑运行环境版本。它回答“谁在执行 settings.gradle.kts / build.gradle.kts 之类的脚本”,不回答“应用 Kotlin 源码用什么编译器编译”。本工程根构建脚本写了 kotlin("jvm") version "2.2.21";:domain 也应用这个插件,没有单独声明另一版。两模块依赖都显式列出 org.jetbrains.kotlin:kotlin-stdlib:2.2.21。插件、库与 Gradle 内嵌版本不能仅凭同一个“Kotlin”标签合并。
对照实际命令和观测:
| 核对对象 | 在这次实验里的值 | 记录位置 |
|---|---|---|
| Wrapper / 构建脚本运行环境 | Gradle 8.14 / 内嵌 Kotlin 2.0.21 | verification/21/environment.txt 的 ./gradlew --version |
| 应用与领域模块的编译配置 | Plugin 2.2.21;language/api 2.2;jvmTarget 21 | 两份 build.gradle.kts,verification/21/build.txt |
| 两模块运行时依赖 | stdlib 2.2.21 | app-dependencies.txt / domain-dependencies.txt |
| 运行和目标字节码 | Launcher/Daemon JDK 21.0.12.1;两模块 class major 65 | environment.txt / 两份 *-bytecode.txt |
jvmTarget = 21 定义编译产物版本,不会帮另一个机器安装 JDK 21;Gradle 的 toolchain 21 决定项目任务选用哪个 JDK,也不能据此把脚本运行时的内嵌 Kotlin 当作应用编译器。两个运行时类路径都实测出现 stdlib 2.2.21,但依赖图也不直接证明一个未来发布的库具有二进制兼容性。Gradle 8.14 兼容性文档与 Kotlin 的 Gradle 项目配置解释能选怎样的版本范围;具体命中哪一项仍以 Wrapper、依赖图和字节码为准。
把哪一部分拆成模块,如何知道应用真的依赖了它
此前 TaskState.kt、TaskStore.kt、TaskRecord.kt 都在根工程;这次原样移动到 domain/src/main/kotlin/,根工程仍保留 CLI、Java 输入边界与面向 Java 的 facade。TaskStore 还是同步内存实现,默认的 putIfAbsent 仍不是原子操作;TaskRecord 中的 MutableList 也没有因为移动而变成不可变对象。TaskId 和部分任务构造函数暂留应用侧:这只是一次可验证的最小边界,不是已发布且所有领域类型都隔离的库。
settings.gradle.kts 加了 include(":domain");根模块通过 implementation(project(":domain")) 取得类,:domain 则使用同一 Kotlin 插件与编译参数。Gradle 的 :dependencies --configuration runtimeClasspath 此次显示 project :domain,其下是 stdlib 2.2.21;领域自己的 runtimeClasspath 也有 stdlib 2.2.21。这是构建项目依赖,不是复制两个 TaskStore.kt 编译,也没有发布仓库产物。未来如果从可执行应用改为对外提供领域库,api/implementation 对消费者可见性的区分需要另测;不能拿这个运行时类路径证明旧二进制消费端兼容,留给选修 E07。
为了让依赖缺失可观察,隔离 Java 源码只声明一个 TaskStore store 字段。javac --release 21 -classpath build/classes/kotlin/main 实际退出 1,诊断 cannot find symbol、class TaskStore;追加 domain/build/classes/kotlin/main 后同一源码退出 0。这个负例仅隔离了手写 classpath 上的领域类字节码:它不是在说 Gradle 配错了,也没有证明这个 Java 类后来能独立运行所有方法。真实 Gradle 构建使用项目依赖,ModuleBoundaryTest 从应用测试集调用领域状态转移与存储,并保留 19、20 的混编与 Java 消费者测试。
一次 clean test 能对哪些断言负责
bash examples/kotlin-language/verification/21.sh 保存 clean test 的实际任务::domain:compileKotlin、:domain:jar、:compileKotlin、:compileJava、:compileTestKotlin、:compileTestJava 和 :test 等任务可在原始输出中定位。00–21 共二十二组 JUnit,各 2/2;新的 ModuleBoundaryTest 核对调用领域状态函数得到 ACTIVE,并检查内存 TaskStore 的相同文本重复提交返回 false。旧 CLI 正例 submit alpha 为退出 0,错误的 submit 为退出 1。两份 javap -verbose 分别对应用 MainKt 和领域 TaskState 报告 major 65。字节码版本说明“编成了什么目标”,不是说领域转移的并发安全已经验证。
第一次拆分时领域模块未定义仓库,离线编译报 Cannot resolve external dependency org.jetbrains.kotlin:kotlin-stdlib:2.2.21 because no repositories are defined;verification/21/build-initial.txt 保留了这个退出 1。给 :domain 加 mavenCentral() 后从已有缓存离线 clean test 才成功。这样的问题不靠换 Kotlin 版本解决;根模块的 repositories 不会自动成为子模块的依赖仓库。
同名素材里有 包含双模块、Wrapper、RUN/VERSIONS 与累计验证的可解包工程 和 SHA-256校验文件。解包后使用 ./gradlew --offline --no-daemon --console=plain clean test,需有 JDK 21 和已缓存的依赖。第 21 章更改的是构建与类路径,不是在 CLI 中增加持久化或任务执行;真实后台任务、网络和数据库仍未运行。
练习:./gradlew --version 看到 Kotlin 2.0.21,是否需要把 stdlib 降到 2.0.21 才“版本一致”?不需要;该输出描述 Gradle 自身,应用运行时图实际解析 2.2.21。第二问:手工 javac 的 classpath 少了 domain/build/classes/kotlin/main 会怎样?本次编译器无法找到 TaskStore;把领域目录补回后同一源码可编译,但只有真正以 Gradle 项目依赖完成测试,才能同时核对根模块的 Kotlin/Java 编译和运行行为。
[PATTERN] 判断 Kotlin 工程版本与模块问题时,分别读取脚本运行环境、插件和编译选项、应用运行时依赖、真实消费者的类路径。一处 --version 只能回答它所测量的那层;一次类路径编译拒绝也不能替别的层下结论。下一篇会把这些能跑的测试进一步划分为行为契约、故意拒绝的编译样本与测试盲区。






