Files
Unlucky-Launcher/crates/cli/src/commands/install/mod.rs
T
AskaEth 0522a41a52
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
release: v0.2.5
2026-11-12 12:06:59 +08:00

146 lines
6.3 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.
//! `ull install <version> [--loader <kind>] [--loader-version <v>]`:装版本本体 / 装加载器层。
//!
//! ```text
//! --loader 缺省 → vanilla:M27 原版安装(resolve → plan → execute → register)
//! --loader fabric|quilt → addon::loader:原版(已装则跳)→ loader 版本 JSON → 补 loader 库 → register
//! --loader neoforge|forge → 同上;需要 installer processors 的版本再跑 M35 执行器(依赖库 → java → 版本 JSON)
//! --loader liteloader → 官方版本表 → 从零合成 launchwrapper 清单(不跑任何外部进程)
//! ```
//!
//! CLI 只做四件事:建下载引擎、把计划/结果排版成人读与 `--json` 两种形态
//! ([`report`] / [`report_loader`])、把错误翻译成稳定错误码、往 stderr 打实时进度。
//! 所有版本/继承/库/下载逻辑都在 core(CLI 不复制任何路径或镜像规则)。
mod loader;
mod loader_processors;
mod progress;
mod report;
mod report_loader;
mod vanilla;
use std::sync::Arc;
use unlucky_core::addon::loader::LoaderKind;
use unlucky_core::download::{DownloadEngine, DownloadErrorKind};
use unlucky_core::game::install::{InstallError, InstallReport, Registration};
use unlucky_core::game::LoaderKind as WideLoaderKind;
use crate::cli::InstallArgs;
use crate::context::Ctx;
use crate::error::CliError;
use crate::output::Report;
/// 文件级并发上限:资源是海量小文件,16 路足够压满带宽又不至于打爆镜像。
pub(super) const CONCURRENCY: usize = 16;
/// 执行 `install`:按 `--loader` 分发。
///
/// "认不出" 与 "认得但装不了" 是两件事,错误码也分开(`loader_unknown` / `loader_unsupported`):
/// `optifine` 属于后者——core 的识别词表里有它(`ull help instances` 也这么写),只是安装管线
/// 没接(要跑外部 `optifine.Patcher` + 原地改 zip)。两者混成一句"未知加载器"会让用户以为
/// 是自己的版本 id 写错了。
///
/// # Errors
///
/// 用法错误、加载器未知/认得但装不了、版本不存在、网络失败、校验不符、写盘失败、实例清单损坏。
pub fn run(ctx: &Ctx, args: &InstallArgs) -> Result<Report, CliError> {
match args
.loader
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
None | Some("vanilla") | Some("none") => vanilla::run(ctx, args),
Some(raw) => match LoaderKind::parse(raw) {
Some(kind) => loader::run(ctx, args, kind),
None => Err(match WideLoaderKind::parse(raw) {
Some(WideLoaderKind::OptiFine) => CliError::user(
"loader_unsupported",
"optifine 认得出但还没接入安装管线(要跑外部 Patcher 并原地改 zip)",
)
.with_hint(
"先用官方安装器装好,再 `%prog% instances list` 确认它已在;或用 fabric / quilt / neoforge / forge / liteloader",
),
_ => CliError::user("loader_unknown", format!("未知加载器 `{raw}`")).with_hint(
"--loader 取值:vanilla / fabric / quilt / neoforge / forge / liteloader(optifine 认得出但装不了)",
),
}),
},
}
}
/// 登记实例(幂等):`profiles.json` + `config/instances/<id>.json`。
///
/// 原版安装与加载器安装共用:前者 `version_id` = 原版 id,后者 = loader 版本 id
/// (实例的 `versionId` 必须指向 loader 版本,否则启动会跑成原版)。
pub(super) fn register_instance(
ctx: &Ctx,
name: &str,
version_id: &str,
) -> Result<Registration, CliError> {
let created_at =
unlucky_core::launch::time::format_rfc3339_millis(unlucky_core::launch::time::now());
unlucky_core::game::install::instance::register(
&ctx.layout(),
name,
version_id,
Some(&created_at),
)
.map_err(|error| CliError::user("instance_register", format!("登记实例失败:{error}")))
}
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,装配参数来自设置(并发 / 重试 / gh-proxy,
/// 读数口径见 [`crate::download`])——不再用裸 `EngineConfig::default()`(M203)。
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
crate::download::engine(ctx)
}
/// 同名但绑定别的版本 → 冲突;同名同版本视为重装(复用实例,允许)。
pub(super) fn ensure_name_free(ctx: &Ctx, name: &str, version: &str) -> Result<(), CliError> {
let profiles = super::instances::read_profiles(ctx)?;
let conflict = profiles
.profiles
.iter()
.find(|profile| profile.name == name && profile.version_id != version);
match conflict {
Some(profile) => Err(CliError::user(
"instance_exists",
format!("同名实例已存在:{name}(绑定版本 {})", profile.version_id),
)
.with_hint("用 --name 换个名字,或 `%prog% instances list` 看看现有实例")),
None => Ok(()),
}
}
/// 安装期错误 → CLI 错误(保留稳定错误码与提示)。
pub(super) fn map_install_error(error: &InstallError, ctx: &Ctx) -> CliError {
let hint = match error.hint() {
Some(hint) => format!("{hint}(游戏根:{})", ctx.game().root().display()),
None => format!("游戏根:{}", ctx.game().root().display()),
};
CliError::user(error.kind().as_str(), error.message().to_string()).with_hint(hint)
}
/// 有文件失败 → 按"最严重的失败类别"给稳定码,并列出前几条明细。
pub(super) fn failure_error(report: &InstallReport, ctx: &Ctx) -> CliError {
let code = match report.failure_kind() {
Some(DownloadErrorKind::Integrity) => "install_integrity",
Some(DownloadErrorKind::Io) => "install_io",
_ => "install_network",
};
let mut message = format!(
"{} 个文件没下成(共 {} 个)",
report.failed(),
report.files.len()
);
for outcome in report.failures(3) {
if let Some(failure) = &outcome.failure {
message.push_str(&format!(";{}:{}", outcome.name, failure.message));
}
}
CliError::user(code, message).with_hint(format!(
"重跑 `%prog% install` 会续传并跳过已就绪文件;游戏根 {}",
ctx.game().root().display()
))
}