Unlucky Launcher v0.0.1-alpha
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s

Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
This commit is contained in:
AskaEth committed 2026-10-31 21:54:10 +08:00
commit bc41dfcf7d
517 files changed
+92844

No files matched your search

+365
View File
@@ -0,0 +1,365 @@
//! 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::cache::now_ms;
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
use unlucky_core::download::region_system::detect_signals;
use unlucky_core::download::{
ContentCache, DownloadEngine, EngineConfig, HealthRegistry, MirrorTable, 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)用 `ull java install 21` 装一个,或设 JAVA_HOME / `ull 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("`ull 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())
}
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,地区判定走 24h 缓存 + 环境信号。
fn build_engine(ctx: &Ctx) -> DownloadEngine {
let layout = ctx.layout();
let cache_root = layout.cache();
let now = now_ms();
let region_path = cache_root.join("region.json");
let region = load_cached(&region_path, now).unwrap_or_else(|| {
let profile = decide_from(&detect_signals(), now);
let _ = store_cached(&region_path, &profile);
profile
});
DownloadEngine::new(
EngineConfig::default(),
Arc::new(MirrorTable::builtin()),
Arc::new(region),
Arc::new(HealthRegistry::open_in(&cache_root, now)),
Arc::new(ContentCache::open(&cache_root)),
)
}
/// 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()
}