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 set <key> <value> 写配置(走 setting schema 校验)
|
||||
java list 列出可用 Java(含每个 JDK 的实际落点)
|
||||
java install <major> [--vendor X] 安装托管 JDK(缺省按地区回退链选发行版;
|
||||
--vendor temurin|oracle|microsoft|zulu|termux 钉死一家)
|
||||
java install <major> [--vendor X] [--version V] [--dry-run]
|
||||
安装托管 JDK(缺省按地区回退链选发行版;
|
||||
--vendor temurin|oracle|microsoft|zulu|termux 钉死一家;
|
||||
--version 钉具体版本,--dry-run 只查版本不下载)
|
||||
java vendors 列出各发行版的镜像/支持版本/当前优先级
|
||||
agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP;
|
||||
[--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读)
|
||||
@@ -251,9 +253,22 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL
|
||||
"\
|
||||
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
|
||||
并打印每个 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 列出各发行版的镜像/支持版本/当前优先级顺序
|
||||
|
||||
指定版本(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 走):
|
||||
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
|
||||
standard Windows %APPDATA%\\Java\\<发行方>\\<版本>\\;macOS ~/Library/Application Support/Java/…
|
||||
|
||||
@@ -55,12 +55,16 @@ pub enum Command {
|
||||
ConfigSet(ConfigSetArgs),
|
||||
/// `java list`。
|
||||
JavaList,
|
||||
/// `java install <major> [--vendor X]`。
|
||||
/// `java install <major> [--vendor X] [--version V] [--dry-run]`。
|
||||
JavaInstall {
|
||||
/// 目标 JDK 主版本(8 / 17 / 21)。
|
||||
major: u32,
|
||||
/// 钉死的发行版(缺省走 D23 的回退链)。
|
||||
vendor: Option<unlucky_core::game::java::JdkVendor>,
|
||||
/// 钉死的具体版本(`--version`;`None` = 该家最新 GA)。
|
||||
version: Option<String>,
|
||||
/// 只查版本、打印将要下载的版本与 URL,不落盘(`--dry-run`)。
|
||||
dry_run: bool,
|
||||
},
|
||||
/// `java vendors`:列出各发行版的可用性 / 镜像 / 当前优先级。
|
||||
JavaVendors,
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use unlucky_core::game::java::JdkVendor;
|
||||
|
||||
use crate::error::CliError;
|
||||
|
||||
mod accounts;
|
||||
mod agent;
|
||||
mod instances;
|
||||
mod java;
|
||||
mod update;
|
||||
|
||||
use super::model::{
|
||||
@@ -45,7 +44,9 @@ pub fn parse(args: &[String]) -> Result<Invocation, CliError> {
|
||||
// 帮助/版本是「伪命令」,但不提前返回:`--help` 出现在命令前面时,
|
||||
// 主题要从后面收完的位置参数里取(`ull --help launch` 与 `ull launch --help` 等价)。
|
||||
"--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" => {
|
||||
index += 1;
|
||||
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),
|
||||
"diag" => parse_diag(tail),
|
||||
"config" => parse_config(tail),
|
||||
"java" => parse_java(tail),
|
||||
"java" => java::parse_java(tail),
|
||||
"accounts" => accounts::parse_accounts(tail),
|
||||
"agent" => agent::parse_agent(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> {
|
||||
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 serde_json::{json, Value};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::download::{DownloadEngine, RegionProfile};
|
||||
use unlucky_core::game::java::{
|
||||
self, location, vendor, InstallRequest, JavaOrigin, JavaRuntime, JdkVendor, LocationMode,
|
||||
DEFAULT_MAJOR,
|
||||
self, location, vendor, JavaOrigin, JavaRuntime, JdkVendor, LocationMode, DEFAULT_MAJOR,
|
||||
};
|
||||
use unlucky_core::launch::request::JavaSelection;
|
||||
use unlucky_core::platform::paths::ProcessEnv;
|
||||
@@ -31,12 +29,10 @@ use crate::context::Ctx;
|
||||
use crate::error::CliError;
|
||||
use crate::output::Report;
|
||||
|
||||
pub use super::java_install::install;
|
||||
pub use super::java_launch::{resolve_for_launch, LaunchJava};
|
||||
pub use super::java_vendors::vendors;
|
||||
|
||||
/// 可自动安装的最老大版本(再老的 Temurin 没有 GA 产物)。
|
||||
const MIN_INSTALLABLE_MAJOR: u32 = 8;
|
||||
|
||||
/// 装配用的 Java 选择(`--java` > `JAVA_HOME` > PATH 上的 `java`)。
|
||||
///
|
||||
/// `snapshot::prepare` 调它拿 [`JavaSelection`];真正的来源优先级在
|
||||
@@ -131,83 +127,6 @@ pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
|
||||
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`)。
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
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(
|
||||
ctx.data.mode,
|
||||
&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 {
|
||||
LocationMode::Custom => format!(
|
||||
"custom(java.installRoot = {})",
|
||||
@@ -282,7 +201,7 @@ fn describe_mode(mode: LocationMode, override_root: Option<&Path>) -> String {
|
||||
}
|
||||
|
||||
/// 一个运行时的 `--json` 形状。
|
||||
fn candidate_json(runtime: &JavaRuntime) -> Value {
|
||||
pub(super) fn candidate_json(runtime: &JavaRuntime) -> Value {
|
||||
json!({
|
||||
"binary": runtime.binary.display().to_string(),
|
||||
"source": runtime.origin.as_str(),
|
||||
@@ -314,7 +233,7 @@ pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
}
|
||||
|
||||
/// 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());
|
||||
match error.hint() {
|
||||
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 instances;
|
||||
pub mod java;
|
||||
pub mod java_install;
|
||||
pub mod java_launch;
|
||||
pub mod java_vendors;
|
||||
pub mod launch;
|
||||
@@ -58,8 +59,13 @@ pub fn run(invocation: &Invocation) -> Result<Outcome, CliError> {
|
||||
config::set(ctx, &args.key, &args.value)
|
||||
}),
|
||||
Command::JavaList => with_ctx(&invocation.globals, java::list),
|
||||
Command::JavaInstall { major, vendor } => with_ctx(&invocation.globals, |ctx| {
|
||||
java::install(ctx, *major, *vendor)
|
||||
Command::JavaInstall {
|
||||
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::UpdateCheck(args) => {
|
||||
|
||||
Reference in new issue
Block a user