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:
1 parent
c8f820caec
commit
9fded45d9e
16 files changed
+1352
-346
No files matched your search
@@ -40,8 +40,10 @@ pub const fn general() -> &'static str {
|
|||||||
config get [<key>] [--show-secret] 读配置(生效值;无 key 时输出整份)
|
config get [<key>] [--show-secret] 读配置(生效值;无 key 时输出整份)
|
||||||
config set <key> <value> 写配置(走 setting schema 校验)
|
config set <key> <value> 写配置(走 setting schema 校验)
|
||||||
java list 列出可用 Java(含每个 JDK 的实际落点)
|
java list 列出可用 Java(含每个 JDK 的实际落点)
|
||||||
java install <major> [--vendor X] 安装托管 JDK(缺省按地区回退链选发行版;
|
java install <major> [--vendor X] [--version V] [--dry-run]
|
||||||
--vendor temurin|oracle|microsoft|zulu|termux 钉死一家)
|
安装托管 JDK(缺省按地区回退链选发行版;
|
||||||
|
--vendor temurin|oracle|microsoft|zulu|termux 钉死一家;
|
||||||
|
--version 钉具体版本,--dry-run 只查版本不下载)
|
||||||
java vendors 列出各发行版的镜像/支持版本/当前优先级
|
java vendors 列出各发行版的镜像/支持版本/当前优先级
|
||||||
agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP;
|
agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP;
|
||||||
[--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读)
|
[--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读)
|
||||||
@@ -251,9 +253,22 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL
|
|||||||
"\
|
"\
|
||||||
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
|
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
|
||||||
并打印每个 JDK 的实际落点(managed 项带目录)
|
并打印每个 JDK 的实际落点(managed 项带目录)
|
||||||
%prog% java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
|
%prog% java install <major> [--vendor X] [--version V] [--dry-run]
|
||||||
|
安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
|
||||||
%prog% java vendors 列出各发行版的镜像/支持版本/当前优先级顺序
|
%prog% java vendors 列出各发行版的镜像/支持版本/当前优先级顺序
|
||||||
|
|
||||||
|
指定版本(M280,`--version`):
|
||||||
|
Temurin temurin 能:打 Adoptium 的「指定版本」端点(不是 latest),版本不存在时列出可用版本
|
||||||
|
Oracle oracle 能:版本串来自镜像目录页(`versions_for` 枚举),选定就用它
|
||||||
|
Microsoft microsoft 只能装它内置快照里那一个版本号(当前 21 是 21.0.13.0.2);写别的版本会报错
|
||||||
|
Zulu zulu 不能:上游只提供 latest;给了 --version 会报错,不会换成别的版本
|
||||||
|
Termux termux 不能:版本由系统包管理器决定
|
||||||
|
钉不了的那几家,报错里会写清原因并提示改用 `--vendor temurin`;不会静默回落到最新,
|
||||||
|
也不会换一家装个别的版本冒充。`--version` 与 `--vendor` 不匹配时就是「这家没有这个版本」。
|
||||||
|
注:全局 `--version`(打印启动器版本)只在子命令**之前**成立:`ull --version` 是版本,
|
||||||
|
`ull java install 21 --version 21.0.13+9` 里的是 JDK 版本。
|
||||||
|
`--dry-run`:只查版本并打印**最终会打哪个版本串 / 哪个 URL**,不下载、不解压、不写盘。
|
||||||
|
|
||||||
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
|
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
|
||||||
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
|
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
|
||||||
standard Windows %APPDATA%\\Java\\<发行方>\\<版本>\\;macOS ~/Library/Application Support/Java/…
|
standard Windows %APPDATA%\\Java\\<发行方>\\<版本>\\;macOS ~/Library/Application Support/Java/…
|
||||||
|
|||||||
@@ -55,12 +55,16 @@ pub enum Command {
|
|||||||
ConfigSet(ConfigSetArgs),
|
ConfigSet(ConfigSetArgs),
|
||||||
/// `java list`。
|
/// `java list`。
|
||||||
JavaList,
|
JavaList,
|
||||||
/// `java install <major> [--vendor X]`。
|
/// `java install <major> [--vendor X] [--version V] [--dry-run]`。
|
||||||
JavaInstall {
|
JavaInstall {
|
||||||
/// 目标 JDK 主版本(8 / 17 / 21)。
|
/// 目标 JDK 主版本(8 / 17 / 21)。
|
||||||
major: u32,
|
major: u32,
|
||||||
/// 钉死的发行版(缺省走 D23 的回退链)。
|
/// 钉死的发行版(缺省走 D23 的回退链)。
|
||||||
vendor: Option<unlucky_core::game::java::JdkVendor>,
|
vendor: Option<unlucky_core::game::java::JdkVendor>,
|
||||||
|
/// 钉死的具体版本(`--version`;`None` = 该家最新 GA)。
|
||||||
|
version: Option<String>,
|
||||||
|
/// 只查版本、打印将要下载的版本与 URL,不落盘(`--dry-run`)。
|
||||||
|
dry_run: bool,
|
||||||
},
|
},
|
||||||
/// `java vendors`:列出各发行版的可用性 / 镜像 / 当前优先级。
|
/// `java vendors`:列出各发行版的可用性 / 镜像 / 当前优先级。
|
||||||
JavaVendors,
|
JavaVendors,
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use unlucky_core::game::java::JdkVendor;
|
|
||||||
|
|
||||||
use crate::error::CliError;
|
use crate::error::CliError;
|
||||||
|
|
||||||
mod accounts;
|
mod accounts;
|
||||||
mod agent;
|
mod agent;
|
||||||
mod instances;
|
mod instances;
|
||||||
|
mod java;
|
||||||
mod update;
|
mod update;
|
||||||
|
|
||||||
use super::model::{
|
use super::model::{
|
||||||
@@ -45,7 +44,9 @@ pub fn parse(args: &[String]) -> Result<Invocation, CliError> {
|
|||||||
// 帮助/版本是「伪命令」,但不提前返回:`--help` 出现在命令前面时,
|
// 帮助/版本是「伪命令」,但不提前返回:`--help` 出现在命令前面时,
|
||||||
// 主题要从后面收完的位置参数里取(`ull --help launch` 与 `ull launch --help` 等价)。
|
// 主题要从后面收完的位置参数里取(`ull --help launch` 与 `ull launch --help` 等价)。
|
||||||
"--help" | "-h" => help_requested = true,
|
"--help" | "-h" => help_requested = true,
|
||||||
"--version" | "-V" => version_requested = true,
|
// 全局 `--version` 只在**子命令之前**成立(`ull --version`):`java install <major>
|
||||||
|
// --version <v>` 里的同名旗标是 JDK 版本,必须原样交给子命令解析(M280)。
|
||||||
|
"--version" | "-V" if rest.is_empty() => version_requested = true,
|
||||||
"--data-dir" => {
|
"--data-dir" => {
|
||||||
index += 1;
|
index += 1;
|
||||||
globals.data_dir = Some(PathBuf::from(take_value(args, index, "data-dir")?));
|
globals.data_dir = Some(PathBuf::from(take_value(args, index, "data-dir")?));
|
||||||
@@ -91,7 +92,7 @@ fn parse_command(rest: &[String], globals: &Globals) -> Result<Command, CliError
|
|||||||
"logs" => parse_logs(tail),
|
"logs" => parse_logs(tail),
|
||||||
"diag" => parse_diag(tail),
|
"diag" => parse_diag(tail),
|
||||||
"config" => parse_config(tail),
|
"config" => parse_config(tail),
|
||||||
"java" => parse_java(tail),
|
"java" => java::parse_java(tail),
|
||||||
"accounts" => accounts::parse_accounts(tail),
|
"accounts" => accounts::parse_accounts(tail),
|
||||||
"agent" => agent::parse_agent(tail),
|
"agent" => agent::parse_agent(tail),
|
||||||
"update" => update::parse_update(tail),
|
"update" => update::parse_update(tail),
|
||||||
@@ -243,38 +244,6 @@ fn parse_config(tail: &[String]) -> Result<Command, CliError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_java(tail: &[String]) -> Result<Command, CliError> {
|
|
||||||
match tail.first().map(String::as_str) {
|
|
||||||
None | Some("list") => {
|
|
||||||
ensure_no_extra(tail.get(1..).unwrap_or_default(), "java list")?;
|
|
||||||
Ok(Command::JavaList)
|
|
||||||
}
|
|
||||||
Some("vendors") => {
|
|
||||||
ensure_no_extra(tail.get(1..).unwrap_or_default(), "java vendors")?;
|
|
||||||
Ok(Command::JavaVendors)
|
|
||||||
}
|
|
||||||
Some("install") => {
|
|
||||||
let parsed = parse_flags(tail.get(1..).unwrap_or_default(), &["vendor"], &[])?;
|
|
||||||
let raw = require_single_positional(&parsed, "java install <major>")?;
|
|
||||||
let major = raw
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|_| CliError::usage(format!("JDK 主版本需要整数,收到 `{raw}`")))?;
|
|
||||||
let vendor = match parsed.value("vendor") {
|
|
||||||
Some(text) => Some(JdkVendor::parse(&text).ok_or_else(|| {
|
|
||||||
CliError::usage(format!(
|
|
||||||
"--vendor 只有 temurin|oracle|microsoft|zulu,收到 `{text}`"
|
|
||||||
))
|
|
||||||
})?),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
Ok(Command::JavaInstall { major, vendor })
|
|
||||||
}
|
|
||||||
Some(other) => Err(CliError::usage(format!(
|
|
||||||
"java 的子命令只有 list|install|vendors,收到 `{other}`"
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 单个位置参数(多一个少一个都算用法错误)。
|
/// 单个位置参数(多一个少一个都算用法错误)。
|
||||||
fn require_single_positional(parsed: &ParsedArgs, usage: &str) -> Result<String, CliError> {
|
fn require_single_positional(parsed: &ParsedArgs, usage: &str) -> Result<String, CliError> {
|
||||||
match parsed.positionals.as_slice() {
|
match parsed.positionals.as_slice() {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
//! `java` 的子命令解析(`list` / `install` / `vendors`)。
|
||||||
|
//!
|
||||||
|
//! 从 `parse.rs` 拆出来只为守 400 行硬线(M280 的 `--version` / `--dry-run` 让那边贴近上限);
|
||||||
|
//! `list` / `vendors` 是纯搬移,`install` 多了两个旗标:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! --version <v> 钉死具体版本(缺省 = 该家最新 GA);能不能钉由发行版决定,
|
||||||
|
//! 钉不了的时候由 core 报错(本层不猜、不改写用户给的值)
|
||||||
|
//! --dry-run 只查版本并打印将要下载的版本串与 URL,不落盘
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use unlucky_core::game::java::JdkVendor;
|
||||||
|
|
||||||
|
use super::super::model::Command;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(super) fn parse_java(tail: &[String]) -> Result<Command, CliError> {
|
||||||
|
match tail.first().map(String::as_str) {
|
||||||
|
None | Some("list") => {
|
||||||
|
ensure_no_extra(tail.get(1..).unwrap_or_default(), "java list")?;
|
||||||
|
Ok(Command::JavaList)
|
||||||
|
}
|
||||||
|
Some("vendors") => {
|
||||||
|
ensure_no_extra(tail.get(1..).unwrap_or_default(), "java vendors")?;
|
||||||
|
Ok(Command::JavaVendors)
|
||||||
|
}
|
||||||
|
Some("install") => parse_java_install(tail.get(1..).unwrap_or_default()),
|
||||||
|
Some(other) => Err(CliError::usage(format!(
|
||||||
|
"java 的子命令只有 list|install|vendors,收到 `{other}`"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `java install <major> [--vendor X] [--version V] [--dry-run]`。
|
||||||
|
fn parse_java_install(args: &[String]) -> Result<Command, CliError> {
|
||||||
|
let parsed = parse_flags(args, &["vendor", "version"], &["dry-run"])?;
|
||||||
|
let raw = require_single_positional(&parsed, "java install <major>")?;
|
||||||
|
let major = raw
|
||||||
|
.parse::<u32>()
|
||||||
|
.map_err(|_| CliError::usage(format!("JDK 主版本需要整数,收到 `{raw}`")))?;
|
||||||
|
let vendor = match parsed.value("vendor") {
|
||||||
|
Some(text) => Some(JdkVendor::parse(&text).ok_or_else(|| {
|
||||||
|
CliError::usage(format!(
|
||||||
|
"--vendor 只有 temurin|oracle|microsoft|zulu,收到 `{text}`"
|
||||||
|
))
|
||||||
|
})?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
// 版本串**原样**往下传(`21.0.13+9` / `jdk-21.0.13+9` / `21.0.13` 各有语义)。
|
||||||
|
// 空值在这里就拦掉:`--version ""` 到 core 会变成一个没有任何版本能匹配的请求。
|
||||||
|
let version = match parsed.value("version") {
|
||||||
|
Some(text) if text.trim().is_empty() => {
|
||||||
|
return Err(
|
||||||
|
CliError::usage("--version 需要一个版本串,例如 --version 21.0.13+9")
|
||||||
|
.with_hint("%prog% help java 看各发行版能不能指定版本"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(text) => Some(text.trim().to_string()),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
Ok(Command::JavaInstall {
|
||||||
|
major,
|
||||||
|
vendor,
|
||||||
|
version,
|
||||||
|
dry_run: parsed.has("dry-run"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use unlucky_core::game::java::JdkVendor;
|
||||||
|
|
||||||
|
use crate::cli::{parse, Command};
|
||||||
|
use crate::error::CliError;
|
||||||
|
|
||||||
|
fn parse_args(args: &[&str]) -> Result<Command, CliError> {
|
||||||
|
let owned: Vec<String> = args.iter().map(|arg| arg.to_string()).collect();
|
||||||
|
parse(&owned).map(|invocation| invocation.command)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command(args: &[&str]) -> Command {
|
||||||
|
parse_args(args).expect("应解析")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M280:`--version` 与 `--dry-run` 是 `java install` 的旗标,不是全局 `--version`。
|
||||||
|
///
|
||||||
|
/// 变异证明:把全局 `--version` 的 `if rest.is_empty()` 守卫去掉 ⇒ 本用例第一条红
|
||||||
|
/// (解析成 `Command::Version`,装版本的需求被吞掉)。
|
||||||
|
#[test]
|
||||||
|
fn pinned_version_and_dry_run_are_install_flags() {
|
||||||
|
assert_eq!(
|
||||||
|
command(&["java", "install", "21", "--version", "21.0.13+9"]),
|
||||||
|
Command::JavaInstall {
|
||||||
|
major: 21,
|
||||||
|
vendor: None,
|
||||||
|
version: Some("21.0.13+9".to_string()),
|
||||||
|
dry_run: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
command(&["java", "install", "21", "--dry-run"]),
|
||||||
|
Command::JavaInstall {
|
||||||
|
major: 21,
|
||||||
|
vendor: None,
|
||||||
|
version: None,
|
||||||
|
dry_run: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
command(&[
|
||||||
|
"java",
|
||||||
|
"install",
|
||||||
|
"21",
|
||||||
|
"--vendor",
|
||||||
|
"oracle",
|
||||||
|
"--version",
|
||||||
|
"21.0.2",
|
||||||
|
"--dry-run",
|
||||||
|
]),
|
||||||
|
Command::JavaInstall {
|
||||||
|
major: 21,
|
||||||
|
vendor: Some(JdkVendor::Oracle),
|
||||||
|
version: Some("21.0.2".to_string()),
|
||||||
|
dry_run: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 空版本串在解析层就拦住(到 core 会变成「没有任何版本能匹配」的请求)。
|
||||||
|
#[test]
|
||||||
|
fn an_empty_version_is_a_usage_error() {
|
||||||
|
assert!(parse_args(&["java", "install", "21", "--version", ""]).is_err());
|
||||||
|
assert!(parse_args(&["java", "install", "21", "--version"]).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use unlucky_core::download::blocking::block_on;
|
|
||||||
use unlucky_core::download::{DownloadEngine, RegionProfile};
|
use unlucky_core::download::{DownloadEngine, RegionProfile};
|
||||||
use unlucky_core::game::java::{
|
use unlucky_core::game::java::{
|
||||||
self, location, vendor, InstallRequest, JavaOrigin, JavaRuntime, JdkVendor, LocationMode,
|
self, location, vendor, JavaOrigin, JavaRuntime, JdkVendor, LocationMode, DEFAULT_MAJOR,
|
||||||
DEFAULT_MAJOR,
|
|
||||||
};
|
};
|
||||||
use unlucky_core::launch::request::JavaSelection;
|
use unlucky_core::launch::request::JavaSelection;
|
||||||
use unlucky_core::platform::paths::ProcessEnv;
|
use unlucky_core::platform::paths::ProcessEnv;
|
||||||
@@ -31,12 +29,10 @@ use crate::context::Ctx;
|
|||||||
use crate::error::CliError;
|
use crate::error::CliError;
|
||||||
use crate::output::Report;
|
use crate::output::Report;
|
||||||
|
|
||||||
|
pub use super::java_install::install;
|
||||||
pub use super::java_launch::{resolve_for_launch, LaunchJava};
|
pub use super::java_launch::{resolve_for_launch, LaunchJava};
|
||||||
pub use super::java_vendors::vendors;
|
pub use super::java_vendors::vendors;
|
||||||
|
|
||||||
/// 可自动安装的最老大版本(再老的 Temurin 没有 GA 产物)。
|
|
||||||
const MIN_INSTALLABLE_MAJOR: u32 = 8;
|
|
||||||
|
|
||||||
/// 装配用的 Java 选择(`--java` > `JAVA_HOME` > PATH 上的 `java`)。
|
/// 装配用的 Java 选择(`--java` > `JAVA_HOME` > PATH 上的 `java`)。
|
||||||
///
|
///
|
||||||
/// `snapshot::prepare` 调它拿 [`JavaSelection`];真正的来源优先级在
|
/// `snapshot::prepare` 调它拿 [`JavaSelection`];真正的来源优先级在
|
||||||
@@ -131,83 +127,6 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
|
|||||||
Ok(Report::new(data, human))
|
Ok(Report::new(data, human))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `ull java install <major> [--vendor X]`:回退链查版本 → 下载 → 解压 → `java -version` 验证。
|
|
||||||
///
|
|
||||||
/// 幂等:同一个归档装过就直接复用(不联网、不解压)。下载走 `core::download` 引擎,
|
|
||||||
/// 国内环境按 D7 的地区策略优先院校/国内镜像。
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// 大版本不合法、平台无产物、所有发行版都失败(网络/校验/解压/跑不起来)时返回用户错误。
|
|
||||||
pub fn install(ctx: &Ctx, major: u32, vendor: Option<JdkVendor>) -> Result<Report, CliError> {
|
|
||||||
if !(MIN_INSTALLABLE_MAJOR..=99).contains(&major) {
|
|
||||||
return Err(CliError::usage(format!(
|
|
||||||
"大版本号看起来不对:{major}(可用 8 / 11 / 17 / 21 / 25 这类 GA 大版本)"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let settings = JavaSettings::load(ctx);
|
|
||||||
let engine = build_engine(ctx);
|
|
||||||
let install_root = settings.location(ctx)?;
|
|
||||||
let request = match vendor {
|
|
||||||
Some(vendor) => {
|
|
||||||
InstallRequest::with_vendor(major, vendor).with_priority(settings.priority.clone())
|
|
||||||
}
|
|
||||||
None => InstallRequest::new(major).with_priority(settings.priority.clone()),
|
|
||||||
};
|
|
||||||
let installed =
|
|
||||||
block_on(java::install(&engine, &install_root, request)).map_err(map_java_error)?;
|
|
||||||
|
|
||||||
let data = json!({
|
|
||||||
"major": installed.major,
|
|
||||||
"vendor": installed.vendor.id(),
|
|
||||||
"release": installed.release.release_name,
|
|
||||||
"version": installed.release.version,
|
|
||||||
"semver": installed.release.semver,
|
|
||||||
"archive": installed.release.archive.name,
|
|
||||||
"sha256": installed.release.archive.sha256,
|
|
||||||
"size": installed.release.archive.size,
|
|
||||||
"root": installed.root.display().to_string(),
|
|
||||||
"install_mode": installed.mode.as_str(),
|
|
||||||
"binary": installed.binary().display().to_string(),
|
|
||||||
"reused": installed.reused(),
|
|
||||||
"source": installed.downloaded,
|
|
||||||
"bytes": installed.bytes,
|
|
||||||
"files": installed.files,
|
|
||||||
"extracted": installed.extracted,
|
|
||||||
"chain": installed.chain.to_json(),
|
|
||||||
"runtime": candidate_json(&installed.runtime),
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut human = Vec::new();
|
|
||||||
if installed.reused() {
|
|
||||||
human.push(format!(
|
|
||||||
"Java {}({})已就绪({}),未走网络",
|
|
||||||
installed.major,
|
|
||||||
installed.vendor.id(),
|
|
||||||
installed.root.display()
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
human.push(format!(
|
|
||||||
"已安装 Java {}({},来源 {},{} 字节 / {} 个文件)",
|
|
||||||
installed.major,
|
|
||||||
installed.release.summary(),
|
|
||||||
installed.downloaded.as_deref().unwrap_or("?"),
|
|
||||||
installed.bytes,
|
|
||||||
installed.files
|
|
||||||
));
|
|
||||||
}
|
|
||||||
human.push(format!("落点:{}", installed.location_line()));
|
|
||||||
human.push(format!("可执行文件:{}", installed.binary().display()));
|
|
||||||
if let Some(version) = &installed.runtime.version {
|
|
||||||
human.push(format!("探测:{version}"));
|
|
||||||
}
|
|
||||||
for note in installed.chain.degradations() {
|
|
||||||
human.push(format!("降级:{note}"));
|
|
||||||
}
|
|
||||||
human.push("`%prog% launch` 会自动优先用它".to_string());
|
|
||||||
Ok(Report::new(data, human))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 设置里与 JDK 有关的两项(`java.installRoot` / `java.vendorPriority`)。
|
/// 设置里与 JDK 有关的两项(`java.installRoot` / `java.vendorPriority`)。
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
pub(crate) struct JavaSettings {
|
pub(crate) struct JavaSettings {
|
||||||
@@ -255,7 +174,7 @@ impl JavaSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 真正要装到哪(解析失败 → 用户错误,带可照着做的提示)。
|
/// 真正要装到哪(解析失败 → 用户错误,带可照着做的提示)。
|
||||||
fn location(&self, ctx: &Ctx) -> Result<java::JavaInstallRoot, CliError> {
|
pub(super) fn location(&self, ctx: &Ctx) -> Result<java::JavaInstallRoot, CliError> {
|
||||||
java::JavaInstallRoot::resolve(
|
java::JavaInstallRoot::resolve(
|
||||||
ctx.data.mode,
|
ctx.data.mode,
|
||||||
&ctx.layout(),
|
&ctx.layout(),
|
||||||
@@ -268,7 +187,7 @@ impl JavaSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 落点模式 → 人读一行(含「没设覆盖键时默认落在哪」的解释)。
|
/// 落点模式 → 人读一行(含「没设覆盖键时默认落在哪」的解释)。
|
||||||
fn describe_mode(mode: LocationMode, override_root: Option<&Path>) -> String {
|
pub(super) fn describe_mode(mode: LocationMode, override_root: Option<&Path>) -> String {
|
||||||
match mode {
|
match mode {
|
||||||
LocationMode::Custom => format!(
|
LocationMode::Custom => format!(
|
||||||
"custom(java.installRoot = {})",
|
"custom(java.installRoot = {})",
|
||||||
@@ -282,7 +201,7 @@ fn describe_mode(mode: LocationMode, override_root: Option<&Path>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 一个运行时的 `--json` 形状。
|
/// 一个运行时的 `--json` 形状。
|
||||||
fn candidate_json(runtime: &JavaRuntime) -> Value {
|
pub(super) fn candidate_json(runtime: &JavaRuntime) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"binary": runtime.binary.display().to_string(),
|
"binary": runtime.binary.display().to_string(),
|
||||||
"source": runtime.origin.as_str(),
|
"source": runtime.origin.as_str(),
|
||||||
@@ -314,7 +233,7 @@ pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// JDK 下载/解压错误 → CLI 错误(保留稳定码与提示)。
|
/// JDK 下载/解压错误 → CLI 错误(保留稳定码与提示)。
|
||||||
fn map_java_error(error: java::JavaError) -> CliError {
|
pub(super) fn map_java_error(error: java::JavaError) -> CliError {
|
||||||
let cli = CliError::user(error.code(), error.message().to_string());
|
let cli = CliError::user(error.code(), error.message().to_string());
|
||||||
match error.hint() {
|
match error.hint() {
|
||||||
Some(hint) => cli.with_hint(hint.to_string()),
|
Some(hint) => cli.with_hint(hint.to_string()),
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
//! `ull java install`:回退链查版本 → 下载 → 解压 → `java -version` 验证(以及 `--dry-run`)。
|
||||||
|
//!
|
||||||
|
//! 从 `commands/java.rs` 拆出来只为守 400 行硬线(M280 的 `--version` / `--dry-run` 让那边到
|
||||||
|
//! 421 行);`list` / 设置读取 / 渲染助手仍在 `java.rs`。与 `java_launch.rs` / `java_vendors.rs`
|
||||||
|
//! 同一个拆法。
|
||||||
|
//!
|
||||||
|
//! 版本这一维(M280):`--version` 不给就是该家**最新 GA**(与加旗标之前逐字节相同);
|
||||||
|
//! 给了就要求**精确**那一版——能按版本取的只有 Temurin / Oracle,Microsoft 只认它内置的
|
||||||
|
//! 那份快照,Zulu 只有 latest。钉不了时 core 报错并列出可用版本,**绝不回落到最新**。
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use unlucky_core::download::blocking::block_on;
|
||||||
|
use unlucky_core::download::DownloadEngine;
|
||||||
|
use unlucky_core::game::java::{self, InstallRequest, JdkVendor};
|
||||||
|
|
||||||
|
use super::java::{build_engine, candidate_json, map_java_error, JavaSettings};
|
||||||
|
use crate::context::Ctx;
|
||||||
|
use crate::error::CliError;
|
||||||
|
use crate::output::Report;
|
||||||
|
|
||||||
|
/// 可自动安装的最老大版本(再老的 Temurin 没有 GA 产物)。
|
||||||
|
const MIN_INSTALLABLE_MAJOR: u32 = 8;
|
||||||
|
|
||||||
|
/// `ull java install <major> [--vendor X] [--version V] [--dry-run]`:
|
||||||
|
/// 回退链查版本 → 下载 → 解压 → `java -version` 验证。
|
||||||
|
///
|
||||||
|
/// 幂等:同一个归档装过就直接复用(不联网、不解压)。下载走 `core::download` 引擎,
|
||||||
|
/// 国内环境按 D7 的地区策略优先院校/国内镜像。
|
||||||
|
///
|
||||||
|
/// `--dry-run` 只查版本:打印最终会打哪个版本串、哪个 URL,一个字节都不落盘。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 大版本不合法、平台无产物、该家没有这个版本、所有发行版都失败(网络/校验/解压/跑不起来)时返回用户错误。
|
||||||
|
pub fn install(
|
||||||
|
ctx: &Ctx,
|
||||||
|
major: u32,
|
||||||
|
vendor: Option<JdkVendor>,
|
||||||
|
version: Option<&str>,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<Report, CliError> {
|
||||||
|
if !(MIN_INSTALLABLE_MAJOR..=99).contains(&major) {
|
||||||
|
return Err(CliError::usage(format!(
|
||||||
|
"大版本号看起来不对:{major}(可用 8 / 11 / 17 / 21 / 25 这类 GA 大版本)"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let settings = JavaSettings::load(ctx);
|
||||||
|
let engine = build_engine(ctx);
|
||||||
|
let install_root = settings.location(ctx)?;
|
||||||
|
let request = match vendor {
|
||||||
|
Some(vendor) => {
|
||||||
|
InstallRequest::with_vendor(major, vendor).with_priority(settings.priority.clone())
|
||||||
|
}
|
||||||
|
None => InstallRequest::new(major).with_priority(settings.priority.clone()),
|
||||||
|
}
|
||||||
|
.with_version(version.map(str::to_string));
|
||||||
|
if dry_run {
|
||||||
|
return dry_run_report(&engine, &install_root, &request);
|
||||||
|
}
|
||||||
|
let installed =
|
||||||
|
block_on(java::install(&engine, &install_root, request.clone())).map_err(map_java_error)?;
|
||||||
|
|
||||||
|
let data = json!({
|
||||||
|
"major": installed.major,
|
||||||
|
"vendor": installed.vendor.id(),
|
||||||
|
"release": installed.release.release_name,
|
||||||
|
"version": installed.release.version,
|
||||||
|
"requested_version": request.version,
|
||||||
|
"semver": installed.release.semver,
|
||||||
|
"archive": installed.release.archive.name,
|
||||||
|
"sha256": installed.release.archive.sha256,
|
||||||
|
"size": installed.release.archive.size,
|
||||||
|
"root": installed.root.display().to_string(),
|
||||||
|
"install_mode": installed.mode.as_str(),
|
||||||
|
"binary": installed.binary().display().to_string(),
|
||||||
|
"reused": installed.reused(),
|
||||||
|
"source": installed.downloaded,
|
||||||
|
"bytes": installed.bytes,
|
||||||
|
"files": installed.files,
|
||||||
|
"extracted": installed.extracted,
|
||||||
|
"chain": installed.chain.to_json(),
|
||||||
|
"runtime": candidate_json(&installed.runtime),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut human = Vec::new();
|
||||||
|
if installed.reused() {
|
||||||
|
human.push(format!(
|
||||||
|
"Java {}({})已就绪({}),未走网络",
|
||||||
|
installed.major,
|
||||||
|
installed.vendor.id(),
|
||||||
|
installed.root.display()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
human.push(format!(
|
||||||
|
"已安装 Java {}({},来源 {},{} 字节 / {} 个文件)",
|
||||||
|
installed.major,
|
||||||
|
installed.release.summary(),
|
||||||
|
installed.downloaded.as_deref().unwrap_or("?"),
|
||||||
|
installed.bytes,
|
||||||
|
installed.files
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(wanted) = &request.version {
|
||||||
|
human.push(format!(
|
||||||
|
"指定版本:{wanted} → 命中 {}",
|
||||||
|
installed.release.version
|
||||||
|
));
|
||||||
|
}
|
||||||
|
human.push(format!("落点:{}", installed.location_line()));
|
||||||
|
human.push(format!("可执行文件:{}", installed.binary().display()));
|
||||||
|
if let Some(version) = &installed.runtime.version {
|
||||||
|
human.push(format!("探测:{version}"));
|
||||||
|
}
|
||||||
|
for note in installed.chain.degradations() {
|
||||||
|
human.push(format!("降级:{note}"));
|
||||||
|
}
|
||||||
|
human.push("`%prog% launch` 会自动优先用它".to_string());
|
||||||
|
Ok(Report::new(data, human))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `--dry-run`:查一次版本(与真装走同一条选择路径),打印版本串与 URL,不下载、不解压、不写盘。
|
||||||
|
///
|
||||||
|
/// 查版本本身仍要联网,但它把「会装哪一版、打哪个 URL」在下载之前摆出来——
|
||||||
|
/// 验证版本选择逻辑不必先下 200 MB。
|
||||||
|
fn dry_run_report(
|
||||||
|
engine: &DownloadEngine,
|
||||||
|
location: &java::JavaInstallRoot,
|
||||||
|
request: &InstallRequest,
|
||||||
|
) -> Result<Report, CliError> {
|
||||||
|
let (release, root, ready) =
|
||||||
|
block_on(java::plan(engine, location, request)).map_err(map_java_error)?;
|
||||||
|
let data = json!({
|
||||||
|
"dry_run": true,
|
||||||
|
"major": release.major,
|
||||||
|
"vendor": release.vendor.id(),
|
||||||
|
"version": release.version,
|
||||||
|
"requested_version": request.version,
|
||||||
|
"semver": release.semver,
|
||||||
|
"release": release.release_name,
|
||||||
|
"archive": release.archive.name,
|
||||||
|
"url": release.archive.url,
|
||||||
|
"sha256": release.archive.sha256,
|
||||||
|
"size": release.archive.size,
|
||||||
|
"root": root.display().to_string(),
|
||||||
|
"install_mode": location.mode.as_str(),
|
||||||
|
"already_installed": ready,
|
||||||
|
});
|
||||||
|
let mut human = vec![format!(
|
||||||
|
"将安装 Java {}({})",
|
||||||
|
release.major,
|
||||||
|
release.summary()
|
||||||
|
)];
|
||||||
|
if let Some(wanted) = &request.version {
|
||||||
|
human.push(format!("指定版本:{wanted} → 命中 {}", release.version));
|
||||||
|
}
|
||||||
|
human.push(format!("版本:{}", release.version));
|
||||||
|
human.push(format!("URL:{}", release.archive.url));
|
||||||
|
human.push(format!(
|
||||||
|
"落点:{}({})",
|
||||||
|
root.display(),
|
||||||
|
location.mode.as_str()
|
||||||
|
));
|
||||||
|
human.push(format!(
|
||||||
|
"已装好:{}",
|
||||||
|
if ready {
|
||||||
|
"是(会直接复用,不联网)"
|
||||||
|
} else {
|
||||||
|
"否(会下载)"
|
||||||
|
}
|
||||||
|
));
|
||||||
|
human.push("(--dry-run:没有下载、没有解压、没有写盘)".to_string());
|
||||||
|
Ok(Report::new(data, human))
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ pub mod install;
|
|||||||
pub mod instance_launch;
|
pub mod instance_launch;
|
||||||
pub mod instances;
|
pub mod instances;
|
||||||
pub mod java;
|
pub mod java;
|
||||||
|
pub mod java_install;
|
||||||
pub mod java_launch;
|
pub mod java_launch;
|
||||||
pub mod java_vendors;
|
pub mod java_vendors;
|
||||||
pub mod launch;
|
pub mod launch;
|
||||||
@@ -58,8 +59,13 @@ pub fn run(invocation: &Invocation) -> Result<Outcome, CliError> {
|
|||||||
config::set(ctx, &args.key, &args.value)
|
config::set(ctx, &args.key, &args.value)
|
||||||
}),
|
}),
|
||||||
Command::JavaList => with_ctx(&invocation.globals, java::list),
|
Command::JavaList => with_ctx(&invocation.globals, java::list),
|
||||||
Command::JavaInstall { major, vendor } => with_ctx(&invocation.globals, |ctx| {
|
Command::JavaInstall {
|
||||||
java::install(ctx, *major, *vendor)
|
major,
|
||||||
|
vendor,
|
||||||
|
version,
|
||||||
|
dry_run,
|
||||||
|
} => with_ctx(&invocation.globals, |ctx| {
|
||||||
|
java::install(ctx, *major, *vendor, version.as_deref(), *dry_run)
|
||||||
}),
|
}),
|
||||||
Command::JavaVendors => with_ctx(&invocation.globals, java::vendors),
|
Command::JavaVendors => with_ctx(&invocation.globals, java::vendors),
|
||||||
Command::UpdateCheck(args) => {
|
Command::UpdateCheck(args) => {
|
||||||
|
|||||||
@@ -236,7 +236,9 @@ fn java_subcommands() {
|
|||||||
invoke(&["java", "install", "21"]).command,
|
invoke(&["java", "install", "21"]).command,
|
||||||
Command::JavaInstall {
|
Command::JavaInstall {
|
||||||
major: 21,
|
major: 21,
|
||||||
vendor: None
|
vendor: None,
|
||||||
|
version: None,
|
||||||
|
dry_run: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// D23:`--vendor` 钉死发行版(别名也认),`java vendors` 列出四家。
|
// D23:`--vendor` 钉死发行版(别名也认),`java vendors` 列出四家。
|
||||||
@@ -245,6 +247,8 @@ fn java_subcommands() {
|
|||||||
Command::JavaInstall {
|
Command::JavaInstall {
|
||||||
major: 21,
|
major: 21,
|
||||||
vendor: Some(unlucky_core::game::java::JdkVendor::Oracle),
|
vendor: Some(unlucky_core::game::java::JdkVendor::Oracle),
|
||||||
|
version: None,
|
||||||
|
dry_run: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -252,6 +256,8 @@ fn java_subcommands() {
|
|||||||
Command::JavaInstall {
|
Command::JavaInstall {
|
||||||
major: 17,
|
major: 17,
|
||||||
vendor: Some(unlucky_core::game::java::JdkVendor::Temurin),
|
vendor: Some(unlucky_core::game::java::JdkVendor::Temurin),
|
||||||
|
version: None,
|
||||||
|
dry_run: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(invoke(&["java", "vendors"]).command, Command::JavaVendors);
|
assert_eq!(invoke(&["java", "vendors"]).command, Command::JavaVendors);
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ use super::vendor::JdkVendor;
|
|||||||
/// Adoptium API 根。
|
/// Adoptium API 根。
|
||||||
pub const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3";
|
pub const ADOPTIUM_API_BASE: &str = "https://api.adoptium.net/v3";
|
||||||
|
|
||||||
|
mod version;
|
||||||
|
|
||||||
|
pub use version::{fetch_version, parse_release, parse_release_list, release_url, releases_url};
|
||||||
|
|
||||||
/// 托管 JDK 的默认供应商标识(D13 v1 的目录名 `jdk-<major>-vendor` 沿用它)。
|
/// 托管 JDK 的默认供应商标识(D13 v1 的目录名 `jdk-<major>-vendor` 沿用它)。
|
||||||
pub const JDK_VENDOR: &str = "temurin";
|
pub const JDK_VENDOR: &str = "temurin";
|
||||||
|
|
||||||
@@ -112,7 +116,7 @@ pub async fn fetch_latest(
|
|||||||
|
|
||||||
/// 响应里的 `package` 子对象。
|
/// 响应里的 `package` 子对象。
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ApiPackage {
|
pub(super) struct ApiPackage {
|
||||||
name: String,
|
name: String,
|
||||||
link: String,
|
link: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -121,9 +125,9 @@ struct ApiPackage {
|
|||||||
size: u64,
|
size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 响应里的 `binary` 子对象。
|
/// 响应里的 `binary` / `binaries[]` 子对象(`assets/latest` 用前者,`assets/release_name` 用后者)。
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ApiBinary {
|
pub(super) struct ApiBinary {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
os: Option<String>,
|
os: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -134,9 +138,9 @@ struct ApiBinary {
|
|||||||
package: Option<ApiPackage>,
|
package: Option<ApiPackage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 响应里的 `version` 子对象。
|
/// 响应里的 `version` / `version_data` 子对象(两者字段同形)。
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ApiVersion {
|
pub(super) struct ApiVersion {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
major: Option<u32>,
|
major: Option<u32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -154,29 +158,30 @@ struct ApiAsset {
|
|||||||
binary: Option<ApiBinary>,
|
binary: Option<ApiBinary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiAsset {
|
impl ApiBinary {
|
||||||
/// 是否就是目标平台的 jdk 归档(`arch` 参数不过滤,必须自己判)。
|
/// 是否就是目标平台的 jdk 归档(`arch` 参数不过滤,必须自己判)。
|
||||||
fn matches(&self, target: AdoptiumTarget) -> bool {
|
pub(super) fn matches(&self, target: AdoptiumTarget) -> bool {
|
||||||
let Some(binary) = &self.binary else {
|
let same_os = self.os.as_deref().is_some_and(|os| os == target.os);
|
||||||
return false;
|
let same_arch = self
|
||||||
};
|
|
||||||
let same_os = binary.os.as_deref().is_some_and(|os| os == target.os);
|
|
||||||
let same_arch = binary
|
|
||||||
.architecture
|
.architecture
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_some_and(|arch| arch == target.arch);
|
.is_some_and(|arch| arch == target.arch);
|
||||||
let is_jdk = binary.image_type.as_deref() != Some("jre");
|
let is_jdk = self.image_type.as_deref() != Some("jre");
|
||||||
same_os && same_arch && is_jdk && binary.package.is_some()
|
same_os && same_arch && is_jdk && self.package.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 转成对外结果(校验 sha256 形态)。
|
/// 转成对外结果(校验 sha256 形态)。
|
||||||
fn into_release(self) -> Result<JdkRelease, JavaError> {
|
///
|
||||||
let package = self
|
/// `assets/latest` 与 `assets/release_name` 的响应把版本包在不同的外层字段里
|
||||||
.binary
|
/// (`version` / `version_data` + `release_name`),所以这两个由调用方传进来。
|
||||||
.and_then(|binary| binary.package)
|
pub(super) fn into_release(
|
||||||
.ok_or_else(|| {
|
self,
|
||||||
JavaError::new(JavaErrorKind::Metadata, "Adoptium 条目缺少 package 子对象")
|
release_name: String,
|
||||||
})?;
|
version: Option<ApiVersion>,
|
||||||
|
) -> Result<JdkRelease, JavaError> {
|
||||||
|
let package = self.package.ok_or_else(|| {
|
||||||
|
JavaError::new(JavaErrorKind::Metadata, "Adoptium 条目缺少 package 子对象")
|
||||||
|
})?;
|
||||||
let sha256 = package
|
let sha256 = package
|
||||||
.checksum
|
.checksum
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -189,7 +194,6 @@ impl ApiAsset {
|
|||||||
)
|
)
|
||||||
.with_hint("换一个大版本,或手动装 JDK 后设 JAVA_HOME")
|
.with_hint("换一个大版本,或手动装 JDK 后设 JAVA_HOME")
|
||||||
})?;
|
})?;
|
||||||
let version = self.version;
|
|
||||||
let major = version
|
let major = version
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|version| version.major)
|
.and_then(|version| version.major)
|
||||||
@@ -200,7 +204,7 @@ impl ApiAsset {
|
|||||||
format!("Adoptium 条目里读不出大版本号({})", package.name),
|
format!("Adoptium 条目里读不出大版本号({})", package.name),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let native_version = version_from_release_name(&self.release_name)
|
let native_version = version_from_release_name(&release_name)
|
||||||
.or_else(|| version.as_ref().and_then(|version| version.semver.clone()))
|
.or_else(|| version.as_ref().and_then(|version| version.semver.clone()))
|
||||||
.unwrap_or_else(|| major.to_string());
|
.unwrap_or_else(|| major.to_string());
|
||||||
Ok(JdkRelease {
|
Ok(JdkRelease {
|
||||||
@@ -208,7 +212,7 @@ impl ApiAsset {
|
|||||||
major,
|
major,
|
||||||
version: native_version,
|
version: native_version,
|
||||||
semver: version.and_then(|version| version.semver),
|
semver: version.and_then(|version| version.semver),
|
||||||
release_name: self.release_name,
|
release_name,
|
||||||
archive: JdkArchive {
|
archive: JdkArchive {
|
||||||
name: package.name,
|
name: package.name,
|
||||||
url: package.link,
|
url: package.link,
|
||||||
@@ -219,6 +223,23 @@ impl ApiAsset {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ApiAsset {
|
||||||
|
/// 是否就是目标平台的 jdk 归档(`arch` 参数不过滤,必须自己判)。
|
||||||
|
fn matches(&self, target: AdoptiumTarget) -> bool {
|
||||||
|
self.binary
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|binary| binary.matches(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 转成对外结果。
|
||||||
|
fn into_release(self) -> Result<JdkRelease, JavaError> {
|
||||||
|
let binary = self.binary.ok_or_else(|| {
|
||||||
|
JavaError::new(JavaErrorKind::Metadata, "Adoptium 条目缺少 binary 子对象")
|
||||||
|
})?;
|
||||||
|
binary.into_release(self.release_name, self.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 从产物名兜底读大版本(`OpenJDK21U-jdk_x64_windows_hotspot_…` → `21`)。
|
/// 从产物名兜底读大版本(`OpenJDK21U-jdk_x64_windows_hotspot_…` → `21`)。
|
||||||
fn major_from_name(name: &str) -> Option<u32> {
|
fn major_from_name(name: &str) -> Option<u32> {
|
||||||
let rest = name.split_once("OpenJDK")?.1;
|
let rest = name.split_once("OpenJDK")?.1;
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
//! Adoptium(Eclipse Temurin)的**指定版本**查询:先枚举该大版本的 GA,把用户给的版本串对上
|
||||||
|
//! 一条 release,再用 `assets/release_name` 取那一份归档。
|
||||||
|
//!
|
||||||
|
//! 为什么是两跳(2026-11-12 逐个端点 curl 验过,见 mission M280 报告):
|
||||||
|
//!
|
||||||
|
//! 1. `/v3/assets/version/{version}` 在 `/q/openapi` 里有这条路由,但**部署上的服务回 404 空体**
|
||||||
|
//! (`21.0.13+9` 与 `21.0.13%2B9` 都一样),所以不用它;
|
||||||
|
//! 2. `GET /v3/assets/release_name/{vendor}/{release_name}`(如 `eclipse/jdk-21.0.13+9`)回 200,
|
||||||
|
//! 体是 `{release_name, version_data{…}, binaries[]{os,architecture,image_type,package{…}}}` ——
|
||||||
|
//! 这就是「指定版本」那一跳;
|
||||||
|
//! 3. 但 `release_name` 要完整名(`jdk-21.0.13+9`),用户给的常是公共版本号(`21.0.13`),
|
||||||
|
//! 所以先打 `GET /v3/assets/feature_releases/{major}/ga?…&sort_order=DESC&page_size=20`
|
||||||
|
//! (openapi 里 `page_size` 的 `maximum` 就是 20)把前者翻成后者。同一份响应的 release 名
|
||||||
|
//! 列表同时是「这个大版本有哪些版本」的唯一来源——版本不存在时就靠它列给人看。
|
||||||
|
//!
|
||||||
|
//! 这里**只有取**:匹配用 [`matches_version`],匹配不上时把可用清单交给
|
||||||
|
//! [`version_pin_error`] 生成错误。**不换家、不回落 latest**(那是 `install` / `source` 的事)。
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::{ApiBinary, ApiVersion, ADOPTIUM_API_BASE};
|
||||||
|
use crate::game::java::catalog::{canonical_version, matches_version, version_pin_error};
|
||||||
|
use crate::game::java::error::{JavaError, JavaErrorKind};
|
||||||
|
use crate::game::java::net;
|
||||||
|
use crate::game::java::release::JdkRelease;
|
||||||
|
use crate::game::java::target::AdoptiumTarget;
|
||||||
|
use crate::game::java::vendor::JdkVendor;
|
||||||
|
|
||||||
|
/// `feature_releases` 的每页条数(Adoptium 允许的上限)。
|
||||||
|
pub const PAGE_SIZE: usize = 20;
|
||||||
|
|
||||||
|
/// 某大版本的 GA 清单地址(新 → 旧)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn releases_url(major: u32, target: AdoptiumTarget) -> String {
|
||||||
|
format!(
|
||||||
|
"{ADOPTIUM_API_BASE}/assets/feature_releases/{major}/ga?os={}&arch={}&image_type=jdk&page=0&page_size={PAGE_SIZE}&sort_order=DESC&vendor=eclipse",
|
||||||
|
target.os, target.arch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 指定 release 名的产物地址。
|
||||||
|
#[must_use]
|
||||||
|
pub fn release_url(release_name: &str, target: AdoptiumTarget) -> String {
|
||||||
|
format!(
|
||||||
|
"{ADOPTIUM_API_BASE}/assets/release_name/eclipse/{release_name}?os={}&arch={}&image_type=jdk&vendor=eclipse",
|
||||||
|
target.os, target.arch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `feature_releases` / `release_name` 响应里的一条(只有这几项用得上)。
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ApiRelease {
|
||||||
|
#[serde(default)]
|
||||||
|
release_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
version_data: Option<ApiVersion>,
|
||||||
|
#[serde(default)]
|
||||||
|
binaries: Vec<ApiBinary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GA 清单响应 → release 名列表(`jdk-21.0.13+9`,上游给的顺序即新 → 旧)。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 响应不是 JSON 时返回 [`JavaErrorKind::Metadata`](调用方据此降级到下一家发行版)。
|
||||||
|
pub fn parse_release_list(json: &str) -> Result<Vec<String>, JavaError> {
|
||||||
|
let releases: Vec<ApiRelease> = serde_json::from_str(json).map_err(|error| {
|
||||||
|
JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!("Adoptium GA 清单不是预期 JSON:{error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(releases
|
||||||
|
.into_iter()
|
||||||
|
.map(|release| release.release_name)
|
||||||
|
.filter(|name| !canonical_version(name).is_empty())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 指定版本的响应 → 一份可下载归档(挑出目标平台的 jdk 那一条)。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 响应不是 JSON、没有目标平台产物、sha256 不合法时返回错误。
|
||||||
|
pub fn parse_release(
|
||||||
|
json: &str,
|
||||||
|
expected: &str,
|
||||||
|
target: AdoptiumTarget,
|
||||||
|
) -> Result<JdkRelease, JavaError> {
|
||||||
|
let release: ApiRelease = serde_json::from_str(json).map_err(|error| {
|
||||||
|
JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!("Adoptium 指定版本响应不是预期 JSON:{error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let name = if release.release_name.is_empty() {
|
||||||
|
expected.to_string()
|
||||||
|
} else {
|
||||||
|
release.release_name
|
||||||
|
};
|
||||||
|
let binary = release
|
||||||
|
.binaries
|
||||||
|
.into_iter()
|
||||||
|
.find(|binary| binary.matches(target))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!(
|
||||||
|
"Adoptium 的 {name} 没有 {}/{} 的 jdk 产物",
|
||||||
|
target.os, target.arch
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
binary.into_release(name, release.version_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查一次「某大版本的指定版本」。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 清单不可达、该大版本没有版本(错误里列出可用版本)、目标平台无产物、sha256 缺失时返回错误。
|
||||||
|
pub async fn fetch_version(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
major: u32,
|
||||||
|
target: AdoptiumTarget,
|
||||||
|
wanted: &str,
|
||||||
|
) -> Result<JdkRelease, JavaError> {
|
||||||
|
let list = net::get_text(client, &releases_url(major, target), "Adoptium GA 清单").await?;
|
||||||
|
let releases = parse_release_list(&list)?;
|
||||||
|
let picked = releases
|
||||||
|
.iter()
|
||||||
|
.find(|name| matches_version(wanted, name))
|
||||||
|
.cloned();
|
||||||
|
let Some(name) = picked else {
|
||||||
|
let versions = releases
|
||||||
|
.iter()
|
||||||
|
.map(|name| canonical_version(name).to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
return Err(version_pin_error(
|
||||||
|
JdkVendor::Temurin,
|
||||||
|
major,
|
||||||
|
wanted,
|
||||||
|
&versions,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let body = net::get_text(client, &release_url(&name, target), "Adoptium 指定版本产物").await?;
|
||||||
|
let release = parse_release(&body, &name, target)?;
|
||||||
|
if release.major != major {
|
||||||
|
return Err(JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!(
|
||||||
|
"Adoptium 的 {name} 属于 Java {},不是请求的 {major}",
|
||||||
|
release.major
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(release)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{parse_release, parse_release_list, release_url, releases_url};
|
||||||
|
use crate::game::java::error::JavaErrorKind;
|
||||||
|
use crate::game::java::target::AdoptiumTarget;
|
||||||
|
|
||||||
|
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
|
||||||
|
|
||||||
|
/// 真机 `feature_releases/21/ga` 响应的裁剪(2026-11-12,只留两条与用到的字段)。
|
||||||
|
fn list_body() -> String {
|
||||||
|
r#"[
|
||||||
|
{"release_name":"jdk-21.0.13+9","version_data":{"major":21,"semver":"21.0.13+9.0.LTS"},"binaries":[]},
|
||||||
|
{"release_name":"jdk-21.0.12+8","version_data":{"major":21,"semver":"21.0.12+8.0.LTS"},"binaries":[]}
|
||||||
|
]"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 真机 `assets/release_name/eclipse/jdk-21.0.13+9` 响应的裁剪。
|
||||||
|
fn release_body() -> String {
|
||||||
|
r#"{
|
||||||
|
"release_name": "jdk-21.0.13+9",
|
||||||
|
"version_data": {"major": 21, "semver": "21.0.13+9.0.LTS", "build": 9},
|
||||||
|
"binaries": [
|
||||||
|
{
|
||||||
|
"os": "windows", "architecture": "x64", "image_type": "jdk", "jvm_impl": "hotspot",
|
||||||
|
"package": {
|
||||||
|
"name": "OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip",
|
||||||
|
"link": "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.13%2B9/OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip",
|
||||||
|
"checksum": "a4dbaa05fca4bd89d84584e58c459bd732efebb1722af80ab13c6516832004f9",
|
||||||
|
"size": 205421281
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn urls_target_the_versioned_endpoints_never_latest() {
|
||||||
|
let releases = releases_url(21, WINDOWS);
|
||||||
|
assert!(releases.starts_with("https://api.adoptium.net/v3/assets/feature_releases/21/ga?"));
|
||||||
|
assert!(releases.contains("page_size=20"));
|
||||||
|
assert!(releases.contains("sort_order=DESC"));
|
||||||
|
assert!(!releases.contains("latest"));
|
||||||
|
let release = release_url("jdk-21.0.13+9", WINDOWS);
|
||||||
|
assert!(release
|
||||||
|
.starts_with("https://api.adoptium.net/v3/assets/release_name/eclipse/jdk-21.0.13+9?"));
|
||||||
|
assert!(release.contains("os=windows") && release.contains("arch=x64"));
|
||||||
|
assert!(!release.contains("latest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_list_keeps_the_upstream_order_and_names() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_release_list(&list_body()).expect("应解析"),
|
||||||
|
vec!["jdk-21.0.13+9".to_string(), "jdk-21.0.12+8".to_string()]
|
||||||
|
);
|
||||||
|
let error = parse_release_list("<html>502</html>").expect_err("HTML 应被拒");
|
||||||
|
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_body_becomes_the_pinned_archive() {
|
||||||
|
let release = parse_release(&release_body(), "jdk-21.0.13+9", WINDOWS).expect("应解析");
|
||||||
|
assert_eq!(release.version, "21.0.13+9", "版本串取自 release 名");
|
||||||
|
assert_eq!(release.semver.as_deref(), Some("21.0.13+9.0.LTS"));
|
||||||
|
assert_eq!(release.major, 21);
|
||||||
|
assert_eq!(
|
||||||
|
release.archive.name,
|
||||||
|
"OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip"
|
||||||
|
);
|
||||||
|
assert_eq!(release.archive.size, 205_421_281);
|
||||||
|
assert!(release.archive.url.contains("temurin21-binaries"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_platform_has_no_pinned_archive() {
|
||||||
|
let error = parse_release(
|
||||||
|
&release_body(),
|
||||||
|
"jdk-21.0.13+9",
|
||||||
|
AdoptiumTarget::new("windows", "x86-32"),
|
||||||
|
)
|
||||||
|
.expect_err("x86-32 应没有条目");
|
||||||
|
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,15 @@
|
|||||||
//!
|
//!
|
||||||
//! [`BASELINE_MAJOR`](17)是 D13 定的基准:缺 Java 时先装它。它同时也是目录的一员,
|
//! [`BASELINE_MAJOR`](17)是 D13 定的基准:缺 Java 时先装它。它同时也是目录的一员,
|
||||||
//! 但不是唯一一员——`26.1` 起 MC 要 25,`1.12.2` 只能吃 8,所以要能挑。
|
//! 但不是唯一一员——`26.1` 起 MC 要 25,`1.12.2` 只能吃 8,所以要能挑。
|
||||||
|
//!
|
||||||
|
//! ## 指定版本(M280)
|
||||||
|
//!
|
||||||
|
//! 「用户给的版本串是不是这一份」只有一个判据:[`matches_version`]。四家的版本串语义不同
|
||||||
|
//! (Temurin `21.0.13+9`、Oracle `21.0.2`、MS `21.0.13.0.2`、Zulu `21.0.13.0.3`),
|
||||||
|
//! 但「相等」这件事可以统一:先按 [`public_version`] 去掉构建号段(`+9`)再比。
|
||||||
|
//! 各家模块只负责**把候选版本喂进来**,不许自己再写一份前缀/包含判断。
|
||||||
|
|
||||||
|
use super::error::{JavaError, JavaErrorKind};
|
||||||
use super::vendor::{self, JdkVendor};
|
use super::vendor::{self, JdkVendor};
|
||||||
|
|
||||||
/// D13 的基准 JDK 大版本(没有上下文时「装哪个」的默认答案)。
|
/// D13 的基准 JDK 大版本(没有上下文时「装哪个」的默认答案)。
|
||||||
@@ -104,11 +112,148 @@ pub fn default_major(required: Option<u32>) -> u32 {
|
|||||||
.unwrap_or(BASELINE_MAJOR)
|
.unwrap_or(BASELINE_MAJOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 版本串的规范化:去首尾空白 + 去掉 `jdk-` 前缀(Adoptium 的 release 名是 `jdk-21.0.13+9`)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn canonical_version(text: &str) -> &str {
|
||||||
|
let trimmed = text.trim();
|
||||||
|
trimmed
|
||||||
|
.strip_prefix("jdk-")
|
||||||
|
.or_else(|| trimmed.strip_prefix("JDK-"))
|
||||||
|
.unwrap_or(trimmed)
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 版本串的「公共部分」:`21.0.13+9` / `21.0.13+9.0.LTS` → `21.0.13`(`+` 之后是构建号)。
|
||||||
|
///
|
||||||
|
/// 目录 `/ 戳记 / --json` 用的是 vendor 原生整串,只有「是不是同一个版本」这件事看公共部分。
|
||||||
|
#[must_use]
|
||||||
|
pub fn public_version(text: &str) -> &str {
|
||||||
|
let canonical = canonical_version(text);
|
||||||
|
let end = canonical.find('+').unwrap_or(canonical.len());
|
||||||
|
canonical[..end].trim_end()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建号的首段数字(`+9` / `+9.0.LTS` / `+9-LTS` 都归成 `9`;没有 `+` 段时 `None`)。
|
||||||
|
fn build_head(text: &str) -> Option<u32> {
|
||||||
|
let canonical = canonical_version(text);
|
||||||
|
let rest = canonical.get(canonical.find('+')? + 1..)?;
|
||||||
|
let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
|
||||||
|
digits.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户点名的版本就是这份候选吗(四家共用的**唯一**判据)。
|
||||||
|
///
|
||||||
|
/// 三种情形都算命中:整串相同(`21.0.13+9`)、只给公共部分(`21.0.13` ↔ `21.0.13+9`)、
|
||||||
|
/// 构建号首段相同(`21.0.13+9` ↔ `21.0.13+9.0.LTS`)。
|
||||||
|
/// **不做**前缀近似:`21.0` 不算 `21.0.13`,`21.0.13` 也不算 Microsoft 的 `21.0.13.0.2`
|
||||||
|
/// —— 少一位就是另一个版本,宁可报错并列出该大版本的可用版本。
|
||||||
|
#[must_use]
|
||||||
|
pub fn matches_version(requested: &str, candidate: &str) -> bool {
|
||||||
|
let wanted = canonical_version(requested);
|
||||||
|
let found = canonical_version(candidate);
|
||||||
|
if wanted.is_empty() || found.is_empty() || public_version(wanted) != public_version(found) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match (build_head(wanted), build_head(found)) {
|
||||||
|
// 只给公共部分:该公共部分的唯一产物就是它。
|
||||||
|
(None, _) => true,
|
||||||
|
(Some(wanted), Some(found)) => wanted == found,
|
||||||
|
// 用户点名了构建号、候选里没给:不能当作命中(可能不是同一个构建)。
|
||||||
|
(Some(_), None) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 兜底闸:实际拿到的版本必须真是用户点名的那一版(`None` = 没钉版本,永远通过)。
|
||||||
|
///
|
||||||
|
/// 这是「不静默回落」的最后一道保险:即使某家的实现哪天真回落到了 latest,
|
||||||
|
/// 调用方(`install` / `plan`)也会把它记成一次失败并继续走链,而不是把 latest
|
||||||
|
/// 当成用户要的版本装下去。
|
||||||
|
#[must_use]
|
||||||
|
pub fn version_mismatch(wanted: Option<&str>, found: &str) -> Option<String> {
|
||||||
|
let wanted = wanted?;
|
||||||
|
(!matches_version(wanted, found)).then(|| format!("返回的版本是 {found},不是请求的 {wanted}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可用版本列表的人读形态(错误里列给人看;超过 `limit` 条就收口成「等 N 个」)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn available_versions_text(versions: &[String], limit: usize) -> String {
|
||||||
|
if versions.is_empty() {
|
||||||
|
return "(上游一个都没列出来)".to_string();
|
||||||
|
}
|
||||||
|
let shown = versions
|
||||||
|
.iter()
|
||||||
|
.take(limit)
|
||||||
|
.map(String::as_str)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" / ");
|
||||||
|
if versions.len() > limit {
|
||||||
|
format!("{shown} 等 {} 个", versions.len())
|
||||||
|
} else {
|
||||||
|
shown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「这家装不了你要的那个版本」的错误:**四家文案互不相同**,各自说清原因 + 一句「哪家能做」。
|
||||||
|
///
|
||||||
|
/// `available` 是这家真正能给的版本串(Temurin/Oracle 给该大版本的清单,MS 给快照里那一个,
|
||||||
|
/// Zulu/Termux 给空数组——上游根本没有版本清单)。这条错误是用户唯一能看见的下一步,
|
||||||
|
/// 所以「不静默回落、也不假装支持」这句话只说一次,就在这儿说。
|
||||||
|
#[must_use]
|
||||||
|
pub fn version_pin_error(
|
||||||
|
vendor: JdkVendor,
|
||||||
|
major: u32,
|
||||||
|
wanted: &str,
|
||||||
|
available: &[String],
|
||||||
|
) -> JavaError {
|
||||||
|
let listed = available_versions_text(available, 10);
|
||||||
|
let try_other = "要这个版本换一家:`--vendor temurin`(Adoptium 全量补丁)或 `--vendor oracle`";
|
||||||
|
let vendor_id = vendor.id();
|
||||||
|
match vendor {
|
||||||
|
JdkVendor::Temurin | JdkVendor::Oracle => {
|
||||||
|
let source = if vendor == JdkVendor::Temurin {
|
||||||
|
"Adoptium"
|
||||||
|
} else {
|
||||||
|
"Oracle 镜像目录"
|
||||||
|
};
|
||||||
|
JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!("{vendor_id} 的 Java {major} 没有版本 {wanted}({source} 里可用:{listed})"),
|
||||||
|
)
|
||||||
|
.with_hint("不给 --version 就装该大版本最新的 GA;指定版本要写上面列出的形态")
|
||||||
|
}
|
||||||
|
JdkVendor::Microsoft => JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!(
|
||||||
|
"microsoft 的 Java {major} 只内置了 {listed} 这一份快照,没有 {wanted}(这家不能自选版本,要自选请换 temurin 或 oracle)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_hint(format!(
|
||||||
|
"微软这套版本号写死在快照里:装它就 `ull java install {major} --vendor microsoft`;{try_other}"
|
||||||
|
)),
|
||||||
|
JdkVendor::Zulu => JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!("zulu 只查最新一份(api 的 latest=true),不能指定版本 {wanted}(要自选请换 temurin 或 oracle)"),
|
||||||
|
)
|
||||||
|
.with_hint(format!(
|
||||||
|
"只装该大版本最新的 Zulu 就去掉 --version;{try_other}"
|
||||||
|
)),
|
||||||
|
JdkVendor::Termux => JavaError::new(
|
||||||
|
JavaErrorKind::Unsupported,
|
||||||
|
format!(
|
||||||
|
"termux 的 JDK 由系统包(pkg install openjdk-{major})决定版本,不能指定 {wanted}(普通平台上要自选请换 temurin 或 oracle)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_hint("去掉 --version 让系统包装它给的那份;要指定版本只能在普通平台上装归档"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{choices, default_major, find, vendors_for, BASELINE_MAJOR};
|
use super::{choices, default_major, find, vendors_for, BASELINE_MAJOR};
|
||||||
use crate::game::java::vendor::{ids, JdkVendor};
|
use crate::game::java::vendor::{ids, JdkVendor};
|
||||||
|
|
||||||
|
use super::{available_versions_text, matches_version, version_pin_error};
|
||||||
|
|
||||||
/// 目录必须覆盖用户点名的三档(8 / 17 / 21),并钉住基准版本。
|
/// 目录必须覆盖用户点名的三档(8 / 17 / 21),并钉住基准版本。
|
||||||
///
|
///
|
||||||
/// 变异证明:从 `CHOICES` 里删掉 8 那一项 ⇒ 本用例红(`find(8)` 为 `None`)。
|
/// 变异证明:从 `CHOICES` 里删掉 8 那一项 ⇒ 本用例红(`find(8)` 为 `None`)。
|
||||||
@@ -168,4 +313,88 @@ mod tests {
|
|||||||
assert_eq!(default_major(Some(25)), 25, "26.1 → 25");
|
assert_eq!(default_major(Some(25)), 25, "26.1 → 25");
|
||||||
assert_eq!(default_major(Some(99)), 17, "超出目录时退回基准");
|
assert_eq!(default_major(Some(99)), 17, "超出目录时退回基准");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 四家的版本串用同一把尺子量(`matches_version` 是唯一判据)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `matches_version` 换成 `starts_with` ⇒ 本用例红
|
||||||
|
/// (`21.0.13` 会命中 MS 的 `21.0.13.0.2`,那是另一个版本)。
|
||||||
|
#[test]
|
||||||
|
fn version_matching_is_shared_and_never_prefix_fuzzy() {
|
||||||
|
// Temurin:公共部分 ↔ 带构建号;semver 与 release 名两种写法都算同一份。
|
||||||
|
assert!(matches_version("21.0.13", "21.0.13+9"));
|
||||||
|
assert!(matches_version("21.0.13+9", "jdk-21.0.13+9"));
|
||||||
|
assert!(matches_version("21.0.13+9", "21.0.13+9.0.LTS"));
|
||||||
|
assert!(matches_version(" 21.0.13 ", "21.0.13"));
|
||||||
|
// Oracle:目录名就是三段,公共部分一致即命中。
|
||||||
|
assert!(matches_version("21.0.2", "21.0.2"));
|
||||||
|
// 少一位、多一段都不是同一个版本(不许前缀近似)。
|
||||||
|
assert!(!matches_version("21.0", "21.0.13"));
|
||||||
|
assert!(!matches_version("21.0.13", "21.0.13.0.2"));
|
||||||
|
assert!(!matches_version("21.0.13+8", "21.0.13+9"));
|
||||||
|
// 用户点名了构建号、候选没给 → 不算命中。
|
||||||
|
assert!(!matches_version("21.0.13+9", "21.0.13"));
|
||||||
|
assert!(!matches_version("", "21.0.13"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可用版本列表:给几条就列几条,多了收口(错误文案不能无限长)。
|
||||||
|
#[test]
|
||||||
|
fn available_versions_text_caps_the_list() {
|
||||||
|
let versions: Vec<String> = ["21.0.13", "21.0.12", "21.0.11"]
|
||||||
|
.iter()
|
||||||
|
.map(|text| (*text).to_string())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
available_versions_text(&versions, 10),
|
||||||
|
"21.0.13 / 21.0.12 / 21.0.11"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
available_versions_text(&versions, 2),
|
||||||
|
"21.0.13 / 21.0.12 等 3 个"
|
||||||
|
);
|
||||||
|
assert!(available_versions_text(&[], 5).contains("一个都没列出来"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 四家的「这家不能按你要的版本装」文案必须**互不相同**、各自说清原因,且给下一步。
|
||||||
|
///
|
||||||
|
/// 变异证明:把某两家共用同一句「不支持指定版本」⇒ 本用例红。
|
||||||
|
#[test]
|
||||||
|
fn pin_refusals_are_distinct_and_each_explains_itself() {
|
||||||
|
let snapshot = vec!["21.0.13.0.2".to_string()];
|
||||||
|
let listed = vec!["21.0.13+9".to_string()];
|
||||||
|
let errors = [
|
||||||
|
version_pin_error(JdkVendor::Temurin, 21, "21.0.5", &listed),
|
||||||
|
version_pin_error(JdkVendor::Oracle, 21, "21.0.5", &["21.0.2".to_string()]),
|
||||||
|
version_pin_error(JdkVendor::Microsoft, 21, "21.0.5", &snapshot),
|
||||||
|
version_pin_error(JdkVendor::Zulu, 21, "21.0.5", &[]),
|
||||||
|
version_pin_error(JdkVendor::Termux, 21, "21.0.5", &[]),
|
||||||
|
];
|
||||||
|
for (index, left) in errors.iter().enumerate() {
|
||||||
|
for right in errors.iter().skip(index + 1) {
|
||||||
|
assert_ne!(
|
||||||
|
left.message(),
|
||||||
|
right.message(),
|
||||||
|
"每家的拒绝理由必须各说各的"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
left.message().contains("21.0.5"),
|
||||||
|
"错误里要带上用户要的版本"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
errors[0].message().contains("21.0.13+9"),
|
||||||
|
"可用版本要列出来"
|
||||||
|
);
|
||||||
|
assert!(errors[2].message().contains("只内置了 21.0.13.0.2"));
|
||||||
|
assert!(errors[2]
|
||||||
|
.hint()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("--vendor microsoft"));
|
||||||
|
assert!(errors[3].message().contains("latest=true"));
|
||||||
|
assert!(errors[3]
|
||||||
|
.hint()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("--vendor temurin"));
|
||||||
|
assert!(errors[4].message().contains("系统包"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
//! ```text
|
//! ```text
|
||||||
//! [Termux] termux_install::attempt 系统包(.deb)——与下面这条链不共用任何一步
|
//! [Termux] termux_install::attempt 系统包(.deb)——与下面这条链不共用任何一步
|
||||||
//! vendor::order(用户优先级, 地区) D23 的尝试顺序
|
//! vendor::order(用户优先级, 地区) D23 的尝试顺序
|
||||||
//! → source::fetch(vendor, major) 查到该家最新 GA(Temurin/Oracle/MS/Zulu)
|
//! → source::fetch(vendor, major, 版本) 该家最新 GA,或 `--version` 指定的那一版
|
||||||
//! → location::JavaInstallRoot::jdk_dir 按落点模式算目录
|
//! → location::JavaInstallRoot::jdk_dir 按落点模式算目录
|
||||||
//! → [戳记 sha256 相同 && bin/java 在] 复用 幂等的第一层
|
//! → [戳记 sha256 相同 && bin/java 在] 复用 幂等的第一层
|
||||||
//! → engine.download(JavaRuntime 请求) 镜像/健康度/退避/续传/边下边算全在引擎
|
//! → engine.download(JavaRuntime 请求) 镜像/健康度/退避/续传/边下边算全在引擎
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use super::catalog;
|
||||||
use super::error::{JavaError, JavaErrorKind};
|
use super::error::{JavaError, JavaErrorKind};
|
||||||
use super::extract;
|
use super::extract;
|
||||||
use super::location::{JavaInstallRoot, LocationMode};
|
use super::location::{JavaInstallRoot, LocationMode};
|
||||||
@@ -35,6 +36,7 @@ use super::probe::{self, JavaRuntime};
|
|||||||
use super::release::JdkRelease;
|
use super::release::JdkRelease;
|
||||||
use super::source::{self, ChainLog};
|
use super::source::{self, ChainLog};
|
||||||
use super::target::AdoptiumTarget;
|
use super::target::AdoptiumTarget;
|
||||||
|
use super::termux;
|
||||||
use super::vendor::{self, JdkVendor};
|
use super::vendor::{self, JdkVendor};
|
||||||
use crate::download::{DownloadEngine, DownloadRequest, ResourceKind};
|
use crate::download::{DownloadEngine, DownloadRequest, ResourceKind};
|
||||||
|
|
||||||
@@ -55,6 +57,11 @@ pub struct InstallRequest {
|
|||||||
pub vendor: Option<JdkVendor>,
|
pub vendor: Option<JdkVendor>,
|
||||||
/// 用户优先级(`java.vendorPriority`,空 = 按地区默认序)。
|
/// 用户优先级(`java.vendorPriority`,空 = 按地区默认序)。
|
||||||
pub priority: Vec<JdkVendor>,
|
pub priority: Vec<JdkVendor>,
|
||||||
|
/// 钉死具体版本(`--version`);`None` = 该家**最新 GA**(M280 之前的唯一行为)。
|
||||||
|
///
|
||||||
|
/// 能不能钉由各家的上游决定(Temurin/Oracle 能、Microsoft 只认它的内置快照、
|
||||||
|
/// Zulu 只有 latest)——不能钉时**报错**,不会回落到最新还声称装成了这一版。
|
||||||
|
pub version: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstallRequest {
|
impl InstallRequest {
|
||||||
@@ -65,6 +72,7 @@ impl InstallRequest {
|
|||||||
major,
|
major,
|
||||||
vendor: None,
|
vendor: None,
|
||||||
priority: Vec::new(),
|
priority: Vec::new(),
|
||||||
|
version: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +83,7 @@ impl InstallRequest {
|
|||||||
major,
|
major,
|
||||||
vendor: Some(vendor),
|
vendor: Some(vendor),
|
||||||
priority: Vec::new(),
|
priority: Vec::new(),
|
||||||
|
version: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +94,13 @@ impl InstallRequest {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 钉死具体版本(`--version`;`None` 与不调用本方法完全等价)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_version(mut self, version: Option<String>) -> Self {
|
||||||
|
self.version = version;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// 实际尝试顺序(`--vendor` 钉死时链里只放它)。
|
/// 实际尝试顺序(`--vendor` 钉死时链里只放它)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn order(&self, domestic: bool) -> Vec<JdkVendor> {
|
pub fn order(&self, domestic: bool) -> Vec<JdkVendor> {
|
||||||
@@ -200,23 +216,47 @@ pub async fn install(
|
|||||||
) -> Result<InstalledJdk, JavaError> {
|
) -> Result<InstalledJdk, JavaError> {
|
||||||
let domestic = engine.region().is_domestic();
|
let domestic = engine.region().is_domestic();
|
||||||
let priority = request.order(domestic);
|
let priority = request.order(domestic);
|
||||||
|
let wanted = request.version.as_deref();
|
||||||
let mut reasons: Vec<String> = Vec::new();
|
let mut reasons: Vec<String> = Vec::new();
|
||||||
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
|
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
|
||||||
let _ = super::location::cleanup_orphans(&location.root);
|
let _ = super::location::cleanup_orphans(&location.root);
|
||||||
|
|
||||||
// Termux 那一条路(系统包)单独成模块:它与归档链不共用任何一步,见 `termux_install`。
|
// Termux 那一条路(系统包)单独成模块:它与归档链不共用任何一步,见 `termux_install`。
|
||||||
// 它把 chain 还回来(没走成时),归档链接着往上记。
|
// 它把 chain 还回来(没走成时),归档链接着往上记。
|
||||||
let mut chain = match termux_install::attempt(
|
// 系统包的版本由包管理器决定,所以钉了版本(`--version`)就不走它:`--vendor termux`
|
||||||
request.vendor,
|
// 直接报「不能指定版本」,其余情况记一条跳过原因后交给归档链。
|
||||||
request.major,
|
let mut chain = match wanted {
|
||||||
location,
|
Some(wanted) if request.vendor == Some(JdkVendor::Termux) => {
|
||||||
ChainLog::new(),
|
return Err(catalog::version_pin_error(
|
||||||
&mut reasons,
|
JdkVendor::Termux,
|
||||||
)
|
request.major,
|
||||||
.await?
|
wanted,
|
||||||
{
|
&[],
|
||||||
termux_install::Outcome::Done(installed) => return Ok(*installed),
|
));
|
||||||
termux_install::Outcome::Skipped(chain) => chain,
|
}
|
||||||
|
Some(_) => {
|
||||||
|
let mut chain = ChainLog::new();
|
||||||
|
// 只有真在 Termux 上才需要解释「为什么不走系统包」(别的平台这条路本来就不存在)。
|
||||||
|
if termux::available() {
|
||||||
|
chain.skip(
|
||||||
|
JdkVendor::Termux,
|
||||||
|
"系统包不能指定版本(--version),改走归档链",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
chain
|
||||||
|
}
|
||||||
|
None => match termux_install::attempt(
|
||||||
|
request.vendor,
|
||||||
|
request.major,
|
||||||
|
location,
|
||||||
|
ChainLog::new(),
|
||||||
|
&mut reasons,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
termux_install::Outcome::Done(installed) => return Ok(*installed),
|
||||||
|
termux_install::Outcome::Skipped(chain) => chain,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 归档发行版需要 Adoptium 的平台参数;Termux/Android 上这里会 Unsupported,
|
// 归档发行版需要 Adoptium 的平台参数;Termux/Android 上这里会 Unsupported,
|
||||||
@@ -245,7 +285,7 @@ pub async fn install(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let release = match source::fetch(engine, vendor, request.major, target).await {
|
let release = match source::fetch(engine, vendor, request.major, target, wanted).await {
|
||||||
Ok(release) => release,
|
Ok(release) => release,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
chain.failed(vendor, error.message().to_string());
|
chain.failed(vendor, error.message().to_string());
|
||||||
@@ -253,6 +293,12 @@ pub async fn install(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 兜底闸:装成的那一版必须真是用户点名的那一版(任何一家都不许回落 latest)。
|
||||||
|
if let Some(reason) = catalog::version_mismatch(wanted, &release.version) {
|
||||||
|
chain.failed(vendor, reason.clone());
|
||||||
|
reasons.push(format!("{}:{reason}", vendor.id()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match install_one(engine, location, &release).await {
|
match install_one(engine, location, &release).await {
|
||||||
Ok(prepared) => {
|
Ok(prepared) => {
|
||||||
chain.hit(vendor, format!("命中 {}", release.summary()));
|
chain.hit(vendor, format!("命中 {}", release.summary()));
|
||||||
@@ -278,7 +324,7 @@ pub async fn install(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(JavaError::new(
|
let error = JavaError::new(
|
||||||
JavaErrorKind::Metadata,
|
JavaErrorKind::Metadata,
|
||||||
format!(
|
format!(
|
||||||
"没有装成 Java {}(试过 {}):{}",
|
"没有装成 Java {}(试过 {}):{}",
|
||||||
@@ -286,40 +332,59 @@ pub async fn install(
|
|||||||
priority.len(),
|
priority.len(),
|
||||||
reasons.join(";")
|
reasons.join(";")
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.with_hint("`ull java vendors` 看各发行版的可用性;或手动装 JDK 后设 JAVA_HOME"))
|
// 钉了版本时给一句能照着做的:哪几家真能按版本装(默认路径的提示一个字都不动)。
|
||||||
|
Err(if wanted.is_some() {
|
||||||
|
error.with_hint(
|
||||||
|
"能按版本装的是 temurin / oracle;microsoft 只认它内置的快照,zulu 上游只提供 latest。\
|
||||||
|
`ull java vendors` 看各发行版的可用性",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
error.with_hint("`ull java vendors` 看各发行版的可用性;或手动装 JDK 后设 JAVA_HOME")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 只做规划不落盘:返回将要安装的版本、目标目录与「是否已经装好」。
|
/// 只做规划不落盘:返回将要安装的版本、目标目录与「是否已经装好」。
|
||||||
///
|
///
|
||||||
|
/// `--dry-run` 用它:`request.version` 与 `--vendor` 都参与选择(钉了版本就把这一维钉死),
|
||||||
|
/// 所以「最后会打哪个 URL、装哪个版本串」在下载之前就能看见——这也是不联网时
|
||||||
|
/// 唯一能验证版本选择逻辑的输出。
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// 与 [`install`] 的查询段相同(平台不支持 / 网络失败 / 所有发行版都没有该大版本)。
|
/// 与 [`install`] 的查询段相同(平台不支持 / 网络失败 / 该家没有这个版本)。
|
||||||
pub async fn plan(
|
pub async fn plan(
|
||||||
engine: &DownloadEngine,
|
engine: &DownloadEngine,
|
||||||
location: &JavaInstallRoot,
|
location: &JavaInstallRoot,
|
||||||
major: u32,
|
request: &InstallRequest,
|
||||||
) -> Result<(JdkRelease, PathBuf, bool), JavaError> {
|
) -> Result<(JdkRelease, PathBuf, bool), JavaError> {
|
||||||
let target = AdoptiumTarget::host()?;
|
let target = AdoptiumTarget::host()?;
|
||||||
let domestic = engine.region().is_domestic();
|
let domestic = engine.region().is_domestic();
|
||||||
|
let wanted = request.version.as_deref();
|
||||||
let mut reasons: Vec<String> = Vec::new();
|
let mut reasons: Vec<String> = Vec::new();
|
||||||
for vendor in vendor::order(&[], domestic) {
|
for vendor in chain_order(request, domestic) {
|
||||||
if !vendor.supports(major) {
|
let release = match source::fetch(engine, vendor, request.major, target, wanted).await {
|
||||||
reasons.push(format!("{}:不支持 Java {major}", vendor.id()));
|
Ok(release) => release,
|
||||||
|
Err(error) => {
|
||||||
|
reasons.push(format!("{}:{}", vendor.id(), error.message()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(reason) = catalog::version_mismatch(wanted, &release.version) {
|
||||||
|
reasons.push(format!("{}:{reason}", vendor.id()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match source::fetch(engine, vendor, major, target).await {
|
let root = location.jdk_dir(release.major, release.vendor, &release.version);
|
||||||
Ok(release) => {
|
let ready = extract::is_ready(&root, release.sha256());
|
||||||
let root = location.jdk_dir(release.major, release.vendor, &release.version);
|
return Ok((release, root, ready));
|
||||||
let ready = extract::is_ready(&root, release.sha256());
|
|
||||||
return Ok((release, root, ready));
|
|
||||||
}
|
|
||||||
Err(error) => reasons.push(format!("{}:{}", vendor.id(), error.message())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(JavaError::new(
|
Err(JavaError::new(
|
||||||
JavaErrorKind::Metadata,
|
JavaErrorKind::Metadata,
|
||||||
format!("没有可用的 Java {major}({} )", reasons.join(";")),
|
format!(
|
||||||
|
"没有可用的 Java {}({} )",
|
||||||
|
request.major,
|
||||||
|
reasons.join(";")
|
||||||
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
//! 3. **GitHub 侧没有任何资产**(`microsoft/openjdk` 的 release `assets` 全空),
|
//! 3. **GitHub 侧没有任何资产**(`microsoft/openjdk` 的 release `assets` 全空),
|
||||||
//! 所以 gh-proxy 对这条链毫无用处——`domestic_mirrors` 实测为空数组。
|
//! 所以 gh-proxy 对这条链毫无用处——`domestic_mirrors` 实测为空数组。
|
||||||
|
|
||||||
|
use super::catalog::{matches_version, version_pin_error};
|
||||||
use super::error::{JavaError, JavaErrorKind};
|
use super::error::{JavaError, JavaErrorKind};
|
||||||
use super::net;
|
use super::net;
|
||||||
use super::release::{parse_sha256_text, JdkArchive, JdkRelease};
|
use super::release::{parse_sha256_text, JdkArchive, JdkRelease};
|
||||||
@@ -137,16 +138,7 @@ pub async fn fetch_latest(
|
|||||||
major: u32,
|
major: u32,
|
||||||
target: AdoptiumTarget,
|
target: AdoptiumTarget,
|
||||||
) -> Result<JdkRelease, JavaError> {
|
) -> Result<JdkRelease, JavaError> {
|
||||||
let version = snapshot_version(major).ok_or_else(|| {
|
let version = snapshot_version(major).ok_or_else(|| missing_major(major))?;
|
||||||
JavaError::new(
|
|
||||||
JavaErrorKind::Metadata,
|
|
||||||
format!(
|
|
||||||
"Microsoft OpenJDK 没有 {major} 的 GA(只发 {})",
|
|
||||||
majors_text()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with_hint("8 用 temurin;其它大版本用 oracle / zulu")
|
|
||||||
})?;
|
|
||||||
let filename = archive_name(version, target);
|
let filename = archive_name(version, target);
|
||||||
let sha_url = expand_template(&table.sha256_template, version, target);
|
let sha_url = expand_template(&table.sha256_template, version, target);
|
||||||
let sha_text = net::get_text(client, &sha_url, "Microsoft OpenJDK 校验文件").await?;
|
let sha_text = net::get_text(client, &sha_url, "Microsoft OpenJDK 校验文件").await?;
|
||||||
@@ -171,6 +163,47 @@ pub async fn fetch_latest(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 查一次「某大版本的指定版本 MS OpenJDK」。
|
||||||
|
///
|
||||||
|
/// 微软没有「按版本查」的接口(官方只有人读 HTML 表),[`SNAPSHOT`] 里每个大版本只有**一个**
|
||||||
|
/// 版本号(`size` 也是按它实测的),所以只认快照里那一个:别的版本号**明确报错**
|
||||||
|
/// (文案在 `catalog::version_pin_error`:说清这家不能自选版本、可用的是哪一个),
|
||||||
|
/// 绝不回落到 latest 假装装成了。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 该大版本微软不发(含 8)、以及请求的版本号不是快照里那一个时返回错误。
|
||||||
|
pub async fn fetch_version(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
table: &MsOpenJdkTable,
|
||||||
|
major: u32,
|
||||||
|
target: AdoptiumTarget,
|
||||||
|
wanted: &str,
|
||||||
|
) -> Result<JdkRelease, JavaError> {
|
||||||
|
let known = snapshot_version(major).ok_or_else(|| missing_major(major))?;
|
||||||
|
if !matches_version(wanted, known) {
|
||||||
|
return Err(version_pin_error(
|
||||||
|
JdkVendor::Microsoft,
|
||||||
|
major,
|
||||||
|
wanted,
|
||||||
|
&[known.to_string()],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
fetch_latest(client, table, major, target).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「微软不发这个 Java 大版本」(含 8)。
|
||||||
|
fn missing_major(major: u32) -> JavaError {
|
||||||
|
JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!(
|
||||||
|
"Microsoft OpenJDK 没有 {major} 的 GA(只发 {})",
|
||||||
|
majors_text()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_hint("8 用 temurin;其它大版本用 oracle / zulu")
|
||||||
|
}
|
||||||
|
|
||||||
fn majors_text() -> String {
|
fn majors_text() -> String {
|
||||||
SNAPSHOT
|
SNAPSHOT
|
||||||
.iter()
|
.iter()
|
||||||
@@ -182,13 +215,41 @@ fn majors_text() -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
archive_name, expand_template, ms_os, parse_version, snapshot_size, snapshot_version,
|
archive_name, expand_template, fetch_version, ms_os, parse_version, snapshot_size,
|
||||||
SNAPSHOT,
|
snapshot_version, SNAPSHOT,
|
||||||
};
|
};
|
||||||
|
use crate::download::MirrorTable;
|
||||||
|
use crate::game::java::error::JavaErrorKind;
|
||||||
use crate::game::java::target::AdoptiumTarget;
|
use crate::game::java::target::AdoptiumTarget;
|
||||||
|
|
||||||
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
|
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
|
||||||
|
|
||||||
|
/// 指定版本:除了快照里那一个版本号,别的一律**报错**(不回落 latest),
|
||||||
|
/// 而且错误里要说清「只有这份快照」与「可用的是哪个」。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `fetch_version` 里的版本校验删掉(直接 `fetch_latest`)⇒ 本用例红。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pinned_version_outside_the_snapshot_is_refused_not_fallen_back() {
|
||||||
|
let table = MirrorTable::builtin();
|
||||||
|
let microsoft = table.microsoft_openjdk().expect("内置表应带 MS 分发");
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
// 错误在取校验文件之前就返回,所以这条用例一个请求都不发。
|
||||||
|
let error = fetch_version(&client, microsoft, 21, WINDOWS, "21.0.5")
|
||||||
|
.await
|
||||||
|
.expect_err("快照里没有 21.0.5");
|
||||||
|
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||||
|
assert!(
|
||||||
|
error.message().contains("21.0.13.0.2"),
|
||||||
|
"错误里要说清这家有哪个版本:{}",
|
||||||
|
error.message()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
error.message().contains("不能自选版本"),
|
||||||
|
"要把「这家不能自选版本」说明白:{}",
|
||||||
|
error.message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn snapshot_covers_the_four_majors_microsoft_ships() {
|
fn snapshot_covers_the_four_majors_microsoft_ships() {
|
||||||
assert_eq!(snapshot_version(21), Some("21.0.13.0.2"));
|
assert_eq!(snapshot_version(21), Some("21.0.13.0.2"));
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ use super::target::AdoptiumTarget;
|
|||||||
use super::vendor::JdkVendor;
|
use super::vendor::JdkVendor;
|
||||||
use crate::download::MirrorTable;
|
use crate::download::MirrorTable;
|
||||||
|
|
||||||
|
mod listing;
|
||||||
|
|
||||||
|
pub use listing::{find_archive, parse_links, pick_version, version_key, versions_for};
|
||||||
|
|
||||||
|
use listing::select_version;
|
||||||
|
|
||||||
/// Oracle 的 os 段(Adoptium 的 `mac` 在 Oracle 里叫 `macos`)。
|
/// Oracle 的 os 段(Adoptium 的 `mac` 在 Oracle 里叫 `macos`)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn oracle_os(target: AdoptiumTarget) -> &'static str {
|
pub fn oracle_os(target: AdoptiumTarget) -> &'static str {
|
||||||
@@ -50,71 +56,10 @@ pub fn oracle_arch(target: AdoptiumTarget) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 目录页 → 本级条目名列表(去掉尾部 `/`、跳过 `../` 与绝对链接,去重保序)。
|
|
||||||
#[must_use]
|
|
||||||
pub fn parse_links(html: &str) -> Vec<String> {
|
|
||||||
let mut out: Vec<String> = Vec::new();
|
|
||||||
let mut rest = html;
|
|
||||||
while let Some(start) = rest.find("href=\"") {
|
|
||||||
let after = &rest[start + "href=\"".len()..];
|
|
||||||
let Some(end) = after.find('"') else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let value = after[..end].trim();
|
|
||||||
rest = &after[end..];
|
|
||||||
if value.is_empty()
|
|
||||||
|| value.starts_with("http")
|
|
||||||
|| value.starts_with("//")
|
|
||||||
|| value.starts_with('?')
|
|
||||||
|| value.contains('#')
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let name = value.trim_end_matches('/');
|
|
||||||
let Some(name) = name.rsplit('/').next() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if name.is_empty() || name == "." || name == ".." {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !out.iter().any(|known| known == name) {
|
|
||||||
out.push(name.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 版本串 → 可比较的数字段(`21.0.2` → `[21, 0, 2]`,`27.0.1.0.3` → `[27,0,1,0,3]`)。
|
|
||||||
#[must_use]
|
|
||||||
pub fn version_key(version: &str) -> Option<Vec<u64>> {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
for segment in version.split('.') {
|
|
||||||
parts.push(segment.parse::<u64>().ok()?);
|
|
||||||
}
|
|
||||||
(!parts.is_empty()).then_some(parts)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 目录页里属于 `major` 的版本串,按版本**降序**(最新在前)。
|
|
||||||
#[must_use]
|
|
||||||
pub fn versions_for(html: &str, major: u32) -> Vec<String> {
|
|
||||||
let mut found: Vec<(Vec<u64>, String)> = parse_links(html)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|name| {
|
|
||||||
let key = version_key(&name)?;
|
|
||||||
(key.first() == Some(&u64::from(major))).then_some((key, name))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
found.sort_by(|left, right| right.0.cmp(&left.0));
|
|
||||||
found.into_iter().map(|(_, name)| name).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 取 `major` 线的最新 GA 版本串。
|
|
||||||
#[must_use]
|
|
||||||
pub fn pick_version(html: &str, major: u32) -> Option<String> {
|
|
||||||
versions_for(html, major).into_iter().next()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 期望文件名(Oracle 的命名是**模板**,哈希只在目录层级上,不在文件名里)。
|
/// 期望文件名(Oracle 的命名是**模板**,哈希只在目录层级上,不在文件名里)。
|
||||||
|
///
|
||||||
|
/// 目录页解析与版本枚举(`parse_links` / `versions_for` / `select_version`)在
|
||||||
|
/// [`listing`] 里,这里只留「拿到版本串之后」的那一段。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn archive_name(version: &str, target: AdoptiumTarget) -> String {
|
pub fn archive_name(version: &str, target: AdoptiumTarget) -> String {
|
||||||
format!(
|
format!(
|
||||||
@@ -137,12 +82,6 @@ pub fn checksum_url(base: &str, version: &str, filename: &str) -> String {
|
|||||||
format!("{}.sha256", archive_url(base, version, filename))
|
format!("{}.sha256", archive_url(base, version, filename))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 目录页里是否有这个文件(**唯一**的「存在」判据:不信状态码)。
|
|
||||||
#[must_use]
|
|
||||||
pub fn find_archive(html: &str, filename: &str) -> bool {
|
|
||||||
parse_links(html).iter().any(|name| name == filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 查一次「某大版本的最新 Oracle OpenJDK GA」。
|
/// 查一次「某大版本的最新 Oracle OpenJDK GA」。
|
||||||
///
|
///
|
||||||
/// 按 `mirrors.json` 里 `oracle_openjdk` 的启用顺序逐个镜像试(国内:华为云 → 南大),
|
/// 按 `mirrors.json` 里 `oracle_openjdk` 的启用顺序逐个镜像试(国内:华为云 → 南大),
|
||||||
@@ -157,6 +96,36 @@ pub async fn fetch_latest(
|
|||||||
table: &MirrorTable,
|
table: &MirrorTable,
|
||||||
major: u32,
|
major: u32,
|
||||||
target: AdoptiumTarget,
|
target: AdoptiumTarget,
|
||||||
|
) -> Result<JdkRelease, JavaError> {
|
||||||
|
try_mirrors(client, table, major, target, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查一次「某大版本的指定版本 Oracle OpenJDK」。
|
||||||
|
///
|
||||||
|
/// 版本串来自镜像目录页([`versions_for`] 本来就能枚举某 major 的全部版本),
|
||||||
|
/// 选定就用它,**不再取 [`pick_version`] 的最新**;目录页里没有这个版本时
|
||||||
|
/// 由 `catalog::version_pin_error` 报错并列出可用版本。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 所有镜像都没有这个版本(或版本目录页 / `.sha256` 取不到)时返回错误。
|
||||||
|
pub async fn fetch_version(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
table: &MirrorTable,
|
||||||
|
major: u32,
|
||||||
|
target: AdoptiumTarget,
|
||||||
|
wanted: &str,
|
||||||
|
) -> Result<JdkRelease, JavaError> {
|
||||||
|
try_mirrors(client, table, major, target, Some(wanted)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 逐个镜像试(`wanted` 决定是取最新还是取指定版本)。
|
||||||
|
async fn try_mirrors(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
table: &MirrorTable,
|
||||||
|
major: u32,
|
||||||
|
target: AdoptiumTarget,
|
||||||
|
wanted: Option<&str>,
|
||||||
) -> Result<JdkRelease, JavaError> {
|
) -> Result<JdkRelease, JavaError> {
|
||||||
let mirrors: Vec<_> = table.oracle_mirrors().collect();
|
let mirrors: Vec<_> = table.oracle_mirrors().collect();
|
||||||
if mirrors.is_empty() {
|
if mirrors.is_empty() {
|
||||||
@@ -168,17 +137,18 @@ pub async fn fetch_latest(
|
|||||||
}
|
}
|
||||||
let mut reasons: Vec<String> = Vec::new();
|
let mut reasons: Vec<String> = Vec::new();
|
||||||
for mirror in mirrors {
|
for mirror in mirrors {
|
||||||
match fetch_from(client, mirror.base(), major, target).await {
|
match fetch_from(client, mirror.base(), major, target, wanted).await {
|
||||||
Ok(release) => return Ok(release),
|
Ok(release) => return Ok(release),
|
||||||
Err(error) => reasons.push(format!("{}:{}", mirror.name, error.message())),
|
Err(error) => reasons.push(format!("{}:{}", mirror.name, error.message())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let subject = match wanted {
|
||||||
|
None => format!("Oracle OpenJDK {major}"),
|
||||||
|
Some(wanted) => format!("oracle 的 Java {major} 版本 {wanted}"),
|
||||||
|
};
|
||||||
Err(JavaError::new(
|
Err(JavaError::new(
|
||||||
JavaErrorKind::Network,
|
JavaErrorKind::Network,
|
||||||
format!(
|
format!("{subject} 在所有镜像上都取不到:{}", reasons.join(";")),
|
||||||
"Oracle OpenJDK {major} 在所有镜像上都取不到:{}",
|
|
||||||
reasons.join(";")
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.with_hint("Oracle 的 21 线只到 21.0.2(只发首个 GA + ≤2 update);换 17/25 或改用 temurin"))
|
.with_hint("Oracle 的 21 线只到 21.0.2(只发首个 GA + ≤2 update);换 17/25 或改用 temurin"))
|
||||||
}
|
}
|
||||||
@@ -189,6 +159,7 @@ async fn fetch_from(
|
|||||||
base: &str,
|
base: &str,
|
||||||
major: u32,
|
major: u32,
|
||||||
target: AdoptiumTarget,
|
target: AdoptiumTarget,
|
||||||
|
wanted: Option<&str>,
|
||||||
) -> Result<JdkRelease, JavaError> {
|
) -> Result<JdkRelease, JavaError> {
|
||||||
let index = net::get_text(
|
let index = net::get_text(
|
||||||
client,
|
client,
|
||||||
@@ -196,15 +167,7 @@ async fn fetch_from(
|
|||||||
"Oracle 镜像根目录",
|
"Oracle 镜像根目录",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let version = pick_version(&index, major).ok_or_else(|| {
|
let version = select_version(&index, major, wanted)?;
|
||||||
JavaError::new(
|
|
||||||
JavaErrorKind::Metadata,
|
|
||||||
format!(
|
|
||||||
"镜像目录里没有 {major} 线的 Oracle OpenJDK(目录页 {} 项)",
|
|
||||||
parse_links(&index).len()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let filename = archive_name(&version, target);
|
let filename = archive_name(&version, target);
|
||||||
let listing = net::get_text(
|
let listing = net::get_text(
|
||||||
client,
|
client,
|
||||||
@@ -248,23 +211,10 @@ async fn fetch_from(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::listing::find_archive;
|
||||||
archive_name, archive_url, checksum_url, find_archive, oracle_os, parse_links,
|
use super::{archive_name, archive_url, checksum_url, oracle_os};
|
||||||
pick_version, version_key, versions_for,
|
|
||||||
};
|
|
||||||
use crate::game::java::target::AdoptiumTarget;
|
use crate::game::java::target::AdoptiumTarget;
|
||||||
|
|
||||||
/// 华为云 `/openjdk/` 的真实目录页片段(2026-10-30 抓取,`<pre>` 列表)。
|
|
||||||
const INDEX: &str = r#"<html><body><pre>Name Last modified Size</pre><hr><pre>
|
|
||||||
<a href="10/" onclick="handleLinkUrl(this)">10/</a> 22-Aug-2021 15:18 -
|
|
||||||
<a href="17.0.2/" onclick="handleLinkUrl(this)">17.0.2/</a> 22-Aug-2021 15:19 -
|
|
||||||
<a href="21/" onclick="handleLinkUrl(this)">21/</a> 22-Aug-2021 15:19 -
|
|
||||||
<a href="21.0.1/" onclick="handleLinkUrl(this)">21.0.1/</a> 22-Aug-2021 15:19 -
|
|
||||||
<a href="21.0.2/" onclick="handleLinkUrl(this)">21.0.2/</a> 06-Jan-2024 15:26 -
|
|
||||||
<a href="27.0.1.0.3/" onclick="handleLinkUrl(this)">27.0.1.0.3/</a> 06-Jan-2026 15:26 -
|
|
||||||
<a href="../" onclick="handleLinkUrl(this)">../</a>
|
|
||||||
</pre></body></html>"#;
|
|
||||||
|
|
||||||
/// 华为云 `/openjdk/21.0.2/` 的真实文件列表片段。
|
/// 华为云 `/openjdk/21.0.2/` 的真实文件列表片段。
|
||||||
const LISTING: &str = r#"<html><body><pre><a href="../">../</a>
|
const LISTING: &str = r#"<html><body><pre><a href="../">../</a>
|
||||||
<a href="openjdk-21.0.2_linux-x64_bin.tar.gz" onclick="handleLinkUrl(this)">openjdk-21.0.2_linux-x64_bin.tar.gz</a> 194.18 MB
|
<a href="openjdk-21.0.2_linux-x64_bin.tar.gz" onclick="handleLinkUrl(this)">openjdk-21.0.2_linux-x64_bin.tar.gz</a> 194.18 MB
|
||||||
@@ -273,30 +223,6 @@ mod tests {
|
|||||||
|
|
||||||
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
|
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn directory_links_are_scraped_without_any_hash_segment() {
|
|
||||||
let links = parse_links(INDEX);
|
|
||||||
assert!(links.contains(&"21.0.2".to_string()));
|
|
||||||
assert!(!links.iter().any(|name| name == ".."));
|
|
||||||
// Oracle 直链里的构建哈希(f2283984656d49d69e91c558476027ac)在镜像目录里根本不存在:
|
|
||||||
// 目录层级只有 `{ver}/`,所以实现里没有任何拼哈希的位置。
|
|
||||||
assert!(!links.iter().any(|name| name.len() == 32));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn newest_version_of_a_line_wins_and_ordering_is_numeric() {
|
|
||||||
assert_eq!(
|
|
||||||
versions_for(INDEX, 21),
|
|
||||||
vec!["21.0.2".to_string(), "21.0.1".to_string(), "21".to_string()]
|
|
||||||
);
|
|
||||||
assert_eq!(pick_version(INDEX, 21).as_deref(), Some("21.0.2"));
|
|
||||||
// 27.0.1.0.3 是四段+,比 27.0.1 新(字符串比较会得出相反结论)。
|
|
||||||
assert_eq!(version_key("27.0.1.0.3"), Some(vec![27, 0, 1, 0, 3]));
|
|
||||||
assert_eq!(version_key("27.0.1"), Some(vec![27, 0, 1]));
|
|
||||||
assert!(version_key("27.0.1") < version_key("27.0.1.0.3"));
|
|
||||||
assert_eq!(pick_version(INDEX, 8), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filename_template_matches_the_archive_on_every_platform() {
|
fn filename_template_matches_the_archive_on_every_platform() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
//! Oracle OpenJDK 的**纯**那一半:目录页解析、版本枚举、版本选择。
|
||||||
|
//!
|
||||||
|
//! 从 `oracle.rs` 拆出来只为守 400 行硬线(M280 的指定版本选择让那边到 418 行);
|
||||||
|
//! 语义未变——这里一个请求都不发,全是「HTML 文本 → 名字列表 / 版本串」的纯函数:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! parse_links 一级目录页 → 条目名(去重保序,跳过 ../ 与绝对链接)
|
||||||
|
//! version_key 版本串 → 可比较的数字段(27.0.1.0.3 > 27.0.1)
|
||||||
|
//! versions_for 目录页里属于某 major 的版本串(降序)
|
||||||
|
//! pick_version 该线最新(= versions_for 的第一个)
|
||||||
|
//! select_version 在目录页里挑版本:`None` = 最新,`Some` = 必须是这个版本(不近似)
|
||||||
|
//! find_archive 目录页里是否真有这个文件名(**唯一**的「存在」判据,不信状态码)
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! 「存在不信状态码」的理由见 `oracle.rs` 头:华为云对任意不存在的路径都回 200 + 门户 HTML。
|
||||||
|
|
||||||
|
use super::super::catalog::{matches_version, version_pin_error};
|
||||||
|
use super::super::error::{JavaError, JavaErrorKind};
|
||||||
|
use super::super::vendor::JdkVendor;
|
||||||
|
|
||||||
|
/// 目录页 → 本级条目名列表(去掉尾部 `/`、跳过 `../` 与绝对链接,去重保序)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn parse_links(html: &str) -> Vec<String> {
|
||||||
|
let mut out: Vec<String> = Vec::new();
|
||||||
|
let mut rest = html;
|
||||||
|
while let Some(start) = rest.find("href=\"") {
|
||||||
|
let after = &rest[start + "href=\"".len()..];
|
||||||
|
let Some(end) = after.find('"') else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let value = after[..end].trim();
|
||||||
|
rest = &after[end..];
|
||||||
|
if value.is_empty()
|
||||||
|
|| value.starts_with("http")
|
||||||
|
|| value.starts_with("//")
|
||||||
|
|| value.starts_with('?')
|
||||||
|
|| value.contains('#')
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = value.trim_end_matches('/');
|
||||||
|
let Some(name) = name.rsplit('/').next() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if name.is_empty() || name == "." || name == ".." {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !out.iter().any(|known| known == name) {
|
||||||
|
out.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 版本串 → 可比较的数字段(`21.0.2` → `[21, 0, 2]`,`27.0.1.0.3` → `[27,0,1,0,3]`)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn version_key(version: &str) -> Option<Vec<u64>> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
for segment in version.split('.') {
|
||||||
|
parts.push(segment.parse::<u64>().ok()?);
|
||||||
|
}
|
||||||
|
(!parts.is_empty()).then_some(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 目录页里属于 `major` 的版本串,按版本**降序**(最新在前)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn versions_for(html: &str, major: u32) -> Vec<String> {
|
||||||
|
let mut found: Vec<(Vec<u64>, String)> = parse_links(html)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|name| {
|
||||||
|
let key = version_key(&name)?;
|
||||||
|
(key.first() == Some(&u64::from(major))).then_some((key, name))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
found.sort_by(|left, right| right.0.cmp(&left.0));
|
||||||
|
found.into_iter().map(|(_, name)| name).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 取 `major` 线的最新 GA 版本串。
|
||||||
|
#[must_use]
|
||||||
|
pub fn pick_version(html: &str, major: u32) -> Option<String> {
|
||||||
|
versions_for(html, major).into_iter().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 目录页里是否有这个文件(**唯一**的「存在」判据:不信状态码)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn find_archive(html: &str, filename: &str) -> bool {
|
||||||
|
parse_links(html).iter().any(|name| name == filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在目录页里挑版本:`None` = 该线最新,`Some` = 必须是这个版本(不做前缀近似)。
|
||||||
|
pub(super) fn select_version(
|
||||||
|
html: &str,
|
||||||
|
major: u32,
|
||||||
|
wanted: Option<&str>,
|
||||||
|
) -> Result<String, JavaError> {
|
||||||
|
let Some(wanted) = wanted else {
|
||||||
|
return pick_version(html, major).ok_or_else(|| missing_major(html, major));
|
||||||
|
};
|
||||||
|
let available = versions_for(html, major);
|
||||||
|
available
|
||||||
|
.iter()
|
||||||
|
.find(|version| matches_version(wanted, version))
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| version_pin_error(JdkVendor::Oracle, major, wanted, &available))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「目录页里没有这个大版本」。
|
||||||
|
pub(super) fn missing_major(html: &str, major: u32) -> JavaError {
|
||||||
|
JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!(
|
||||||
|
"镜像目录里没有 {major} 线的 Oracle OpenJDK(目录页 {} 项)",
|
||||||
|
parse_links(html).len()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{parse_links, pick_version, select_version, version_key, versions_for};
|
||||||
|
|
||||||
|
/// 华为云 `/openjdk/` 的真实目录页片段(2026-10-30 抓取,`<pre>` 列表)。
|
||||||
|
const INDEX: &str = r#"<html><body><pre>Name Last modified Size</pre><hr><pre>
|
||||||
|
<a href="10/" onclick="handleLinkUrl(this)">10/</a> 22-Aug-2021 15:18 -
|
||||||
|
<a href="17.0.2/" onclick="handleLinkUrl(this)">17.0.2/</a> 22-Aug-2021 15:19 -
|
||||||
|
<a href="21/" onclick="handleLinkUrl(this)">21/</a> 22-Aug-2021 15:19 -
|
||||||
|
<a href="21.0.1/" onclick="handleLinkUrl(this)">21.0.1/</a> 22-Aug-2021 15:19 -
|
||||||
|
<a href="21.0.2/" onclick="handleLinkUrl(this)">21.0.2/</a> 06-Jan-2024 15:26 -
|
||||||
|
<a href="27.0.1.0.3/" onclick="handleLinkUrl(this)">27.0.1.0.3/</a> 06-Jan-2026 15:26 -
|
||||||
|
<a href="../" onclick="handleLinkUrl(this)">../</a>
|
||||||
|
</pre></body></html>"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn directory_links_are_scraped_without_any_hash_segment() {
|
||||||
|
let links = parse_links(INDEX);
|
||||||
|
assert!(links.contains(&"21.0.2".to_string()));
|
||||||
|
assert!(!links.iter().any(|name| name == ".."));
|
||||||
|
// Oracle 直链里的构建哈希(f2283984656d49d69e91c558476027ac)在镜像目录里根本不存在:
|
||||||
|
// 目录层级只有 `{ver}/`,所以实现里没有任何拼哈希的位置。
|
||||||
|
assert!(!links.iter().any(|name| name.len() == 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newest_version_of_a_line_wins_and_ordering_is_numeric() {
|
||||||
|
assert_eq!(
|
||||||
|
versions_for(INDEX, 21),
|
||||||
|
vec!["21.0.2".to_string(), "21.0.1".to_string(), "21".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(pick_version(INDEX, 21).as_deref(), Some("21.0.2"));
|
||||||
|
// 27.0.1.0.3 是四段+,比 27.0.1 新(字符串比较会得出相反结论)。
|
||||||
|
assert_eq!(version_key("27.0.1.0.3"), Some(vec![27, 0, 1, 0, 3]));
|
||||||
|
assert_eq!(version_key("27.0.1"), Some(vec![27, 0, 1]));
|
||||||
|
assert!(version_key("27.0.1") < version_key("27.0.1.0.3"));
|
||||||
|
assert_eq!(pick_version(INDEX, 8), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 指定版本走同一条枚举路径:命中就用那一版,**不是** `pick_version` 的最新;
|
||||||
|
/// 版本不存在时错误里列出该 major 真正有的版本(不许只回一句 404)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `select_version` 的 `Some` 分支改回 `pick_version` ⇒ 第一条断言红。
|
||||||
|
#[test]
|
||||||
|
fn pinned_version_is_taken_from_the_listing_not_the_newest() {
|
||||||
|
assert_eq!(
|
||||||
|
select_version(INDEX, 21, Some("21.0.1")).expect("目录页里有 21.0.1"),
|
||||||
|
"21.0.1"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
select_version(INDEX, 21, Some("21.0.2")).expect("目录页里有 21.0.2"),
|
||||||
|
"21.0.2"
|
||||||
|
);
|
||||||
|
// 不给版本 = 今天的行为(最新)。
|
||||||
|
assert_eq!(select_version(INDEX, 21, None).expect("最新"), "21.0.2");
|
||||||
|
let error = select_version(INDEX, 21, Some("21.0.9")).expect_err("目录页里没有 21.0.9");
|
||||||
|
assert!(error.message().contains("21.0.9"), "{}", error.message());
|
||||||
|
assert!(
|
||||||
|
error.message().contains("21.0.2") && error.message().contains("21.0.1"),
|
||||||
|
"错误里要列出可用版本:{}",
|
||||||
|
error.message()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,59 +11,108 @@
|
|||||||
//! 换下一家;同一家内部的镜像换源(TUNA→官方、华为云→南大)仍然归 `core::download` 引擎管。
|
//! 换下一家;同一家内部的镜像换源(TUNA→官方、华为云→南大)仍然归 `core::download` 引擎管。
|
||||||
|
|
||||||
use super::api;
|
use super::api;
|
||||||
use super::error::JavaError;
|
use super::catalog;
|
||||||
|
use super::error::{JavaError, JavaErrorKind};
|
||||||
use super::microsoft;
|
use super::microsoft;
|
||||||
use super::oracle;
|
use super::oracle;
|
||||||
use super::release::JdkRelease;
|
use super::release::JdkRelease;
|
||||||
use super::target::AdoptiumTarget;
|
use super::target::AdoptiumTarget;
|
||||||
use super::vendor::JdkVendor;
|
use super::vendor::JdkVendor;
|
||||||
use super::zulu;
|
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
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// 平台无产物、清单不可达、该家没有这个大版本、校验缺失时返回错误——调用方据此降级。
|
/// 平台无产物、清单不可达、该版本不存在、校验缺失、这家不支持指定版本时返回错误——调用方据此降级。
|
||||||
pub async fn fetch(
|
pub async fn fetch(
|
||||||
engine: &DownloadEngine,
|
engine: &DownloadEngine,
|
||||||
vendor: JdkVendor,
|
vendor: JdkVendor,
|
||||||
major: u32,
|
major: u32,
|
||||||
target: AdoptiumTarget,
|
target: AdoptiumTarget,
|
||||||
|
version: Option<&str>,
|
||||||
) -> Result<JdkRelease, JavaError> {
|
) -> Result<JdkRelease, JavaError> {
|
||||||
let client = engine.client();
|
let client = engine.client();
|
||||||
let table = engine.mirrors();
|
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 {
|
match vendor {
|
||||||
JdkVendor::Temurin => api::fetch_latest(client, major, target).await,
|
JdkVendor::Temurin => api::fetch_latest(client, major, target).await,
|
||||||
JdkVendor::Oracle => oracle::fetch_latest(client, table, major, target).await,
|
JdkVendor::Oracle => oracle::fetch_latest(client, table, major, target).await,
|
||||||
JdkVendor::Microsoft => {
|
JdkVendor::Microsoft => {
|
||||||
let table = table.microsoft_openjdk().ok_or_else(|| {
|
microsoft::fetch_latest(client, microsoft_table(table)?, major, target).await
|
||||||
JavaError::new(
|
|
||||||
super::error::JavaErrorKind::Metadata,
|
|
||||||
"镜像清单里没有 microsoft_openjdk 表",
|
|
||||||
)
|
|
||||||
.with_hint("升级 mirrors.json 到 v3,或改用 --vendor temurin")
|
|
||||||
})?;
|
|
||||||
microsoft::fetch_latest(client, table, major, target).await
|
|
||||||
}
|
}
|
||||||
JdkVendor::Zulu => {
|
JdkVendor::Zulu => zulu::fetch_latest(client, zulu_table(table)?, major, target).await,
|
||||||
let table = table.zulu().ok_or_else(|| {
|
JdkVendor::Termux => Err(termux_archive_error()),
|
||||||
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 源)")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 该家的指定版本(`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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct VendorAttempt {
|
pub struct VendorAttempt {
|
||||||
|
|||||||
Reference in new issue
Block a user