Files
Unlucky-Launcher/crates/cli/src/commands/install/loader_processors.rs
T
AskaEth d517f28140
Release / 选槽位(默认两个 Windows) (push) Canceled after 0s
Release / build (${{ matrix.slot }}) (push) Canceled after 0s
Release / GitHub Release (push) Canceled after 0s
Unlucky Launcher v0.2.1
Single-commit source snapshot of the v0.2.1 release.
2026-11-06 12:35:56 +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, report, 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 = report::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()
}