fix(core): launch --dry-run 不再落盘(natives 只预览,零建目录/零写文件)

`ull launch --dry-run` 在「环境准备」段走的是真解压:即使只打印命令行,也会在
`versions/<id>/natives/` 下建目录、解 dll、写戳记,把别人的版本树改脏(父目录
mtime 也变)。失败路径(缺 jar 报 missing_components)照样留下这些副作用。

- natives 新增 `preview()`:与 `extract()` 共用条目筛选与戳记口径,但零落盘
  (无 create_dir_all / clear_natives / write_stamp),只清点「将写下几个原生库」;
  jar 打不开照样报错,预览不藏坏包。
- `NativesReport` 增 `planned` / `dir_exists`:预览报告自报家门,人读行与 JSON
  说清「未落盘」「目录尚不存在」。
- `Launcher::dry_run` 改走 `preview_environment`,`spawn` 仍走 `prepare_environment`。
- 报告类型拆到 `natives/report.rs`(mod.rs 曾破 400 行红线)。
- 回归测试(natives_dry_run_cases.rs):有 jar / 无 jar / 空 jar / 已解压过四条
  零落盘判据,用「节点类型 + 纳秒 mtime」快照逐字比较版本树,并带一条判据自检
  (瞬时建删目录必须被快照看见,防假绿)。
- help:`--dry-run` 的承诺补成「不写盘、不解压 natives、不建目录」。

证据(外部版本树 versions/1.16.5 + 真 natives jar):
修复前 dry-run 后 versions/1.16.5 mtime 1794342830.6861426 → 1794342832.4544542,
新增 natives/ + lwjgl.dll + OpenAL.dll + .ull-natives.json;修复后同一场景快照逐字
不变(mtime 1794343730.6618167),JSON natives.planned=true / dir_exists=false。
This commit is contained in:
AskaEth committed 2026-11-11 05:18:54 +08:00
1 parent 774a547aa0
commit 1fa30c4885
11 files changed
+583 -239

No files matched your search

+1 -104
View File
@@ -2,44 +2,12 @@
//!
//! 假 jar 用 `zip` 的 Stored 模式现造(不依赖真下载),真机验收另有人工步骤。
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipWriter};
use std::path::Path;
use unlucky_core::launch::natives::{self, NativeJar, NativesEra, NativesPlan, STAMP_FILE};
use crate::prelude::*;
/// 造一个 natives jar:`entries` 是(条目名, 内容)。
fn make_jar(path: &Path, entries: &[(&str, &[u8])]) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("建 jar 目录");
}
let file = File::create(path).expect("建 jar");
let mut writer = ZipWriter::new(file);
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
for (name, bytes) in entries {
writer.start_file(*name, options).expect("写条目");
writer.write_all(bytes).expect("写条目内容");
}
writer.finish().expect("收尾 jar");
}
/// 一份 1.16.5 风格的 natives jar 的条目集(原生库 + 该被排除的噪音)。
fn lwjgl_jar_entries() -> Vec<(&'static str, &'static [u8])> {
vec![
("lwjgl.dll", b"DLL-BYTES".as_slice()),
("OpenAL.dll", b"OPENAL-BYTES".as_slice()),
("META-INF/MANIFEST.MF", b"manifest".as_slice()),
("META-INF/LWJGL.SF", b"signature".as_slice()),
("lwjgl.dll.sha1", b"deadbeef".as_slice()),
("README.txt", b"noise".as_slice()),
]
}
fn plan(jars: Vec<NativeJar>) -> NativesPlan {
NativesPlan::new(jars)
}
@@ -208,74 +176,3 @@ fn natives_extracted_event_carries_report_fields() {
.expect("dir 是字符串")
.ends_with("natives"));
}
/// `--dry-run` 也做环境准备:第二次跑要看到 up_to_date(真机验收的判定依据)。
#[test]
fn dry_run_prepares_natives_and_is_idempotent() {
let root = temp_dir("natives-dry-run");
let jar = root.join("natives-windows.jar");
make_jar(&jar, &[("lwjgl.dll", b"DLL".as_slice())]);
let natives_dir: PathBuf = root.join("versions/1.16.5/natives");
let (mut request, _) = legacy_forge_request();
request.dirs.natives_dir = natives_dir.clone();
request.natives = NativesPlan::new(vec![NativeJar::new(&jar, "lwjgl")]);
request.manifest.legacy_arguments = Some("--username ${auth_player_name}".to_string());
let sessions = temp_sessions_root("natives-dry-run-sessions");
let launcher = Launcher::new(LauncherConfig::new(&sessions));
let first = launcher.dry_run(&request).expect("首次 dry-run");
let report = first.natives.expect("应有 natives 报告");
assert_eq!(report.files, 1, "{report:?}");
assert!(!report.up_to_date);
assert!(natives_dir.join("lwjgl.dll").is_file());
let second = launcher.dry_run(&request).expect("二次 dry-run");
let report = second.natives.expect("应有 natives 报告");
assert!(report.up_to_date, "第二次应命中戳记:{report:?}");
assert_eq!(report.skipped, 1);
}
/// 缺 natives jar:适配层当成组件缺失(组装前显式失败),而不是等到 UnsatisfiedLinkError。
#[test]
fn missing_native_jar_becomes_missing_component() {
let (mut request, ctx) = legacy_forge_request();
request.natives = NativesPlan::new(vec![NativeJar::new(
"C:/mc/libraries/missing-natives-windows.jar",
"org.lwjgl:lwjgl:3.2.2:natives-windows",
)]);
// 直接验证适配层:给一个不存在的 natives jar,严格模式应产出 MissingNativeJar 告警。
let libraries: Vec<LibraryInput> = request
.classpath
.iter()
.map(|path| LibraryInput::new(path.clone(), "x"))
.collect();
let natives = request.natives.jars.clone();
let snapshot = SnapshotInput {
version_id: "1.16.5",
main_class: Some("net.minecraft.client.main.Main"),
libraries: &libraries,
natives: &natives,
..SnapshotInput::minimal("1.16.5", "net.minecraft.client.main.Main")
};
let options = AdapterOptions {
java: JavaSelection::new("C:/java/bin/java.exe", 17, 64),
..AdapterOptions::strict()
};
let output = build_request(&snapshot, options);
assert!(
output
.warnings
.iter()
.any(|warning| matches!(warning, AdapterWarning::MissingNativeJar { .. })),
"应有 MissingNativeJar 告警:{:?}",
output.warnings
);
assert_eq!(output.request.natives.era, NativesEra::Legacy);
// 组装按既有语义直接失败(missing_components 非空)。
assert!(matches!(
assemble(&output.request, &ctx).err(),
Some(LaunchError::Assembly(AssemblyError::MissingComponents(_)))
));
}