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
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:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,379 @@
|
||||
//! 托管 JDK 安装编排:发行版回退链 → 下载引擎 → 解压 → `java -version` 验证。
|
||||
//!
|
||||
//! ```text
|
||||
//! vendor::order(用户优先级, 地区) D23 的尝试顺序
|
||||
//! → source::fetch(vendor, major) 查到该家最新 GA(Temurin/Oracle/MS/Zulu)
|
||||
//! → location::JavaInstallRoot::jdk_dir 按落点模式算目录
|
||||
//! → [戳记 sha256 相同 && bin/java 在] 复用 幂等的第一层
|
||||
//! → engine.download(JavaRuntime 请求) 镜像/健康度/退避/续传/边下边算全在引擎
|
||||
//! → extract::extract_archive zip / tar.gz + 幂等戳记
|
||||
//! → probe::probe(bin/java -version) 幂等的第二层,也是「装完真能用」的证据
|
||||
//! ```
|
||||
//!
|
||||
//! **回退的粒度是发行版**(D23):该家没有这个大版本、镜像全 4xx、sha256 取不到、下载校验失败,
|
||||
//! 都记一条降级原因然后换下一家;只有全部失败才整体报错,错误里带上每一跳的原因。
|
||||
//!
|
||||
//! 这一步**不拼任何镜像 URL**(Oracle 的哈希段不可推导,见 `oracle` 模块):Temurin 的
|
||||
//! GitHub 直链交给 `MirrorTable::expand` 展开成院校镜像,Oracle/MS/Zulu 的链接由各自模块
|
||||
//! 从目录页/模板/API 拿到最终形态,引擎只负责「把这个 URL 下下来并校验」。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::error::{io_error, JavaError, JavaErrorKind};
|
||||
use super::extract;
|
||||
use super::location::{JavaInstallRoot, LocationMode};
|
||||
use super::probe::{self, JavaOrigin, JavaRuntime};
|
||||
use super::release::JdkRelease;
|
||||
use super::source::{self, ChainLog};
|
||||
use super::target::AdoptiumTarget;
|
||||
use super::vendor::{self, JdkVendor};
|
||||
use crate::download::{DownloadEngine, DownloadRequest, ResourceKind};
|
||||
|
||||
/// 归档缓存目录名(在安装根下;托管目录扫描跳过它)。
|
||||
pub use super::location::CACHE_DIR;
|
||||
|
||||
/// 一次安装请求。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InstallRequest {
|
||||
/// 目标大版本。
|
||||
pub major: u32,
|
||||
/// 钉死某一家发行版(`--vendor`);`None` = 按回退链自己选。
|
||||
pub vendor: Option<JdkVendor>,
|
||||
/// 用户优先级(`java.vendorPriority`,空 = 按地区默认序)。
|
||||
pub priority: Vec<JdkVendor>,
|
||||
}
|
||||
|
||||
impl InstallRequest {
|
||||
/// 只要大版本(走回退链,优先级用地区默认)。
|
||||
#[must_use]
|
||||
pub const fn new(major: u32) -> Self {
|
||||
Self {
|
||||
major,
|
||||
vendor: None,
|
||||
priority: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 钉死发行版。
|
||||
#[must_use]
|
||||
pub const fn with_vendor(major: u32, vendor: JdkVendor) -> Self {
|
||||
Self {
|
||||
major,
|
||||
vendor: Some(vendor),
|
||||
priority: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 带上用户优先级(只影响没有 `--vendor` 的情形)。
|
||||
#[must_use]
|
||||
pub fn with_priority(mut self, priority: Vec<JdkVendor>) -> Self {
|
||||
self.priority = priority;
|
||||
self
|
||||
}
|
||||
|
||||
/// 实际尝试顺序(`--vendor` 钉死时链里只放它)。
|
||||
#[must_use]
|
||||
pub fn order(&self, domestic: bool) -> Vec<JdkVendor> {
|
||||
match self.vendor {
|
||||
Some(vendor) => vec![vendor],
|
||||
None => vendor::order(&self.priority, domestic),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 会真正进入回退链的发行版(顺序 + 能力过滤)。
|
||||
///
|
||||
/// 与 [`install`] 的循环同源,但**纯**:不联网、不落盘,给 `--dry-run` 与离线测试用。
|
||||
/// 过滤条件只有一条——该家发不发这个 GA 大版本(MS 没有 8、Oracle 从 9 起)。
|
||||
/// `--vendor` 钉死时**不过滤**:链里就它一个,由 `install` 明确报「不支持 Java X」,
|
||||
/// 而不是悄悄把用户点名的发行版换成别的。
|
||||
#[must_use]
|
||||
pub fn chain_order(request: &InstallRequest, domestic: bool) -> Vec<JdkVendor> {
|
||||
if request.vendor.is_some() {
|
||||
return request.order(domestic);
|
||||
}
|
||||
request
|
||||
.order(domestic)
|
||||
.into_iter()
|
||||
.filter(|vendor| vendor.supports(request.major))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 一次成功安装的结果。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InstalledJdk {
|
||||
/// 大版本号。
|
||||
pub major: u32,
|
||||
/// 实际装上的发行版。
|
||||
pub vendor: JdkVendor,
|
||||
/// 版本查询结果(含归档名与 sha256)。
|
||||
pub release: JdkRelease,
|
||||
/// 托管目录(Portable:`java-managed/jdk-21-temurin`;Standard:`Java/oracle/21.0.2`)。
|
||||
pub root: PathBuf,
|
||||
/// 落点模式(报告里要说清「装到哪」)。
|
||||
pub mode: LocationMode,
|
||||
/// 归档路径(缓存)。
|
||||
pub archive: PathBuf,
|
||||
/// 探测到的运行时。
|
||||
pub runtime: JavaRuntime,
|
||||
/// 实际下载的源标识(`None` = 复用已装,没走网络)。
|
||||
pub downloaded: Option<String>,
|
||||
/// 本次下载字节数(复用已装时为 0)。
|
||||
pub bytes: u64,
|
||||
/// 本次解压的文件数(复用已装时可能为 0)。
|
||||
pub files: usize,
|
||||
/// 本次是否真的解压了。
|
||||
pub extracted: bool,
|
||||
/// 回退链记录(命中与降级原因)。
|
||||
pub chain: ChainLog,
|
||||
}
|
||||
|
||||
impl InstalledJdk {
|
||||
/// 是否完全复用了已装的那一份。
|
||||
#[must_use]
|
||||
pub fn reused(&self) -> bool {
|
||||
self.downloaded.is_none()
|
||||
}
|
||||
|
||||
/// `bin/java[.exe]` 路径。
|
||||
#[must_use]
|
||||
pub fn binary(&self) -> PathBuf {
|
||||
extract::java_binary(&self.root)
|
||||
}
|
||||
|
||||
/// 实际落点(人读一行)。
|
||||
#[must_use]
|
||||
pub fn location_line(&self) -> String {
|
||||
format!("{} · {}", self.mode.as_str(), self.root.display())
|
||||
}
|
||||
}
|
||||
|
||||
/// 托管目录路径(Portable 布局 `java-managed/jdk-<major>-<vendor>`)。
|
||||
#[must_use]
|
||||
pub fn managed_root_dir(managed_root: &Path, major: u32, vendor: &str) -> PathBuf {
|
||||
managed_root.join(probe::managed_dir_name(major, vendor))
|
||||
}
|
||||
|
||||
/// 归档缓存路径(`<root>/.cache/<归档名>`)。
|
||||
#[must_use]
|
||||
pub fn archive_path(root: &Path, release: &JdkRelease) -> PathBuf {
|
||||
root.join(CACHE_DIR).join(&release.archive.name)
|
||||
}
|
||||
|
||||
/// 归档下载请求(`ResourceKind::JavaRuntime` + sha256 + 期望大小)。
|
||||
#[must_use]
|
||||
pub fn archive_request(release: &JdkRelease, dest: PathBuf) -> DownloadRequest {
|
||||
let request =
|
||||
DownloadRequest::new(ResourceKind::JavaRuntime, release.archive.url.clone(), dest)
|
||||
.with_sha256(release.archive.sha256.clone());
|
||||
if release.archive.size > 0 {
|
||||
request.with_size(release.archive.size)
|
||||
} else {
|
||||
request
|
||||
}
|
||||
}
|
||||
|
||||
/// 按优先级链装一个托管 JDK(幂等:同一份归档装过就直接复用)。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 全部发行版都失败(没有该大版本 / 平台无产物 / 网络 / 校验 / 解压 / 装完跑不起来)时
|
||||
/// 返回错误,`message()` 里带着每一跳的降级原因。
|
||||
pub async fn install(
|
||||
engine: &DownloadEngine,
|
||||
location: &JavaInstallRoot,
|
||||
request: InstallRequest,
|
||||
) -> Result<InstalledJdk, JavaError> {
|
||||
let target = AdoptiumTarget::host()?;
|
||||
let domestic = engine.region().is_domestic();
|
||||
let priority = request.order(domestic);
|
||||
let mut chain = ChainLog::new();
|
||||
let mut reasons: Vec<String> = Vec::new();
|
||||
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
|
||||
let _ = super::location::cleanup_orphans(&location.root);
|
||||
|
||||
for candidate in &priority {
|
||||
let vendor = *candidate;
|
||||
if !vendor.supports(request.major) {
|
||||
let reason = format!(
|
||||
"不支持 Java {}({} 只发 {})",
|
||||
request.major,
|
||||
vendor.id(),
|
||||
majors_text(vendor)
|
||||
);
|
||||
chain.skip(vendor, reason.clone());
|
||||
reasons.push(format!("{}:{reason}", vendor.id()));
|
||||
continue;
|
||||
}
|
||||
let release = match source::fetch(engine, vendor, request.major, target).await {
|
||||
Ok(release) => release,
|
||||
Err(error) => {
|
||||
chain.failed(vendor, error.message().to_string());
|
||||
reasons.push(format!("{}:{}", vendor.id(), error.message()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match install_one(engine, location, &release).await {
|
||||
Ok(prepared) => {
|
||||
chain.hit(vendor, format!("命中 {}", release.summary()));
|
||||
return Ok(InstalledJdk {
|
||||
major: release.major,
|
||||
vendor,
|
||||
release,
|
||||
root: prepared.root,
|
||||
mode: location.mode,
|
||||
archive: prepared.archive,
|
||||
runtime: prepared.runtime,
|
||||
downloaded: prepared.downloaded,
|
||||
bytes: prepared.bytes,
|
||||
files: prepared.files,
|
||||
extracted: prepared.extracted,
|
||||
chain,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
chain.failed(vendor, error.message().to_string());
|
||||
reasons.push(format!("{}:{}", vendor.id(), error.message()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(JavaError::new(
|
||||
JavaErrorKind::Metadata,
|
||||
format!(
|
||||
"没有装成 Java {}(试过 {}):{}",
|
||||
request.major,
|
||||
priority.len(),
|
||||
reasons.join(";")
|
||||
),
|
||||
)
|
||||
.with_hint("`ull java vendors` 看各发行版的可用性;或手动装 JDK 后设 JAVA_HOME"))
|
||||
}
|
||||
|
||||
/// 某一家的中间结果(把七元组元组换成具名结构,读起来才不费劲)。
|
||||
struct Prepared {
|
||||
root: PathBuf,
|
||||
archive: PathBuf,
|
||||
runtime: JavaRuntime,
|
||||
downloaded: Option<String>,
|
||||
bytes: u64,
|
||||
files: usize,
|
||||
extracted: bool,
|
||||
}
|
||||
|
||||
/// 装某一家的那一份(下载 → 解压 → 探测)。
|
||||
async fn install_one(
|
||||
engine: &DownloadEngine,
|
||||
location: &JavaInstallRoot,
|
||||
release: &JdkRelease,
|
||||
) -> Result<Prepared, JavaError> {
|
||||
let root = location.jdk_dir(release.major, release.vendor, &release.version);
|
||||
let binary = extract::java_binary(&root);
|
||||
let archive = archive_path(&location.root, release);
|
||||
|
||||
// 幂等第一层:戳记里的 sha256 与本次一致,且 bin/java 在 → 一个字节都不动。
|
||||
if extract::is_ready(&root, release.sha256()) {
|
||||
let runtime = probe::probe(&binary, JavaOrigin::Managed);
|
||||
if runtime.major == Some(release.major) {
|
||||
let mut runtime = runtime;
|
||||
runtime.vendor = Some(release.vendor.id().to_string());
|
||||
return Ok(Prepared {
|
||||
root,
|
||||
archive,
|
||||
runtime,
|
||||
downloaded: None,
|
||||
bytes: 0,
|
||||
files: 0,
|
||||
extracted: false,
|
||||
});
|
||||
}
|
||||
note(&format!(
|
||||
"托管目录 {} 里的 java 跑不起来,重装一份",
|
||||
root.display()
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(parent) = archive.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|error| io_error("创建归档缓存目录", parent.display(), &error))?;
|
||||
}
|
||||
let outcome = engine
|
||||
.download(archive_request(release, archive.clone()))
|
||||
.await?;
|
||||
let report = extract::extract_archive(&archive, release.sha256(), release.major, &root)?;
|
||||
let mut runtime = probe::probe(&binary, JavaOrigin::Managed);
|
||||
runtime.vendor = Some(release.vendor.id().to_string());
|
||||
if runtime.major != Some(release.major) {
|
||||
return Err(JavaError::new(
|
||||
JavaErrorKind::Probe,
|
||||
format!(
|
||||
"装好的 JDK 探测结果是 Java {},与请求的 {} 不符({})",
|
||||
runtime
|
||||
.major
|
||||
.map_or_else(|| "未知".to_string(), |major| major.to_string()),
|
||||
release.major,
|
||||
binary.display()
|
||||
),
|
||||
)
|
||||
.with_hint("删掉该托管目录后重跑 `ull java install`;或手动设 JAVA_HOME"));
|
||||
}
|
||||
Ok(Prepared {
|
||||
root,
|
||||
archive,
|
||||
runtime,
|
||||
downloaded: Some(outcome.source),
|
||||
bytes: outcome.bytes,
|
||||
files: report.files,
|
||||
extracted: report.wrote(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 只做规划不落盘:返回将要安装的版本、目标目录与「是否已经装好」。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 与 [`install`] 的查询段相同(平台不支持 / 网络失败 / 所有发行版都没有该大版本)。
|
||||
pub async fn plan(
|
||||
engine: &DownloadEngine,
|
||||
location: &JavaInstallRoot,
|
||||
major: u32,
|
||||
) -> Result<(JdkRelease, PathBuf, bool), JavaError> {
|
||||
let target = AdoptiumTarget::host()?;
|
||||
let domestic = engine.region().is_domestic();
|
||||
let mut reasons: Vec<String> = Vec::new();
|
||||
for vendor in vendor::order(&[], domestic) {
|
||||
if !vendor.supports(major) {
|
||||
reasons.push(format!("{}:不支持 Java {major}", vendor.id()));
|
||||
continue;
|
||||
}
|
||||
match source::fetch(engine, vendor, major, target).await {
|
||||
Ok(release) => {
|
||||
let root = location.jdk_dir(release.major, release.vendor, &release.version);
|
||||
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(
|
||||
JavaErrorKind::Metadata,
|
||||
format!("没有可用的 Java {major}({} )", reasons.join(";")),
|
||||
))
|
||||
}
|
||||
|
||||
fn majors_text(vendor: JdkVendor) -> String {
|
||||
vendor
|
||||
.hint_majors()
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
fn note(message: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("[ull::game::java] {message}");
|
||||
} else {
|
||||
let _ = message;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user