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:
1 parent
774a547aa0
commit
1fa30c4885
11 files changed
+583
-239
No files matched your search
@@ -11,7 +11,7 @@
|
||||
//!
|
||||
//! 阶段 B 说明:diagnose(M15)尚未合并,诊断走 [`DiagnosisHook`] 接缝,默认 [`NoopDiagnosis`]。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::launch::args_table::{assemble, AssembledCommand};
|
||||
use crate::launch::diagnosis_hook::{DiagnosisHook, NoopDiagnosis};
|
||||
use crate::launch::error::{AssemblyError, Result};
|
||||
use crate::launch::launch_session::{LaunchSession, SessionHandle};
|
||||
use crate::launch::natives::{self, NativesReport};
|
||||
use crate::launch::natives::{self, NativesError, NativesPlan, NativesReport};
|
||||
use crate::launch::platform::PlatformCtx;
|
||||
use crate::launch::process::{ProcessConfig, RunningProcess};
|
||||
use crate::launch::redact::Redactor;
|
||||
@@ -91,10 +91,11 @@ impl LauncherConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// `--dry-run` 的输出(D18):完整命令行,但**不 spawn、不写会话文件**。
|
||||
/// `--dry-run` 的输出(D18):完整命令行,但**不 spawn、不写会话文件、不落盘**。
|
||||
///
|
||||
/// 会做的事与真实启动**一致到「环境准备」为止**:natives 解压照做(幂等,`natives` 字段
|
||||
/// 回报结果),因为「装完能不能起」的一半答案就在「native 库有没有落地」上。
|
||||
/// 环境准备(第 6 段)在 dry-run 下走 [`crate::launch::natives::preview`]:只算「会解出
|
||||
/// 几个原生库 / natives 目录是不是已经就绪」,**不建目录、不写文件、不删文件**——
|
||||
/// `--dry-run` 对版本树(尤其是别人铺的 `versions/<id>/`)是纯读的。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DryRun {
|
||||
/// 实际要执行的程序。
|
||||
@@ -109,7 +110,9 @@ pub struct DryRun {
|
||||
pub classpath_entries: usize,
|
||||
/// 日志解码编码。
|
||||
pub encoding: String,
|
||||
/// natives 解压结果(没有 natives 目录、即请求未配置时为 `None`)。
|
||||
/// natives 预览结果(没有 natives 目录、即请求未配置时为 `None`)。
|
||||
///
|
||||
/// `planned == true`:`files` 是「真启动会写下」的数量,盘上零改动。
|
||||
pub natives: Option<NativesReport>,
|
||||
}
|
||||
|
||||
@@ -161,15 +164,32 @@ impl Launcher {
|
||||
assemble(request, &self.config.platform)
|
||||
}
|
||||
|
||||
/// 环境准备(管线第 6 段):natives 解压。
|
||||
/// 环境准备(管线第 6 段):natives 解压(**落盘**,真启动专用)。
|
||||
///
|
||||
/// 目标目录取 `request.dirs.natives_dir`(与 `${natives_directory}` 同一真相源);
|
||||
/// 目录为空视为调用方不需要,返回 `Ok(None)`。
|
||||
fn prepare_environment(&self, request: &LaunchRequest) -> Result<Option<NativesReport>> {
|
||||
self.environment(request, natives::extract)
|
||||
}
|
||||
|
||||
/// 环境准备的第 6 段预览版:只算「会解出什么」,**零落盘**(`--dry-run` 专用)。
|
||||
fn preview_environment(&self, request: &LaunchRequest) -> Result<Option<NativesReport>> {
|
||||
self.environment(request, natives::preview)
|
||||
}
|
||||
|
||||
/// 两个入口共用的外壳:空 `natives_dir` = 调用方不需要,失败统一折成组装错误。
|
||||
///
|
||||
/// `run` 是 [`natives::extract`](落盘)或 [`natives::preview`](零落盘);
|
||||
/// 返回值类型要写全 `std::result::Result`——本模块的 `Result` 别名固定带 `LaunchError`。
|
||||
fn environment(
|
||||
&self,
|
||||
request: &LaunchRequest,
|
||||
run: fn(&Path, &NativesPlan) -> std::result::Result<NativesReport, NativesError>,
|
||||
) -> Result<Option<NativesReport>> {
|
||||
if request.dirs.natives_dir.as_os_str().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
natives::extract(&request.dirs.natives_dir, &request.natives)
|
||||
run(&request.dirs.natives_dir, &request.natives)
|
||||
.map(Some)
|
||||
.map_err(|err| AssemblyError::NativesExtraction {
|
||||
message: err.message(),
|
||||
@@ -177,12 +197,13 @@ impl Launcher {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// D18:组装完整命令行并返回,**不 spawn 进程、不写会话文件**。
|
||||
/// D18:组装完整命令行并返回,**不 spawn 进程、不写会话文件、不落盘**。
|
||||
///
|
||||
/// 供 `unlucky-launcher --dry-run <instance>` 打印一行命令行后退出;
|
||||
/// natives 走 [`natives::preview`](不建目录、不写文件),所以对版本树是纯读的;
|
||||
/// accessToken 已脱敏(绝不把凭据打到终端/日志里)。
|
||||
pub fn dry_run(&self, request: &LaunchRequest) -> Result<DryRun> {
|
||||
let natives = self.prepare_environment(request)?;
|
||||
let natives = self.preview_environment(request)?;
|
||||
let command = self.plan(request)?;
|
||||
let redactor = self.redactor(request);
|
||||
let warnings = command
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//!
|
||||
//! # 公共面
|
||||
//!
|
||||
//! - [`Launcher`]:`dry_run`(D18,只组装命令行)、`spawn`/`launch`(完整管线);
|
||||
//! - [`Launcher`]:`dry_run`(D18,只组装命令行、零落盘)、`spawn`/`launch`(完整管线);
|
||||
//! - [`LaunchRequest`] 是命令行组装的**纯数据输入**,由 [`adapter`] 从 game 快照映射而来;
|
||||
//! - [`AssembledCommand`] 是组装结果(program + argv + classpath + 环境变量 + 告警);
|
||||
//! - 事件流([`session`])与索引([`session_index`])是 M15 诊断采集器与
|
||||
|
||||
@@ -68,26 +68,33 @@ pub(crate) fn io_error(
|
||||
}
|
||||
}
|
||||
|
||||
/// 解压单个 jar:排除前缀 → 原生库白名单 → zip-slip 防护 → 落盘。
|
||||
pub(crate) fn extract_jar(
|
||||
native: &NativeJar,
|
||||
dir: &Path,
|
||||
report: &mut NativesReport,
|
||||
) -> Result<(), NativesError> {
|
||||
/// 打开一个 natives jar(打不开 / 不是合法 zip 都以 [`NativesError`] 带路径报出)。
|
||||
fn open_archive(native: &NativeJar) -> Result<ZipArchive<File>, NativesError> {
|
||||
let file = File::open(&native.path)
|
||||
.map_err(|source| io_error("open_native_jar", &native.path, source))?;
|
||||
let mut archive = ZipArchive::new(file).map_err(|err| NativesError::Zip {
|
||||
ZipArchive::new(file).map_err(|err| NativesError::Zip {
|
||||
path: native.path.clone(),
|
||||
reason: err.to_string(),
|
||||
})?;
|
||||
})
|
||||
}
|
||||
|
||||
/// 该落盘的条目(zip 索引 + 相对路径):排除前缀 → 原生库白名单 → zip-slip 防护。
|
||||
///
|
||||
/// 只读遍历,不解压:`extract_jar` 拿它算落点,`count_jar` 拿它清点数量,
|
||||
/// 两边共用同一套筛选口径(预览与真解压不会给出不同的数字)。
|
||||
fn extractable_entries(
|
||||
archive: &mut ZipArchive<File>,
|
||||
native: &NativeJar,
|
||||
report: &mut NativesReport,
|
||||
) -> Result<Vec<(usize, PathBuf)>, NativesError> {
|
||||
let excludes: Vec<&str> = DEFAULT_EXCLUDES
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(native.exclude.iter().map(String::as_str))
|
||||
.collect();
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for index in 0..archive.len() {
|
||||
let mut entry = archive.by_index(index).map_err(|err| NativesError::Zip {
|
||||
let entry = archive.by_index(index).map_err(|err| NativesError::Zip {
|
||||
path: native.path.clone(),
|
||||
reason: err.to_string(),
|
||||
})?;
|
||||
@@ -104,6 +111,23 @@ pub(crate) fn extract_jar(
|
||||
report.ignored += 1;
|
||||
continue;
|
||||
}
|
||||
entries.push((index, relative));
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// 解压单个 jar:`extractable_entries` 选条目,再逐个落到 `dir` 下。
|
||||
pub(crate) fn extract_jar(
|
||||
native: &NativeJar,
|
||||
dir: &Path,
|
||||
report: &mut NativesReport,
|
||||
) -> Result<(), NativesError> {
|
||||
let mut archive = open_archive(native)?;
|
||||
for (index, relative) in extractable_entries(&mut archive, native, report)? {
|
||||
let mut entry = archive.by_index(index).map_err(|err| NativesError::Zip {
|
||||
path: native.path.clone(),
|
||||
reason: err.to_string(),
|
||||
})?;
|
||||
let dest = dir.join(&relative);
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
@@ -118,6 +142,20 @@ pub(crate) fn extract_jar(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 只读清点单个 jar:只算「真解压会写下几个原生库」,一个字节都不落盘。
|
||||
///
|
||||
/// 与 [`extract_jar`] 的差别只有「写」这一步;jar 打不开照样报错,所以预览不会把
|
||||
/// 真启动会遇到的坏包藏起来。
|
||||
pub(crate) fn count_jar(
|
||||
native: &NativeJar,
|
||||
report: &mut NativesReport,
|
||||
) -> Result<(), NativesError> {
|
||||
let mut archive = open_archive(native)?;
|
||||
let entries = extractable_entries(&mut archive, native, report)?;
|
||||
report.files += entries.len();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 该条目要不要解压:排除前缀命中、签名文件、非原生库扩展名 → 跳过。
|
||||
pub(crate) fn should_extract(name: &str, excludes: &[&str]) -> bool {
|
||||
if excludes.iter().any(|prefix| name.starts_with(prefix)) {
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
//!
|
||||
//! 这是 10 阶段管线里**第 6 段「环境准备」**的落盘部分(`PlanDoc/06` §1),发生顺序在
|
||||
//! 命令行组装之前(HMCL 也在组装期做:`DefaultLauncher.decompressNatives`,
|
||||
//! `analyse/03` §6.4-(7)),因此 `--dry-run` 也能验证「装完到底能不能起」。
|
||||
//! `analyse/03` §6.4-(7))。
|
||||
//!
|
||||
//! # 两条路径
|
||||
//!
|
||||
//! - [`extract`]:真启动走这条,把原生库落到 `java.library.path`;
|
||||
//! - [`preview`]:`--dry-run` 走这条,**零落盘**(不建目录、不写文件、不删文件),
|
||||
//! 只回答「真启动会解出几个文件 / 目录是不是已经就绪」。断言口径与 [`extract`]
|
||||
//! 共用(同一套条目筛选、同一份戳记),所以预览能当「装完能不能起」的依据,
|
||||
//! 又不会在别人的版本树里留下 `natives/` 目录或改掉父目录 mtime。
|
||||
//!
|
||||
//! # 三个时代(`SubAgentsTalk/launch-natives-解压与时代分界.md`,piston-meta 实测)
|
||||
//!
|
||||
@@ -24,6 +32,8 @@
|
||||
//! 混在同一个 `java.library.path` 里(HMCL 也是每次清目录,这里只把「没变就不动」补上)。
|
||||
|
||||
pub(crate) mod jar;
|
||||
/// 报告与错误类型(`mod.rs` 只留计划与两个入口)。
|
||||
pub(crate) mod report;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -164,115 +174,8 @@ impl NativesPlan {
|
||||
}
|
||||
}
|
||||
|
||||
/// 解压结果(进 `natives_extracted` 事件与 `--dry-run` 输出)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NativesReport {
|
||||
/// 目标目录。
|
||||
pub dir: PathBuf,
|
||||
/// 时代。
|
||||
pub era: NativesEra,
|
||||
/// 参与的 jar 数。
|
||||
pub jars: usize,
|
||||
/// 本次写盘的原生库文件数。
|
||||
pub files: usize,
|
||||
/// 按戳记跳过、未重新解压的文件数(= 上次解压的文件数)。
|
||||
pub skipped: usize,
|
||||
/// 被排除 / 非原生库而忽略的条目数(`META-INF/`、签名文件)。
|
||||
pub ignored: usize,
|
||||
/// 目录是否用户自定义。
|
||||
pub custom: bool,
|
||||
/// 是否按戳记判定「目录已就绪」而整体跳过。
|
||||
pub up_to_date: bool,
|
||||
}
|
||||
|
||||
impl NativesReport {
|
||||
/// 人读一行(CLI / 日志用)。
|
||||
pub fn summary(&self) -> String {
|
||||
if self.jars == 0 {
|
||||
return format!(
|
||||
"natives 目录已就绪(该版本无需解压,LWJGL 运行期自取)→ {}",
|
||||
self.dir.display()
|
||||
);
|
||||
}
|
||||
if self.up_to_date {
|
||||
return format!(
|
||||
"natives 已就绪({} 个原生库,{} 个 jar 未变)",
|
||||
self.skipped, self.jars
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"natives 解压 {} 个原生库({} 个 jar,忽略 {} 个条目)→ {}",
|
||||
self.files,
|
||||
self.jars,
|
||||
self.ignored,
|
||||
self.dir.display()
|
||||
)
|
||||
}
|
||||
|
||||
/// `--json` 的 `natives` 块(`ull launch --dry-run` 用;字段名是接口的一部分)。
|
||||
#[must_use]
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"dir": self.dir.display().to_string(),
|
||||
"files": self.files,
|
||||
"skipped": self.skipped,
|
||||
"ignored": self.ignored,
|
||||
"jars": self.jars,
|
||||
"custom": self.custom,
|
||||
"up_to_date": self.up_to_date,
|
||||
"era": self.era.as_str(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 解压失败原因。
|
||||
#[derive(Debug)]
|
||||
pub enum NativesError {
|
||||
/// 目标目录为空(调用方漏填 `natives_dir`)。
|
||||
NoDir,
|
||||
/// 文件系统操作失败。
|
||||
Io {
|
||||
/// 操作名。
|
||||
op: &'static str,
|
||||
/// 相关路径。
|
||||
path: PathBuf,
|
||||
/// 底层错误。
|
||||
source: std::io::Error,
|
||||
},
|
||||
/// natives jar 打不开 / 不是合法 zip(下载损坏的最常见形态)。
|
||||
Zip {
|
||||
/// jar 路径。
|
||||
path: PathBuf,
|
||||
/// 原因。
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl NativesError {
|
||||
/// 一行人读描述。
|
||||
pub fn message(&self) -> String {
|
||||
match self {
|
||||
Self::NoDir => "natives 目录未配置(natives_dir 为空)".to_string(),
|
||||
Self::Io { op, path, source } => {
|
||||
format!("{op} 失败({}):{source}", path.display())
|
||||
}
|
||||
Self::Zip { path, reason } => {
|
||||
format!(
|
||||
"natives jar 无法作为 zip 打开({}):{reason}",
|
||||
path.display()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NativesError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.message())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NativesError {}
|
||||
/// 解压结果 / 预览结果与失败原因单独成文件(`mod.rs` 只留计划与两个入口)。
|
||||
pub use report::{NativesError, NativesReport};
|
||||
|
||||
/// 从版本链库表收集 natives 分类器构件(adapter / app / CLI 共用这一段,别各写一份)。
|
||||
///
|
||||
@@ -326,6 +229,8 @@ pub fn collect(
|
||||
|
||||
/// 解压一个计划(幂等:目录与源 jar 都没变时零写盘)。
|
||||
///
|
||||
/// `--dry-run` 不走这里,走 [`preview`]:那里一个字节都不写。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 目录为空、建目录 / 写文件失败、jar 不是合法 zip 时返回 [`NativesError`]。
|
||||
@@ -342,10 +247,13 @@ pub fn extract(dir: &Path, plan: &NativesPlan) -> Result<NativesReport, NativesE
|
||||
ignored: 0,
|
||||
custom: plan.custom,
|
||||
up_to_date: false,
|
||||
planned: false,
|
||||
dir_exists: false,
|
||||
};
|
||||
// 1.19+ 没有 jar 也要建目录:LWJGL 3.3 往 SharedLibraryExtractPath 里解压。
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|source| jar::io_error("create_natives_dir", dir, source))?;
|
||||
report.dir_exists = true;
|
||||
if plan.jars.is_empty() {
|
||||
report.up_to_date = true;
|
||||
return Ok(report);
|
||||
@@ -368,3 +276,51 @@ pub fn extract(dir: &Path, plan: &NativesPlan) -> Result<NativesReport, NativesE
|
||||
write_stamp(dir, report.files, &stamps)?;
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
/// 预览一个计划:算出「真启动会怎么解压」,**不建目录、不写文件、不删文件**。
|
||||
///
|
||||
/// `--dry-run` 的唯一环境准备入口。与 [`extract`] 共用口径:
|
||||
/// 无 jar(1.19+)= 无需解压;戳记与当前 jar 集一致 = 已就绪;否则读 jar 清点
|
||||
/// 「将写下几个原生库」。区别只有「不落盘」:没有 `create_dir_all`、没有 `clear_natives`、
|
||||
/// 没有 `write_stamp`,因此版本树的目录结构与父目录 mtime 都不动。
|
||||
///
|
||||
/// jar 打不开 / 不是合法 zip 照样报错(与真启动同一条失败路径),预览不会藏坏包。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 目录为空、jar 打不开或不是合法 zip 时返回 [`NativesError`]。
|
||||
pub fn preview(dir: &Path, plan: &NativesPlan) -> Result<NativesReport, NativesError> {
|
||||
if dir.as_os_str().is_empty() {
|
||||
return Err(NativesError::NoDir);
|
||||
}
|
||||
let mut report = NativesReport {
|
||||
dir: dir.to_path_buf(),
|
||||
era: plan.era,
|
||||
jars: plan.jars.len(),
|
||||
files: 0,
|
||||
skipped: 0,
|
||||
ignored: 0,
|
||||
custom: plan.custom,
|
||||
up_to_date: false,
|
||||
planned: true,
|
||||
dir_exists: dir.is_dir(),
|
||||
};
|
||||
if plan.jars.is_empty() {
|
||||
report.up_to_date = true;
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
let stamps = jar_stamps(&plan.jars);
|
||||
if let Some(stamp) = read_stamp(dir) {
|
||||
if stamp.matches(&stamps) {
|
||||
report.up_to_date = true;
|
||||
report.skipped = stamp.files();
|
||||
return Ok(report);
|
||||
}
|
||||
}
|
||||
|
||||
for native in &plan.jars {
|
||||
jar::count_jar(native, &mut report)?;
|
||||
}
|
||||
Ok(report)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! natives 的报告与错误:真解压与预览共用的 [`NativesReport`] / [`NativesError`]。
|
||||
//!
|
||||
//! 单独成文件是为了让 `mod.rs` 只留「计划 + 两个入口(`extract` / `preview`)」,
|
||||
//! 本仓的红线是单文件 ≤400 行。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::NativesEra;
|
||||
|
||||
/// 解压结果 / 预览结果(进 `natives_extracted` 事件与 `--dry-run` 输出)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NativesReport {
|
||||
/// 目标目录。
|
||||
pub dir: PathBuf,
|
||||
/// 时代。
|
||||
pub era: NativesEra,
|
||||
/// 参与的 jar 数。
|
||||
pub jars: usize,
|
||||
/// 原生库文件数:真解压时是**本次写下**的数量,预览时是**将会写下**的数量。
|
||||
pub files: usize,
|
||||
/// 按戳记跳过、未重新解压的文件数(= 上次解压的文件数)。
|
||||
pub skipped: usize,
|
||||
/// 被排除 / 非原生库而忽略的条目数(`META-INF/`、签名文件)。
|
||||
pub ignored: usize,
|
||||
/// 目录是否用户自定义。
|
||||
pub custom: bool,
|
||||
/// 是否按戳记判定「目录已就绪」而整体跳过。
|
||||
pub up_to_date: bool,
|
||||
/// 报告是否来自只读预览([`super::preview`])。true = 盘上零改动,`files` 只是预计值。
|
||||
pub planned: bool,
|
||||
/// 报告生成时目标目录是否已在盘上。
|
||||
///
|
||||
/// 预览时这是**盘上真值**(用于区分「目录已就绪」与「真启动才会建出来」);
|
||||
/// 真解压路径在 `create_dir_all` 之后恒为 true。
|
||||
pub dir_exists: bool,
|
||||
}
|
||||
|
||||
impl NativesReport {
|
||||
/// 人读一行(CLI / 日志用)。
|
||||
pub fn summary(&self) -> String {
|
||||
if self.planned {
|
||||
return self.planned_summary();
|
||||
}
|
||||
if self.jars == 0 {
|
||||
return format!(
|
||||
"natives 目录已就绪(该版本无需解压,LWJGL 运行期自取)→ {}",
|
||||
self.dir.display()
|
||||
);
|
||||
}
|
||||
if self.up_to_date {
|
||||
return format!(
|
||||
"natives 已就绪({} 个原生库,{} 个 jar 未变)",
|
||||
self.skipped, self.jars
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"natives 解压 {} 个原生库({} 个 jar,忽略 {} 个条目)→ {}",
|
||||
self.files,
|
||||
self.jars,
|
||||
self.ignored,
|
||||
self.dir.display()
|
||||
)
|
||||
}
|
||||
|
||||
/// 预览版人读一行:句句带「将 / 未落盘」,别让用户把预览当成已经解好了。
|
||||
fn planned_summary(&self) -> String {
|
||||
if self.jars == 0 {
|
||||
return if self.dir_exists {
|
||||
format!(
|
||||
"natives 目录已就绪(该版本无需解压,LWJGL 运行期自取)→ {}",
|
||||
self.dir.display()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"natives 目录尚不存在,真启动会建出来(--dry-run 未落盘)→ {}",
|
||||
self.dir.display()
|
||||
)
|
||||
};
|
||||
}
|
||||
if self.up_to_date {
|
||||
return format!(
|
||||
"natives 已就绪({} 个原生库,{} 个 jar 未变;--dry-run 未落盘)",
|
||||
self.skipped, self.jars
|
||||
);
|
||||
}
|
||||
format!(
|
||||
"将解压 {} 个原生库({} 个 jar,忽略 {} 个条目)→ {}(--dry-run 未落盘)",
|
||||
self.files,
|
||||
self.jars,
|
||||
self.ignored,
|
||||
self.dir.display()
|
||||
)
|
||||
}
|
||||
|
||||
/// `--json` 的 `natives` 块(`ull launch --dry-run` 用;字段名是接口的一部分)。
|
||||
#[must_use]
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"dir": self.dir.display().to_string(),
|
||||
"files": self.files,
|
||||
"skipped": self.skipped,
|
||||
"ignored": self.ignored,
|
||||
"jars": self.jars,
|
||||
"custom": self.custom,
|
||||
"up_to_date": self.up_to_date,
|
||||
"era": self.era.as_str(),
|
||||
"planned": self.planned,
|
||||
"dir_exists": self.dir_exists,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 解压失败原因。
|
||||
#[derive(Debug)]
|
||||
pub enum NativesError {
|
||||
/// 目标目录为空(调用方漏填 `natives_dir`)。
|
||||
NoDir,
|
||||
/// 文件系统操作失败。
|
||||
Io {
|
||||
/// 操作名。
|
||||
op: &'static str,
|
||||
/// 相关路径。
|
||||
path: PathBuf,
|
||||
/// 底层错误。
|
||||
source: std::io::Error,
|
||||
},
|
||||
/// natives jar 打不开 / 不是合法 zip(下载损坏的最常见形态)。
|
||||
Zip {
|
||||
/// jar 路径。
|
||||
path: PathBuf,
|
||||
/// 原因。
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl NativesError {
|
||||
/// 一行人读描述。
|
||||
pub fn message(&self) -> String {
|
||||
match self {
|
||||
Self::NoDir => "natives 目录未配置(natives_dir 为空)".to_string(),
|
||||
Self::Io { op, path, source } => {
|
||||
format!("{op} 失败({}):{source}", path.display())
|
||||
}
|
||||
Self::Zip { path, reason } => {
|
||||
format!(
|
||||
"natives jar 无法作为 zip 打开({}):{reason}",
|
||||
path.display()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NativesError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.message())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NativesError {}
|
||||
@@ -305,8 +305,9 @@ pub struct LaunchRequest {
|
||||
pub dirs: LaunchDirs,
|
||||
/// natives 解压计划(`dirs.natives_dir` 为目标目录,`jars` 为分类器构件清单)。
|
||||
///
|
||||
/// 解压发生在**组装期**(`Launcher::dry_run`/`spawn` 的「环境准备」段),
|
||||
/// 见 [`crate::launch::natives`] 与 `SubAgentsTalk/launch-natives-解压与时代分界.md`。
|
||||
/// 解压发生在**组装期**(`spawn`/`launch` 的「环境准备」段);`dry_run` 只**预览**同一段
|
||||
/// 的结果(不建目录、不写文件),见 [`crate::launch::natives`] 与
|
||||
/// `SubAgentsTalk/launch-natives-解压与时代分界.md`。
|
||||
pub natives: NativesPlan,
|
||||
/// 账户。
|
||||
pub auth: AuthPlaceholders,
|
||||
|
||||
Reference in new issue
Block a user