用户原话:「我们内置了那么多镜像源 甚至用户不能手动选择指定版本下载。」这条之前
`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。
266 lines
10 KiB
Rust
266 lines
10 KiB
Rust
//! Java 运行时:`ull java list` / `install` / `vendors`(D13 v1 + D23 v2)。
|
||
//!
|
||
//! 四件事:建下载引擎、把 core 的结果排成人读与 `--json` 两种形态、把错误翻成稳定码、
|
||
//! 从设置里取 JDK 的落点与发行版优先级。探测/下载/解压/映射/回退链全在 `core::game::java`。
|
||
//!
|
||
//! D23 之后的三个用户可见变化:
|
||
//!
|
||
//! 1. **落点可选**:`java.installRoot` 设了就装到那里;否则 Standard 数据模式装到
|
||
//! `%APPDATA%\Java\<发行方>\<版本>\`(mac/Linux 见 `game::java::location`),
|
||
//! Portable 仍在数据根 `java-managed/`(v1 布局不变);
|
||
//! 2. **发行版可选**:`--vendor temurin|oracle|microsoft|zulu` 钉死一家;不给就按
|
||
//! `java.vendorPriority` → 地区默认序跑回退链,降级原因进 `--json` 的 `chain` 块;
|
||
//! 3. **`java vendors`** 一眼看清四家的可用性、镜像与当前顺序。
|
||
|
||
use std::path::{Path, PathBuf};
|
||
use std::sync::Arc;
|
||
|
||
use serde_json::{json, Value};
|
||
use unlucky_core::download::{DownloadEngine, RegionProfile};
|
||
use unlucky_core::game::java::{
|
||
self, location, vendor, JavaOrigin, JavaRuntime, JdkVendor, LocationMode, DEFAULT_MAJOR,
|
||
};
|
||
use unlucky_core::launch::request::JavaSelection;
|
||
use unlucky_core::platform::paths::ProcessEnv;
|
||
use unlucky_core::setting::keys::launcher_schema;
|
||
use unlucky_core::setting::store::SettingStore;
|
||
|
||
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;
|
||
|
||
/// 装配用的 Java 选择(`--java` > `JAVA_HOME` > PATH 上的 `java`)。
|
||
///
|
||
/// `snapshot::prepare` 调它拿 [`JavaSelection`];真正的来源优先级在
|
||
/// [`resolve_for_launch`] 里求值(那一步还会看托管目录与版本要求)。
|
||
#[must_use]
|
||
pub fn selection(explicit: Option<&Path>) -> JavaSelection {
|
||
let (binary, origin) = match explicit {
|
||
Some(path) => (path.to_path_buf(), JavaOrigin::Instance),
|
||
None => match env_java() {
|
||
Some(binary) => (binary, JavaOrigin::JavaHome),
|
||
None => (PathBuf::from(java::java_binary_name()), JavaOrigin::Path),
|
||
},
|
||
};
|
||
let runtime = java::probe(&binary, origin);
|
||
JavaSelection::new(binary, runtime.major.unwrap_or(DEFAULT_MAJOR), runtime.bits)
|
||
}
|
||
|
||
/// 托管 JDK 可能存在的全部安装根(Portable 数据根 + Standard/Custom 落点,D23 后不止一个)。
|
||
#[must_use]
|
||
pub fn managed_roots(ctx: &Ctx) -> Vec<PathBuf> {
|
||
let settings = JavaSettings::load(ctx);
|
||
location::scan_roots(
|
||
ctx.data.mode,
|
||
&ctx.layout(),
|
||
settings.install_root.as_deref(),
|
||
ctx.platform,
|
||
&ProcessEnv,
|
||
)
|
||
}
|
||
|
||
/// `ull java list`:探测托管目录 / `JAVA_HOME` / `PATH` / 实例覆盖,并显示各 JDK 的落点。
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// 实例清单读不了时返回用户错误(其余探测失败只体现为缺项)。
|
||
pub fn list(ctx: &Ctx) -> Result<Report, CliError> {
|
||
let settings = JavaSettings::load(ctx);
|
||
let roots = managed_roots(ctx);
|
||
// 托管 → JAVA_HOME → PATH → 标准安装位置(Program Files 系 / 官方启动器 runtime / 注册表),
|
||
// 同一路径只列一次且保留优先级高者(`discover::dedupe` 负责)。
|
||
let mut candidates = java::candidates_in_roots(&roots);
|
||
let profiles = super::instances::read_profiles(ctx)?;
|
||
for profile in &profiles.profiles {
|
||
if let Some(java_path) = &profile.java_path {
|
||
candidates.push(java::probe(java_path, JavaOrigin::Instance));
|
||
}
|
||
}
|
||
let candidates = java::discover::dedupe(candidates);
|
||
let candidates: Vec<JavaRuntime> = candidates
|
||
.into_iter()
|
||
.filter(|candidate| candidate.major.is_some())
|
||
.collect();
|
||
|
||
let mode = settings.location_mode(ctx);
|
||
let installed_root = settings.location(ctx).map(|root| root.root).ok();
|
||
let data = json!({
|
||
// 兼容 v1 的 `managed_root`(首个扫描根),同时给出多根清单(D23 之后不止一个根)。
|
||
"managed_root": roots.first().map(|root| root.display().to_string()),
|
||
"managed_roots": roots.iter().map(|root| root.display().to_string()).collect::<Vec<_>>(),
|
||
"install": {
|
||
"mode": mode.as_str(),
|
||
"root": installed_root.as_ref().map(|root| root.display().to_string()),
|
||
"override": settings.install_root.as_ref().map(|root| root.display().to_string()),
|
||
},
|
||
"candidates": candidates.iter().map(candidate_json).collect::<Vec<_>>(),
|
||
});
|
||
let mut human = vec![format!("Java {} 个", candidates.len())];
|
||
if candidates.is_empty() {
|
||
human.push(
|
||
"(没找到可用 Java)用 `%prog% java install 21` 装一个,或设 JAVA_HOME / `%prog% launch --java <path>`"
|
||
.to_string(),
|
||
);
|
||
}
|
||
human.push(format!(
|
||
"落点模式:{}",
|
||
describe_mode(mode, settings.install_root.as_deref())
|
||
));
|
||
for root in &roots {
|
||
human.push(format!(" 扫描根:{}", root.display()));
|
||
}
|
||
for candidate in &candidates {
|
||
human.push(format!(
|
||
" {} {} [{}]",
|
||
candidate
|
||
.version
|
||
.clone()
|
||
.unwrap_or_else(|| "版本未知".to_string()),
|
||
candidate.binary.display(),
|
||
source_label(candidate)
|
||
));
|
||
}
|
||
Ok(Report::new(data, human))
|
||
}
|
||
|
||
/// 设置里与 JDK 有关的两项(`java.installRoot` / `java.vendorPriority`)。
|
||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||
pub(crate) struct JavaSettings {
|
||
pub(crate) install_root: Option<PathBuf>,
|
||
pub(crate) priority: Vec<JdkVendor>,
|
||
}
|
||
|
||
impl JavaSettings {
|
||
/// 读 `<数据根>/.ULL/config/launcher.json`(坏文件退化成默认值,不阻断命令)。
|
||
pub(crate) fn load(ctx: &Ctx) -> Self {
|
||
let store = SettingStore::load(ctx.launcher_config(), launcher_schema());
|
||
let install_root = store
|
||
.doc()
|
||
.effective_str("java.installRoot")
|
||
.map(str::trim)
|
||
.filter(|text| !text.is_empty())
|
||
.map(PathBuf::from);
|
||
let priority = store
|
||
.effective("java.vendorPriority")
|
||
.and_then(Value::as_array)
|
||
.map(|items| {
|
||
let texts: Vec<String> = items
|
||
.iter()
|
||
.filter_map(Value::as_str)
|
||
.map(str::to_string)
|
||
.collect();
|
||
vendor::parse_priority(&texts)
|
||
})
|
||
.unwrap_or_default();
|
||
Self {
|
||
install_root,
|
||
priority,
|
||
}
|
||
}
|
||
|
||
/// 落点模式(只描述,不建目录)。
|
||
fn location_mode(&self, ctx: &Ctx) -> LocationMode {
|
||
if self.install_root.is_some() {
|
||
LocationMode::Custom
|
||
} else if ctx.data.mode == unlucky_core::platform::data_dir::DataMode::Standard {
|
||
LocationMode::Standard
|
||
} else {
|
||
LocationMode::Portable
|
||
}
|
||
}
|
||
|
||
/// 真正要装到哪(解析失败 → 用户错误,带可照着做的提示)。
|
||
pub(super) fn location(&self, ctx: &Ctx) -> Result<java::JavaInstallRoot, CliError> {
|
||
java::JavaInstallRoot::resolve(
|
||
ctx.data.mode,
|
||
&ctx.layout(),
|
||
self.install_root.as_deref(),
|
||
ctx.platform,
|
||
&ProcessEnv,
|
||
)
|
||
.map_err(map_java_error)
|
||
}
|
||
}
|
||
|
||
/// 落点模式 → 人读一行(含「没设覆盖键时默认落在哪」的解释)。
|
||
pub(super) fn describe_mode(mode: LocationMode, override_root: Option<&Path>) -> String {
|
||
match mode {
|
||
LocationMode::Custom => format!(
|
||
"custom(java.installRoot = {})",
|
||
override_root.unwrap_or(Path::new("?")).display()
|
||
),
|
||
LocationMode::Standard => {
|
||
"standard(%APPDATA%\\Java\\<发行方>\\<版本> 等系统标准目录)".to_string()
|
||
}
|
||
LocationMode::Portable => "portable(<数据根>/.ULL/data/java-managed)".to_string(),
|
||
}
|
||
}
|
||
|
||
/// 一个运行时的 `--json` 形状。
|
||
pub(super) fn candidate_json(runtime: &JavaRuntime) -> Value {
|
||
json!({
|
||
"binary": runtime.binary.display().to_string(),
|
||
"source": runtime.origin.as_str(),
|
||
"source_category": runtime.origin.category(),
|
||
"version": runtime.version,
|
||
"major": runtime.major,
|
||
"bits": runtime.bits,
|
||
"vendor": runtime.vendor,
|
||
"install_root": install_root_of(runtime),
|
||
})
|
||
}
|
||
|
||
/// 托管 JDK 的实际目录(`bin/java` 的上两级);非托管来源为 `None`。
|
||
fn install_root_of(runtime: &JavaRuntime) -> Option<String> {
|
||
if runtime.origin != JavaOrigin::Managed {
|
||
return None;
|
||
}
|
||
runtime
|
||
.binary
|
||
.parent()
|
||
.and_then(Path::parent)
|
||
.map(|dir| dir.display().to_string())
|
||
}
|
||
|
||
/// 建下载引擎:装配参数来自设置(并发 / 重试 / gh-proxy,读数口径见 [`crate::download`]),
|
||
/// 引擎本体与 `install` / `pack` 共用同一个(core 按缓存根共享)。
|
||
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||
crate::download::engine(ctx)
|
||
}
|
||
|
||
/// JDK 下载/解压错误 → CLI 错误(保留稳定码与提示)。
|
||
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()),
|
||
None => cli,
|
||
}
|
||
}
|
||
|
||
/// `JAVA_HOME/bin/java`(存在才返回)。
|
||
fn env_java() -> Option<PathBuf> {
|
||
let home = std::env::var_os("JAVA_HOME")?;
|
||
let binary = PathBuf::from(home)
|
||
.join("bin")
|
||
.join(java::java_binary_name());
|
||
binary.is_file().then_some(binary)
|
||
}
|
||
|
||
/// 运行时 → 展示标签(托管项带上 vendor)。
|
||
fn source_label(runtime: &JavaRuntime) -> String {
|
||
match &runtime.vendor {
|
||
Some(vendor) => format!("{} · {vendor}", runtime.origin.as_str()),
|
||
None => runtime.origin.as_str().to_string(),
|
||
}
|
||
}
|
||
|
||
/// 当前地区档案(`java vendors` 展示「国内/国外」口径;也给 GUI 复用)。
|
||
#[must_use]
|
||
pub fn region_profile(ctx: &Ctx) -> RegionProfile {
|
||
build_engine(ctx).region().clone()
|
||
}
|