Files
Unlucky-Launcher/crates/cli/src/commands/install/loader_processors.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

182 lines
6.2 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.
//! 加载器的 installer processors 段(M35):依赖库下载 → 选 Java → 串行执行。
//!
//! 只在 `LoaderSupport::RequiresInstaller` 的版本上出现(Forge 1.13~1.20.x 主流段、
//! NeoForge 1.20.5+)。其余加载器(Fabric/Quilt、Forge spec 0)走纯 json 路线,这一段整块跳过。
//!
//! ```text
//! ProcessorPlan::build(core,纯计算:变量表 + 每步命令行 + 依赖库清单)
//! → download_libraries(M27 引擎:镜像/健康度/退避/校验)
//! → select_java(M33 解析链:托管 > JAVA_HOME > PATH > 扫盘)
//! → run(串行 java -cp,产物存在性硬校验、摘要软校验)
//! ```
//!
//! 顺序上与版本 JSON 的关系是**硬约束**:processors 全部成功之后才让 M27 的 `execute`
//! 写 `versions/<id>/<id>.json`——那份清单是"这版装完了"的判据,抢在 processors 前写会
//! 留下一个启动必崩却被报成"已安装"的实例。
use std::sync::Arc;
use unlucky_core::addon::loader::processors::{
self, ProcessorError, ProcessorPlan, ProcessorPlanInput, ProcessorRun, StepOutcome, StepSink,
};
use unlucky_core::addon::loader::LoaderRelease;
use unlucky_core::download::blocking::block_on;
use unlucky_core::download::DownloadEngine;
use unlucky_core::game::arguments::Environment;
use unlucky_core::game::install::InstallReport;
use super::{failure_error, progress, CONCURRENCY};
use crate::cli::InstallArgs;
use crate::context::Ctx;
use crate::error::CliError;
use crate::output::progress_line;
/// 组执行计划;不需要跑 installer 的加载器返回 `None`。
///
/// # Errors
///
/// install profile 的 `data` 表/`processors` 形状不对时返回 `processor_plan` 错误。
pub(super) fn plan(ctx: &Ctx, release: &LoaderRelease) -> Result<Option<ProcessorPlan>, CliError> {
if !release.requires_installer() {
return Ok(None);
}
let (Some(profile), Some(installer)) = (
release.install_profile.as_ref(),
release.installer.as_deref(),
) else {
return Err(CliError::user(
"loader_metadata",
format!(
"{} {} 需要跑 installer processors,但没拿到 install profile / installer 路径",
release.kind.label(),
release.loader_version
),
)
.with_hint("重跑一次(元数据缓存可能不完整)"));
};
let game = ctx.game();
let scratch = ctx
.layout()
.cache()
.join("processors")
.join(safe_name(&release.version_id));
let plan = ProcessorPlan::build(ProcessorPlanInput {
kind: release.kind,
mc_version: &release.mc_version,
version_id: &release.version_id,
profile,
layout: &game,
installer,
scratch: &scratch,
environment: &Environment::host(),
})
.map_err(map_processor_error)?;
Ok(Some(plan))
}
/// 下载依赖库 + 执行 processors。
///
/// # Errors
///
/// 依赖库下载失败(稳定码沿用 `install_*`)、找不到可用 Java、processor 非零退出、
/// 产物没落盘。
pub(super) fn execute(
ctx: &Ctx,
engine: &DownloadEngine,
plan: &ProcessorPlan,
args: &InstallArgs,
) -> Result<(InstallReport, ProcessorRun), CliError> {
let sink = progress::progress_sink(args.progress, args.json_progress, plan.libraries.len());
let libraries = block_on(processors::download_libraries(
engine,
&plan.libraries,
CONCURRENCY,
sink,
));
if !libraries.is_complete() {
return Err(failure_error(&libraries, ctx));
}
let managed = ctx.layout().java_managed();
let Some(java) = processors::select_java(&managed, &plan.java_requirement) else {
return Err(CliError::user(
"processor_java_missing",
format!(
"跑 installer processors 需要 {},但一个可用的 Java 都没找到",
plan.java_requirement.describe()
),
)
.with_hint(format!(
"`%prog% java install {}` 装一个托管 JDK,或把 JAVA_HOME 指向可用的 JDK(托管目录 {})",
plan.java_requirement.minimum,
managed.display()
)));
};
let sink = step_sink(args.progress, args.json_progress, plan.step_count());
let run = processors::run(plan, &java, sink).map_err(map_processor_error)?;
Ok((libraries, run))
}
/// 每步一行的实时进度(只写 stderr)。
fn step_sink(enabled: bool, json_mode: bool, total: usize) -> Option<StepSink> {
if !enabled {
return None;
}
Some(Arc::new(move |outcome: &StepOutcome| {
let record = serde_json::json!({
"event": "processor",
"index": outcome.index,
"total": total,
"jar": outcome.label,
"status": outcome.status.as_str(),
"elapsed_ms": outcome.elapsed_ms,
});
let text = format!(
"processor {}/{} {} → {}(输出 {} 个{})",
outcome.index,
total,
outcome.label,
if outcome.status.as_str() == "skipped" {
"跳过(产物已就绪)"
} else {
"执行完成"
},
outcome.outputs,
if outcome.elapsed_ms > 0 {
format!(",{} ms", outcome.elapsed_ms)
} else {
String::new()
}
);
if json_mode {
progress_line(&record.to_string());
} else {
progress_line(&text);
}
}))
}
/// processor 执行错误 → CLI 错误(稳定码 `processor_*`)。
fn map_processor_error(error: ProcessorError) -> CliError {
let cli = CliError::user(error.kind().as_str(), error.message().to_string());
match error.hint() {
Some(hint) => cli.with_hint(hint.to_string()),
None => cli,
}
}
/// 版本 id → 可做目录名的片段(`+` 与路径分隔符都换掉)。
fn safe_name(raw: &str) -> String {
raw.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_') {
ch
} else {
'_'
}
})
.collect::<String>()
.trim_end_matches('.')
.to_string()
}