feat(java): --version 指定 JDK 版本(Temurin/Oracle 能,MS 只认快照,Zulu 报错)+ --dry-run(M280)

用户原话:「我们内置了那么多镜像源 甚至用户不能手动选择指定版本下载。」这条之前
`InstallRequest` 根本没有版本字段,四家全是「该 major 最新 GA」。

core:
- `InstallRequest.version: Option<String>` + `with_version`;`None` 时仍走 `source::fetch`
  的老路径(`latest_url` / `parse_latest` / `fetch_latest` 一个字节未动)。
- `source::fetch` 在唯一分派点按 `Option<&str>` 分两路:`latest` 与 `pinned`,后者绝不换家。
- Temurin(新 `api/version.rs`):先 `assets/feature_releases/{major}/ga` 枚举(`page_size`
  上限 20),把用户给的公共版本号翻成 release 名,再打 `assets/release_name/eclipse/{name}`
  取归档——不走 `latest`。实测 `/v3/assets/version/{v}` 在线上回 404,故不用它(见报告)。
- Oracle:`versions_for` 接进选择路径(`select_version`),`None` 仍是 `pick_version`。
- Microsoft:只认写死快照里那一个版本号,别的版本明确报错并说清「这家不能自选版本」。
- Zulu / Termux:上游只有 latest / 版本由系统包定 → 明确报错,不回落、不换家。
- `catalog.rs` 收策略:`matches_version`(唯一相等判据,不做前缀近似)、
  `version_pin_error`(四家文案互不相同且各自说清原因 + 下一步)、`version_mismatch`
  (兜底闸:装到的版本必须真是用户要的那一版)。
- `plan()` 改成接 `&InstallRequest`,`--dry-run` 与真装走同一条选择路径。

cli:
- `java install <major> [--version V] [--dry-run] [--vendor X]`;`--json` 增 `requested_version`。
- 全局 `--version` 只在子命令之前成立,否则 `java install 21 --version 21.0.13+9` 会被
  当成「打印启动器版本」吞掉。

拆分(守 400 行硬线,均为纯搬移 + 逐字节核对):
- `api.rs` → `api/version.rs`、`oracle.rs` → `oracle/listing.rs`(纯的那一半)、
  `cli/parse.rs` → `cli/parse/java.rs`、`commands/java.rs` → `commands/java_install.rs`。

门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-280):core --lib / --test game / --test launch、
cli --all-targets、fmt --check、clippy -D warnings 全部 EXIT=0。
This commit is contained in:
worker-javapin authored and AskaEth committed 2026-11-12 22:05:17 +08:00
1 parent c8f820caec
commit 9fded45d9e
16 files changed
+1352 -346

No files matched your search

+76 -27
View File
@@ -11,59 +11,108 @@
//! 换下一家;同一家内部的镜像换源(TUNA→官方、华为云→南大)仍然归 `core::download` 引擎管。
use super::api;
use super::error::JavaError;
use super::catalog;
use super::error::{JavaError, JavaErrorKind};
use super::microsoft;
use super::oracle;
use super::release::JdkRelease;
use super::target::AdoptiumTarget;
use super::vendor::JdkVendor;
use super::zulu;
use crate::download::DownloadEngine;
use crate::download::{DownloadEngine, MirrorTable};
/// 查一次「某发行版某大版本的最新 GA」。
/// 查一次「某发行版某大版本」的归档。
///
/// `version = None` 是要该家**最新 GA**(D13 口径,行为与 M280 之前逐字节一致);
/// `Some(v)` 要求精确版本:能按版本取的只有 Temurin / Oracle,Microsoft 只认内置快照里那一个,
/// Zulu(只有 `latest=true`)与 Termux(系统包定版本)直接报错——错误文案由
/// [`catalog::version_pin_error`] 统一出,**这里不换家、不回落 latest**。
///
/// # Errors
///
/// 平台无产物、清单不可达、该家没有这个大版本、校验缺失时返回错误——调用方据此降级。
/// 平台无产物、清单不可达、该版本不存在、校验缺失、这家不支持指定版本时返回错误——调用方据此降级。
pub async fn fetch(
engine: &DownloadEngine,
vendor: JdkVendor,
major: u32,
target: AdoptiumTarget,
version: Option<&str>,
) -> Result<JdkRelease, JavaError> {
let client = engine.client();
let table = engine.mirrors();
match version {
None => latest(client, table, vendor, major, target).await,
Some(wanted) => pinned(client, table, vendor, major, target, wanted).await,
}
}
/// 该家的最新 GA(`fetch` 的 `None` 那一路,M280 之前只有这一条)。
async fn latest(
client: &reqwest::Client,
table: &MirrorTable,
vendor: JdkVendor,
major: u32,
target: AdoptiumTarget,
) -> Result<JdkRelease, JavaError> {
match vendor {
JdkVendor::Temurin => api::fetch_latest(client, major, target).await,
JdkVendor::Oracle => oracle::fetch_latest(client, table, major, target).await,
JdkVendor::Microsoft => {
let table = table.microsoft_openjdk().ok_or_else(|| {
JavaError::new(
super::error::JavaErrorKind::Metadata,
"镜像清单里没有 microsoft_openjdk 表",
)
.with_hint("升级 mirrors.json 到 v3,或改用 --vendor temurin")
})?;
microsoft::fetch_latest(client, table, major, target).await
microsoft::fetch_latest(client, microsoft_table(table)?, major, target).await
}
JdkVendor::Zulu => {
let table = table.zulu().ok_or_else(|| {
JavaError::new(
super::error::JavaErrorKind::Metadata,
"镜像清单里没有 zulu 表",
)
.with_hint("升级 mirrors.json 到 v3,或改用 --vendor temurin")
})?;
zulu::fetch_latest(client, table, major, target).await
}
JdkVendor::Termux => Err(JavaError::new(
super::error::JavaErrorKind::Unsupported,
"Termux 的 JDK 由系统包管理器安装,不走归档下载链",
)
.with_hint("在 Termux 里用 `ull java install <major>`(默认走 termux 源)")),
JdkVendor::Zulu => zulu::fetch_latest(client, zulu_table(table)?, major, target).await,
JdkVendor::Termux => Err(termux_archive_error()),
}
}
/// 该家的指定版本(`fetch` 的 `Some` 那一路)。
async fn pinned(
client: &reqwest::Client,
table: &MirrorTable,
vendor: JdkVendor,
major: u32,
target: AdoptiumTarget,
wanted: &str,
) -> Result<JdkRelease, JavaError> {
match vendor {
JdkVendor::Temurin => api::fetch_version(client, major, target, wanted).await,
JdkVendor::Oracle => oracle::fetch_version(client, table, major, target, wanted).await,
JdkVendor::Microsoft => {
microsoft::fetch_version(client, microsoft_table(table)?, major, target, wanted).await
}
// 上游只给最新一份(Zulu)/ 由系统包管理器定版本(Termux):明确报错,
// 绝不换成别的版本假装装成了用户点名的那一版。
JdkVendor::Zulu | JdkVendor::Termux => {
Err(catalog::version_pin_error(vendor, major, wanted, &[]))
}
}
}
fn microsoft_table(table: &MirrorTable) -> Result<&crate::download::MsOpenJdkTable, JavaError> {
table.microsoft_openjdk().ok_or_else(|| {
JavaError::new(
JavaErrorKind::Metadata,
"镜像清单里没有 microsoft_openjdk 表",
)
.with_hint("升级 mirrors.json 到 v3,或改用 --vendor temurin")
})
}
fn zulu_table(table: &MirrorTable) -> Result<&crate::download::ZuluTable, JavaError> {
table.zulu().ok_or_else(|| {
JavaError::new(JavaErrorKind::Metadata, "镜像清单里没有 zulu 表")
.with_hint("升级 mirrors.json 到 v3,或改用 --vendor temurin")
})
}
fn termux_archive_error() -> JavaError {
JavaError::new(
JavaErrorKind::Unsupported,
"Termux 的 JDK 由系统包管理器安装,不走归档下载链",
)
.with_hint("在 Termux 里用 `ull java install <major>`(默认走 termux 源)")
}
/// 一跳的结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VendorAttempt {