Files
Unlucky-Launcher/crates/core/src/launch/natives/mod.rs
T
AskaEth 5046f96f7e
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.2.1
2026-11-06 20:47:19 +08:00

356 lines
13 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.
//! natives 解压:把版本链里的 natives 分类器构件解到 `versions/<id>/natives/`。
//!
//! 这是 10 阶段管线里**第 6 段「环境准备」**的落盘部分(`PlanDoc/06` §1),发生顺序在
//! 命令行组装之前(HMCL 也在组装期做:`DefaultLauncher.decompressNatives`,
//! `analyse/03` §6.4-(7)),因此 `--dry-run` 也能验证「装完到底能不能起」。
//!
//! # 三个时代(`SubAgentsTalk/launch-natives-解压与时代分界.md`,piston-meta 实测)
//!
//! | 版本区间 | natives 形态 | 谁负责 `-Djava.library.path` |
//! |---|---|---|
//! | ≤1.12.2(LWJGL 2.9) | `natives` 表 + `downloads.classifiers` | 启动器补(`defaults::legacy_jvm_arguments`) |
//! | 1.13–1.18.2(LWJGL 3.1/3.2) | 同上(另有同坐标的主构件条目) | 清单自己写 `arguments.jvm` |
//! | 1.19+(LWJGL 3.3) | **没有 `natives` 表**:natives jar 是普通 classpath 库 | 清单写 4 个目录属性,LWJGL 运行期自解压 |
//!
//! 所以三个时代走**同一条代码路径**:有 jar 就解压,没 jar 就只把目录建出来
//! (1.19+ 的 `-Dorg.lwjgl.system.SharedLibraryExtractPath` 指向它;目录不存在时
//! LWJGL 会静默走到「找不到 native」)。
//!
//! # 幂等
//!
//! 解压完在目录里写一份戳记 [`jar::STAMP_FILE`](每个源 jar 的路径 + 字节数 + mtime)。
//! 下次启动:戳记与当前 jar 集完全一致 → 目录视为就绪,**不读 jar、不写盘**直接跳过;
//! 不一致(换版本 / 库升级 / 用户删了目录)→ 清掉旧原生库再全量解压,避免两代 dll
//! 混在同一个 `java.library.path` 里(HMCL 也是每次清目录,这里只把「没变就不动」补上)。
pub(crate) mod jar;
use std::path::{Path, PathBuf};
use crate::game::arguments::Environment;
use crate::game::layout::GameLayout;
use crate::game::library::Library;
use crate::launch::natives::jar::{
clear_natives, extract_jar, jar_stamps, read_stamp, write_stamp,
};
/// 戳记文件名:放在 natives 目录里,跟着目录一起删 / 建。
///
/// 公开出来是为了「一键修复」类功能能认出自己的痕迹(判断目录里哪些文件是我们解出来的)。
pub const STAMP_FILE: &str = ".ull-natives.json";
/// 一个待解压的 natives 构件(版本链里 `natives` 分类器命中的 jar)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeJar {
/// 磁盘路径(`libraries/<group>/.../<artifact>-<ver>-natives-<os>.jar`)。
pub path: PathBuf,
/// 坐标 + 分类器(`org.lwjgl:lwjgl:3.2.2:natives-windows`,报错与事件里用)。
pub name: String,
/// 清单 `extract.exclude` 的排除前缀(`["META-INF/"]`)。
pub exclude: Vec<String>,
}
impl NativeJar {
/// 构造(无排除规则)。
pub fn new(path: impl Into<PathBuf>, name: impl Into<String>) -> Self {
Self {
path: path.into(),
name: name.into(),
exclude: Vec::new(),
}
}
/// 带清单排除规则构造。
pub fn with_exclude(
path: impl Into<PathBuf>,
name: impl Into<String>,
exclude: Vec<String>,
) -> Self {
Self {
path: path.into(),
name: name.into(),
exclude,
}
}
}
/// natives 处理的时代(决定「要不要解压」以及「`-Djava.library.path` 从哪来」)。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NativesEra {
/// ≤1.12.2:清单没有 `arguments.jvm`,启动器自己补 `-Djava.library.path`。
#[default]
Legacy,
/// 1.13–1.18.2:清单有 `arguments.jvm`,natives 走分类器 jar(需要解压)。
Lwjgl3,
/// 1.19+:natives jar 是普通 classpath 库,LWJGL 3.3 运行期自解压(只需目录存在)。
ClasspathNative,
}
impl NativesEra {
/// 按「清单有没有 `arguments.jvm`」+「有没有 natives 分类器 jar」判时代。
pub fn detect(manifest_jvm_args: usize, natives_jars: usize) -> Self {
if manifest_jvm_args == 0 {
Self::Legacy
} else if natives_jars == 0 {
Self::ClasspathNative
} else {
Self::Lwjgl3
}
}
/// 事件/日志里的稳定字符串。
pub fn as_str(self) -> &'static str {
match self {
Self::Legacy => "legacy",
Self::Lwjgl3 => "lwjgl3",
Self::ClasspathNative => "classpath_native",
}
}
}
/// 解压计划(`LaunchRequest` 的一部分,纯数据)。
///
/// 目标目录**不在**这里:唯一真相源是 `LaunchDirs::natives_dir`(`${natives_directory}` 也用它),
/// 免得出现「参数指向 A、dll 解到 B」的静默错位。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativesPlan {
/// 待解压的 natives 分类器构件。
pub jars: Vec<NativeJar>,
/// 时代(只影响报告字段;解压路径三时代统一)。
pub era: NativesEra,
/// 目录是否用户自定义(事件 payload 的 `custom` 字段,给「自备 dll 被覆盖」留证据)。
pub custom: bool,
}
impl Default for NativesPlan {
fn default() -> Self {
Self {
jars: Vec::new(),
era: NativesEra::ClasspathNative,
custom: false,
}
}
}
impl NativesPlan {
/// 以 jar 清单构造:有 jar 按 [`NativesEra::Lwjgl3`],没有按 [`NativesEra::ClasspathNative`]。
///
/// 这一层分不出「≤1.12.2 的 LWJGL 2」与「1.13–1.18.2 的 LWJGL 3」(两者都有分类器 jar),
/// 所以适配层用 [`NativesPlan::with_era`] 明确给值;直接构造只适合测试与脚本。
pub fn new(jars: Vec<NativeJar>) -> Self {
let era = NativesEra::detect(usize::from(!jars.is_empty()), jars.len());
Self {
jars,
era,
custom: false,
}
}
/// 指定时代。
pub fn with_era(mut self, era: NativesEra) -> Self {
self.era = era;
self
}
/// 标记目录为用户自定义。
pub fn with_custom(mut self, custom: bool) -> Self {
self.custom = custom;
self
}
/// 没有任何 natives 构件(1.19+ 的常态)。
pub fn is_empty(&self) -> bool {
self.jars.is_empty()
}
}
/// 解压结果(进 `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()
)
}
}
/// 解压失败原因。
#[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 {}
/// 从版本链库表收集 natives 分类器构件(adapter / app / CLI 共用这一段,别各写一份)。
///
/// 口径与安装层 `game::install::libraries::plan` **除 `hint=local` 落点外**逐条对齐:
/// 1. 先按环境过滤(`Library::applies`);
/// 2. `natives` 表命中当前 OS 才取分类器(1.19+ 没有 `natives` 表 → 返回空表);
/// 3. 落点优先用清单 `downloads.classifiers.<classifier>.path`,缺了才按 Maven 坐标推导
/// (`Library::native_relative_path`);`hint=local` 的库在实例私有目录里
/// (`analyse/03` §6.8 的 HMCL 口径)。
///
/// ⚠️ 安装层当前的 natives 分类器分支硬编码共享 `libraries/`、不认 `hint=local`(主构件认),
/// 所以「`hint=local` + `natives` 同体」的 manifest(只有 HMCL 补丁导入才会有)会出现
/// 「装到共享目录、解压去实例目录」→ 由 [`crate::launch::adapter::AdapterWarning::MissingNativeJar`]
/// 转成启动前 `missing_components` 显式失败。此处仍按 HMCL 权威口径实现,等 install 侧对齐。
pub fn collect(
layout: &GameLayout,
version_id: &str,
libraries: &[Library],
environment: &Environment,
) -> Vec<NativeJar> {
let mut natives = Vec::new();
for library in libraries {
if !library.applies(environment) {
continue;
}
let Some(classifier) = library.native_classifier(environment) else {
continue;
};
let Ok(relative) = library.native_relative_path(&classifier) else {
continue;
};
// 安装层当前对 natives 不做这条分支(见上方警告);这里按 HMCL 口径落实例私有目录。
let base = if library.is_local() {
layout.version_libraries_dir(version_id)
} else {
layout.libraries_dir()
};
let exclude = library
.extract
.as_ref()
.map(|extract| extract.exclude.clone())
.unwrap_or_default();
natives.push(NativeJar::with_exclude(
base.join(relative),
format!("{}:{classifier}", library.name),
exclude,
));
}
natives
}
/// 解压一个计划(幂等:目录与源 jar 都没变时零写盘)。
///
/// # Errors
///
/// 目录为空、建目录 / 写文件失败、jar 不是合法 zip 时返回 [`NativesError`]。
pub fn extract(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,
};
// 1.19+ 没有 jar 也要建目录:LWJGL 3.3 往 SharedLibraryExtractPath 里解压。
std::fs::create_dir_all(dir)
.map_err(|source| jar::io_error("create_natives_dir", dir, source))?;
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);
}
}
// 源变了:先清旧原生库(避免两代 dll 混在同一个 java.library.path 里),再全量解。
clear_natives(dir);
for native in &plan.jars {
extract_jar(native, dir, &mut report)?;
}
write_stamp(dir, report.files, &stamps)?;
Ok(report)
}