fix(coexist): r2 返工——帮助写真实例路径 + 加载器安装也说出「原版清单被替换」+ --json 给出绑定成败(M216)
r2 复审两条 P2 必修 + 一条「能一行接上就接」的 P3。
P2-1(`ull help pack` 的落点少一层 `.ULL/data`)
- 「数据根」在这份帮助自己的定义里是「含 `.ULL/` 与 `.minecraft/` 的父目录」(`--data-dir` 那行),
所以真路径是 `<数据根>/.ULL/data/instances/<实例目录名>`。r1 写成 `<数据根>/instances/<实例名>`,
照读会去一个不存在的位置找包内容。
- help.rs 与 model.rs 的字段文档改成真路径,并点明目录名是归一化后的结果(`Better-MC`)。
- 用例改成 `help_describes_the_new_default_target_with_the_real_path`:先断言真路径整串在、
再断言 `<数据根>/instances/` **不在**(这一条就是抓「少写 .ULL/data」那种错法)。
P2-2(装加载器时原版那一步的「替换了清单」没人说)
- `loader.rs`:`plan.vanilla_install_needed` 那条路以前只拿 `vanilla_report` 判 `is_complete()`
就丢掉;现在把它留在 `vanilla_report` 里传给 `report_loader::build`。
- `report_loader.rs`:`build` / `human_lines` 各多一个 `vanilla: Option<&InstallReport>`;
新增 `vanilla_lines`(人读:`告警(原版): …` + `原版版本清单已替换(旧的那份备份在 …)`)与
`vanilla_json`(`--json` 的 `vanilla` 段:warnings / version_json / manifest{kind,backup})。
- 用例 `a_replaced_vanilla_manifest_is_said_out_loud_in_the_loader_output`:用真
`LoaderInstallPlan`(最小 Fabric,零网络)调 `human_lines`,断言告警与备份路径都在;
未先装原版时不多说话;`vanilla_json` 的 `kind`/`backup` 结构正确、`None` 时没有该段。
P3(复审「能一行接上就接」):`--json` 给出导入绑定的**结构化成败**
- core `pack_dir::bind_created_instance` 返回 `Option<Result<String, String>>`(None=不需要绑 /
Ok=已绑 / Err=绑失败),一句话就能分出成败,不必猜文案。
- CLI:`outcome` 多写一个 `"instance_bound": true|false`(人读行照旧给原因);
GUI:`import.rs` 按 Ok/Err 分别记 `instance_bound` / `instance_binding_failed` 注记。
- 端到端用例里补断言 `data.instance_bound == true`。
This commit is contained in:
1 parent
4e1323212b
commit
aa61565fe7
8 files changed
+188
-27
No files matched your search
@@ -104,8 +104,9 @@ pub fn topic(name: &str) -> Option<&'static str> {
|
||||
MultiMC mmc-pack.json(formatVersion 必须为 1)+ instance.cfg + .minecraft/
|
||||
(Prism 实例;非加载器 component 会逐项列进报告,不静默丢)
|
||||
|
||||
目标目录缺省 = 新实例目录 `<数据根>/instances/<实例名>`(并登记实例,M216 起不再落共享
|
||||
.minecraft 根);`--target` 可钉到别处(例如某个实例的运行目录)。
|
||||
目标目录缺省 = 新实例目录 `<数据根>/.ULL/data/instances/<实例目录名>`(并登记实例,M216 起
|
||||
不再落共享 .minecraft 根;目录名是包名/`--name` 经 ASCII 归一化后的结果,如 `Better-MC`);
|
||||
`--target` 可钉到别处(例如某个实例的运行目录)。
|
||||
两阶段落盘:先 overrides(三方合并:包内原值 / 磁盘现状 / 上次基线),再补远端文件。
|
||||
· 用户改过的文件不会被静默覆盖(默认保留,逐条进冲突清单);`--overwrite` 才用包覆盖
|
||||
· 缺文件(断网 / CurseForge files[] 补不出来)可容忍,但结果降级为「部分成功」
|
||||
|
||||
@@ -157,8 +157,8 @@ pub struct InstallArgs {
|
||||
pub struct PackImportArgs {
|
||||
/// 整合包路径:`.zip` / `.mrpack` / MultiMC 实例目录。
|
||||
pub path: PathBuf,
|
||||
/// 目标实例目录(缺省 = 新实例目录 `<数据根>/instances/<实例名>`,M216 前是共享游戏目录
|
||||
/// `game_home`)。
|
||||
/// 目标实例目录(缺省 = 新实例目录 `<数据根>/.ULL/data/instances/<实例目录名>`,M216 前是
|
||||
/// 共享游戏目录 `game_home`)。
|
||||
pub target: Option<PathBuf>,
|
||||
/// 实例显示名(缺省 = 包名)。
|
||||
pub name: Option<String>,
|
||||
|
||||
@@ -85,12 +85,13 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
|
||||
skipped: false,
|
||||
});
|
||||
return Ok(report_loader::build(
|
||||
ctx, &engine, now, true, &plan, None, None, &name, section,
|
||||
ctx, &engine, now, true, &plan, None, None, None, &name, section,
|
||||
));
|
||||
}
|
||||
|
||||
let mut vanilla_report = None;
|
||||
if plan.vanilla_install_needed {
|
||||
let (_, vanilla_report) = vanilla::plan_and_install(
|
||||
let (_, report) = vanilla::plan_and_install(
|
||||
ctx,
|
||||
&engine,
|
||||
&version,
|
||||
@@ -98,11 +99,14 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
|
||||
args.progress,
|
||||
args.json_progress,
|
||||
)?;
|
||||
if let Some(report) = &vanilla_report {
|
||||
if let Some(report) = &report {
|
||||
if !report.is_complete() {
|
||||
return Err(failure_error(report, ctx));
|
||||
}
|
||||
}
|
||||
// **M216 r2 P2-2**:原版那段 report 不能在这里丢掉——它的 `warnings` / `manifest`
|
||||
// (可能替换了共享 `.minecraft` 里 `versions/<原版>/<原版>.json`)必须进人读与 `--json`。
|
||||
vanilla_report = report;
|
||||
}
|
||||
|
||||
// installer processors 段:版本 JSON 已在盘上 = 上一轮早已跑完,整段跳过。
|
||||
@@ -143,6 +147,7 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
|
||||
false,
|
||||
&plan,
|
||||
Some(&install_report),
|
||||
vanilla_report.as_ref(),
|
||||
Some(®istration),
|
||||
&name,
|
||||
section,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! 与 [`super::report`] 的分工:那边是"原版安装"(客户端 jar + 资源 + 库),这边是
|
||||
//! "叠在原版上的 loader 层"(只有库)。共用同一套进度回调与字节格式化。
|
||||
|
||||
use serde_json::json;
|
||||
use serde_json::{json, Value};
|
||||
use unlucky_core::addon::loader::processors::{ProcessorPlan, ProcessorRun};
|
||||
use unlucky_core::addon::loader::LoaderInstallPlan;
|
||||
use unlucky_core::download::DownloadEngine;
|
||||
@@ -39,6 +39,7 @@ pub fn build(
|
||||
dry_run: bool,
|
||||
plan: &LoaderInstallPlan,
|
||||
report: Option<&InstallReport>,
|
||||
vanilla: Option<&InstallReport>,
|
||||
registration: Option<&Registration>,
|
||||
name: &str,
|
||||
processors: Option<ProcessorSection<'_>>,
|
||||
@@ -78,6 +79,9 @@ pub fn build(
|
||||
if let Some(section) = &processors {
|
||||
data["processors"] = processor_json(section);
|
||||
}
|
||||
if let Some(block) = vanilla_json(vanilla) {
|
||||
data["vanilla"] = block;
|
||||
}
|
||||
|
||||
Report::new(
|
||||
data,
|
||||
@@ -85,6 +89,7 @@ pub fn build(
|
||||
dry_run,
|
||||
plan,
|
||||
report,
|
||||
vanilla,
|
||||
registration,
|
||||
&samples,
|
||||
name,
|
||||
@@ -115,10 +120,47 @@ fn processor_json(section: &ProcessorSection<'_>) -> serde_json::Value {
|
||||
node
|
||||
}
|
||||
|
||||
/// `--json` 的 `vanilla` 段:先装原版那一步的告警 + 清单结局(`None` = 这次没先装原版)。
|
||||
///
|
||||
/// **M216 r2 P2-2**:原版 report 在 `is_complete()` 之后就被丢了 ⇒ 「替换了共享 `.minecraft`
|
||||
/// 里的原版清单」在加载器安装的 JSON 里没有痕迹。
|
||||
fn vanilla_json(vanilla: Option<&InstallReport>) -> Option<Value> {
|
||||
let report = vanilla?;
|
||||
Some(json!({
|
||||
"warnings": report.warnings,
|
||||
"version_json": report.version_json.as_ref().map(|path| path.display().to_string()),
|
||||
"manifest": report.manifest.as_ref().map(|write| json!({
|
||||
"kind": write.as_str(),
|
||||
"backup": write.backup().map(|path| path.display().to_string()),
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 人读行里「先装原版」那一段(**M216 r2 P2-2**,与 [`vanilla_json`] 同源):以前
|
||||
/// `human_lines` 只遍历 `plan.warnings()`,原版那份 report 的告警与「清单被替换」都没说。
|
||||
fn vanilla_lines(vanilla: Option<&InstallReport>) -> Vec<String> {
|
||||
let Some(report) = vanilla else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut lines: Vec<String> = report
|
||||
.warnings
|
||||
.iter()
|
||||
.map(|warning| format!("告警(原版): {warning}"))
|
||||
.collect();
|
||||
if let Some(backup) = report.manifest.as_ref().and_then(|write| write.backup()) {
|
||||
lines.push(format!(
|
||||
"原版版本清单已替换(旧的那份备份在 {})",
|
||||
backup.display()
|
||||
));
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn human_lines(
|
||||
dry_run: bool,
|
||||
plan: &LoaderInstallPlan,
|
||||
report: Option<&InstallReport>,
|
||||
vanilla: Option<&InstallReport>,
|
||||
registration: Option<&Registration>,
|
||||
samples: &[serde_json::Value],
|
||||
name: &str,
|
||||
@@ -181,6 +223,7 @@ fn human_lines(
|
||||
for warning in plan.warnings() {
|
||||
human.push(format!("告警: {warning}"));
|
||||
}
|
||||
human.extend(vanilla_lines(vanilla));
|
||||
if let Some(section) = processors {
|
||||
human.extend(processor_lines(section));
|
||||
}
|
||||
@@ -272,3 +315,85 @@ fn processor_lines(section: &ProcessorSection<'_>) -> Vec<String> {
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
use unlucky_core::addon::loader::{
|
||||
LoaderInstallPlan, LoaderKind, LoaderPlanInput, LoaderRelease, LoaderSupport,
|
||||
};
|
||||
use unlucky_core::game::arguments::Environment;
|
||||
use unlucky_core::game::install::ManifestWrite;
|
||||
use unlucky_core::game::layout::GameLayout;
|
||||
use unlucky_core::game::manifest::VersionJson;
|
||||
|
||||
/// 一份最小的 Fabric 加载器计划(零网络:只算路径,不落盘)。
|
||||
fn loader_plan(root: &std::path::Path) -> LoaderInstallPlan {
|
||||
let layout = GameLayout::new(root);
|
||||
let release = LoaderRelease {
|
||||
kind: LoaderKind::Fabric,
|
||||
mc_version: "1.20.1".to_string(),
|
||||
loader_version: "0.15.11".to_string(),
|
||||
version_id: "1.20.1-fabric-0.15.11".to_string(),
|
||||
version_json: VersionJson::from_json_str(
|
||||
r#"{"id":"1.20.1-fabric-0.15.11","mainClass":"net.fabricmc.loader.impl.launch.knot.KnotClient","inheritsFrom":"1.20.1","libraries":[]}"#,
|
||||
)
|
||||
.expect("loader 清单应可解析"),
|
||||
support: LoaderSupport::JsonRoute,
|
||||
installer: None,
|
||||
install_profile: None,
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &layout,
|
||||
release: &release,
|
||||
environment: &Environment::host(),
|
||||
vanilla_installed: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// **M216 r2 P2-2**:装加载器时「先装原版」那一步替换了共享 `.minecraft` 里的原版清单,
|
||||
/// 这句话必须出现在**加载器安装**的人读输出里(此前只出现在纯原版安装的输出里)。
|
||||
///
|
||||
/// 抓得住:`human_lines` 只遍历 `plan.warnings()`(= r1 状态);备份路径没渲出来;
|
||||
/// `--json` 的 `vanilla` 段没了。抓不住:真实 `ull install --loader …` 整条链路(要网络),
|
||||
/// 以及「原版 report 是否真被 `loader.rs` 传进来」那步接线(见报告)。
|
||||
#[test]
|
||||
fn a_replaced_vanilla_manifest_is_said_out_loud_in_the_loader_output() {
|
||||
let root = std::env::temp_dir().join(format!("ull216-loader-{}", std::process::id()));
|
||||
let plan = loader_plan(&root);
|
||||
let backup = PathBuf::from("/g/.minecraft/versions/1.20.1/1.20.1.json.ullbak-99");
|
||||
let vanilla = InstallReport {
|
||||
files: Vec::new(),
|
||||
version_json: Some(PathBuf::from("/g/.minecraft/versions/1.20.1/1.20.1.json")),
|
||||
manifest: Some(ManifestWrite::Replaced {
|
||||
backup: backup.clone(),
|
||||
}),
|
||||
warnings: vec!["已替换版本清单 /g/.minecraft/versions/1.20.1/1.20.1.json".to_string()],
|
||||
};
|
||||
|
||||
let text = human_lines(false, &plan, None, Some(&vanilla), None, &[], "n", None).join("\n");
|
||||
assert!(
|
||||
text.contains("已替换版本清单"),
|
||||
"原版那段的告警没进人读输出:{text}"
|
||||
);
|
||||
assert!(
|
||||
text.contains(&backup.display().to_string()),
|
||||
"原版清单的备份路径没进人读输出:{text}"
|
||||
);
|
||||
// 没先装原版时不许凭空多说话。
|
||||
let plain = human_lines(false, &plan, None, None, None, &[], "n", None).join("\n");
|
||||
assert!(!plain.contains("原版版本清单已替换"), "{plain}");
|
||||
|
||||
// `--json` 的 `vanilla` 段(结构化,脚本可读)。
|
||||
let block = vanilla_json(Some(&vanilla)).expect("应当有 vanilla 段");
|
||||
assert_eq!(block["manifest"]["kind"], Value::String("replaced".into()));
|
||||
assert_eq!(
|
||||
block["manifest"]["backup"].as_str(),
|
||||
Some(backup.to_string_lossy().as_ref())
|
||||
);
|
||||
assert!(vanilla_json(None).is_none());
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ pub fn run(ctx: &Ctx, args: &PackImportArgs) -> Result<Outcome, CliError> {
|
||||
&target,
|
||||
&seen,
|
||||
args,
|
||||
binding.as_deref(),
|
||||
binding.as_ref(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ fn outcome(
|
||||
target: &Path,
|
||||
stages: &[&'static str],
|
||||
args: &PackImportArgs,
|
||||
binding: Option<&str>,
|
||||
binding: Option<&Result<String, String>>,
|
||||
) -> Outcome {
|
||||
let mut data = serde_json::to_value(report).unwrap_or_else(|_| {
|
||||
json!({
|
||||
@@ -144,8 +144,14 @@ fn outcome(
|
||||
if !stages.is_empty() {
|
||||
human.push(format!("阶段:{}", stages.join(" → ")));
|
||||
}
|
||||
if let Some(note) = binding {
|
||||
human.push(note.to_string());
|
||||
if let Some(binding) = binding {
|
||||
// **M216 r2**:`--json` 里给**结构化成败**(r1 只印人读行,脚本看不到成败)。
|
||||
if let Some(object) = data.as_object_mut() {
|
||||
object.insert("instance_bound".to_string(), json!(binding.is_ok()));
|
||||
}
|
||||
human.push(match binding {
|
||||
Ok(note) | Err(note) => note.clone(),
|
||||
});
|
||||
}
|
||||
human.push(format!(
|
||||
"结果:{} 文件 新{} / 覆盖{} / 跳过{} / 保留{} 冲突 {} 缺文件 {}",
|
||||
|
||||
@@ -221,6 +221,14 @@ fn the_default_target_is_a_new_instance_dir_and_the_instance_is_bound_to_it() {
|
||||
]);
|
||||
assert_eq!(code, 0, "stdout={stdout} stderr={stderr}");
|
||||
|
||||
// **M216 r2**:绑定成败也进 `--json`(结构化,不必猜文案)。
|
||||
let value = parse_json(&stdout);
|
||||
assert_eq!(
|
||||
value["data"]["instance_bound"],
|
||||
Value::Bool(true),
|
||||
"缺省导入应当把实例绑到内容目录并报 true:{stdout}"
|
||||
);
|
||||
|
||||
let instance_dir = data.path().join(".ULL/data/instances/My-Pack");
|
||||
assert!(
|
||||
instance_dir.join("mods/a.jar").is_file(),
|
||||
@@ -266,18 +274,27 @@ fn the_default_target_is_a_new_instance_dir_and_the_instance_is_bound_to_it() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **M216 P2-1**:`--help` 里的缺省落点必须与新行为一致(写旧行为 = 用户照读会以为文件进了
|
||||
/// `.minecraft`)。
|
||||
/// **M216 P2-1 / r2**:`--help` 里的缺省落点必须是**真路径**。
|
||||
///
|
||||
/// 抓得住:文案退回「共享游戏目录」;新落点没写进去。抓不住:`model.rs` 的字段文档
|
||||
/// (那是编译期文档,测试读不到)——所以断言落在这里只覆盖用户能看见的那一份。
|
||||
/// 「数据根」在这份帮助自己的定义里是「含 `.ULL/` 与 `.minecraft/` 的父目录」(`--data-dir` 那行),
|
||||
/// 所以实例目录的真路径是 `<数据根>/.ULL/data/instances/<目录名>`。r1 写成了
|
||||
/// `<数据根>/instances/<实例名>` —— 少一层 `.ULL/data`,照读会去一个不存在的位置找包内容。
|
||||
///
|
||||
/// 抓得住:缺 `.ULL/data` 这一层(下面第二条断言就是抓它的——少写时该子串恰好出现);
|
||||
/// 文案退回「共享游戏目录」;目录名归一化这件事没提。抓不住:`model.rs` 的字段文档
|
||||
/// (编译期文档,测试读不到),以及真实 `--data-dir` 下的绝对路径拼接(那由上面那条端到端
|
||||
/// 用例按真目录断言)。
|
||||
#[test]
|
||||
fn help_describes_the_new_default_target() {
|
||||
fn help_describes_the_new_default_target_with_the_real_path() {
|
||||
let (code, stdout, _) = run_cli(&["help", "pack"]);
|
||||
assert_eq!(code, 0);
|
||||
assert!(
|
||||
stdout.contains("instances/<实例名>"),
|
||||
"帮助里没写新缺省落点:{stdout}"
|
||||
stdout.contains("<数据根>/.ULL/data/instances/<实例目录名>"),
|
||||
"帮助里没写真路径 <数据根>/.ULL/data/instances/<实例目录名>:{stdout}"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("<数据根>/instances/"),
|
||||
"帮助里的落点少了 `.ULL/data` 这一层(r2 抓的就是这个错法):{stdout}"
|
||||
);
|
||||
assert!(
|
||||
!stdout.contains("缺省 = 数据根的共享游戏目录"),
|
||||
|
||||
Reference in new issue
Block a user