Unlucky Launcher v0.2.1
Release / 选槽位(默认两个 Windows) (push) Canceled after 0s
Release / build (${{ matrix.slot }}) (push) Canceled after 0s
Release / GitHub Release (push) Canceled after 0s

Single-commit source snapshot of the v0.2.1 release.
This commit is contained in:
AskaEth committed 2026-11-06 12:35:56 +08:00
commit d517f28140
1092 files changed
+211068

No files matched your search

@@ -0,0 +1,181 @@
//! 加载器的 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()
}