Files
Unlucky-Launcher/crates/cli/src/commands/java.rs
T
AskaEth b31ca636d1 feat(m203): CLI 三条下载命令改吃 download.* 设置(并发/重试/gh-proxy)
`ull install` / `ull java` / `ull pack` 此前各自 `DownloadEngine::new(EngineConfig::default(), …)`:
用户在设置里配的 `download.concurrency` / `retry_limit` / `gh_proxy` 一概不生效;M193 把出厂并发
从 64 改成 8 之后,它们还静默跟着降了。现在三处都经 `crates/cli/src/download.rs::engine(ctx)`:

- core:`shared/tuned.rs` 新增 `engine_with_tuning(cache_root, tune)` = `set_tuning` + `engine_for`,
  由 `shared.rs` 重导出;`engine_for` 的签名与行为一字未改(presenter 的两步语义原样保留)。
  放子模块是为了不动 `engine.rs`(那是 400 行零余量),`shared.rs` 仍 398 行。
- CLI:`download::tuning(ctx)` 用 `SettingStore::load(ctx.launcher_config(), launcher_schema())` 读
  三个键(与 `ull config set` 同一份文件),默认值/夹取/回落在 `DownloadTuning::from_settings`;
  键缺失或坏值静默回落出厂值,`engine()` 没有错误通道,安装不会因此中断。
- 顺带:`ull update` 复用 `install::build_engine`,也随之吃到设置。
- 测试:`crates/cli/src/download/tests.rs` 三条命令各一条(读 `config().max_concurrency` 与
  `concurrency_probe()` 的真实值)+ 一条「缺失/越界/负数/类型不对 ⇒ 静默回落出厂值」;
  `shared/tuned.rs` 两条守住「灌调优与取引擎是一步」。变异实测:pack 换回裸 `EngineConfig::default()`
  ⇒ 只有 pack 那条红(left 8 / right 2)。
2026-11-09 10:07:41 +08:00

347 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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::blocking::block_on;
use unlucky_core::download::{DownloadEngine, RegionProfile};
use unlucky_core::game::java::{
self, location, vendor, InstallRequest, 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_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`];真正的来源优先级在
/// [`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))
}
/// `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 {
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
}
}
/// 真正要装到哪(解析失败 → 用户错误,带可照着做的提示)。
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)
}
}
/// 落点模式 → 人读一行(含「没设覆盖键时默认落在哪」的解释)。
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` 形状。
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 错误(保留稳定码与提示)。
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()
}