- Library::linux_arm64_native_name 收敛改写规则,install 与 launch 共用 - presenter/CLI 的 classpath 组装:1.19+ 的独立 natives 条目按 arm64 坐标 找文件并进 classpath;≤1.18 的 natives 表条目仍归 natives::collect - 真机验证:ull install 1.20.4(740MB)→ glibc Temurin 21 + arm64 natives + softpipe GL,MC 主菜单在 termux-x11 中渲染(窗口 854x480) - 已知非致命:narrator flite 缺失、LWJGL JNI 版本告警、oshi cpuVendor NPE
410 lines
17 KiB
Rust
410 lines
17 KiB
Rust
//! 依赖库展开:主构件 + natives 分类器构件 → 待下载文件。
|
||
//!
|
||
//! 依据:`DevDocs/analyse/03-version-download.md` §6.4(libraries 含 natives 规则)、
|
||
//! §6.8(目录布局速查)。
|
||
//!
|
||
//! 三条容易踩的规则:
|
||
//! 1. **主构件与 natives 构件都要下**:老版本(lwjgl 2.x)一条库里同时有
|
||
//! `downloads.artifact` 与 `natives` 表,HMCL 也是两者都取;
|
||
//! 2. **路径优先用 JSON 给的 `path`**(Mojang 权威值),缺了才按 Maven 坐标推导;
|
||
//! 3. **`hint=local` 的库落到实例私有目录** `versions/<id>/libraries/`,其余一律进
|
||
//! 恒共享的 `libraries/`(D20:任何模式都不按实例复制)。
|
||
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use super::super::arguments::Environment;
|
||
use super::super::layout::GameLayout;
|
||
use super::super::library::Library;
|
||
use super::plan::{FileRole, PlanFile};
|
||
use crate::download::ResourceKind;
|
||
|
||
/// 没写 `url` 的库兜底用的 Maven 基址(Mojang 官方库仓)。
|
||
pub const DEFAULT_MAVEN_BASE: &str = "https://libraries.minecraft.net/";
|
||
|
||
/// 展开一层(已合并)清单里的库列表。
|
||
///
|
||
/// 返回(文件清单,人读告警)。告警不致命:缺下载地址的库会被跳过并告知,
|
||
/// 让用户知道"缺哪个文件"而不是拿到一个静默不完整的实例。
|
||
#[must_use]
|
||
pub fn plan(
|
||
layout: &GameLayout,
|
||
version_id: &str,
|
||
libraries: &[Library],
|
||
environment: &Environment,
|
||
) -> (Vec<PlanFile>, Vec<String>) {
|
||
let mut files = Vec::new();
|
||
let mut warnings = Vec::new();
|
||
|
||
for library in libraries {
|
||
if !library.applies(environment) {
|
||
continue;
|
||
}
|
||
// 现代清单把 natives 作为独立库条目(`org.lwjgl:lwjgl:3.3.2:natives-linux`),
|
||
// 不是 `natives` 表。Linux arm64 上改写坐标并兜底到 Maven 镜像。
|
||
if let Some(rewritten) = linux_arm64_native_entry(library, environment) {
|
||
files.push(PlanFile {
|
||
role: FileRole::Native,
|
||
name: rewritten.name,
|
||
url: rewritten.url,
|
||
sha1: None,
|
||
size: None,
|
||
dest: layout.libraries_dir().join(&rewritten.relative),
|
||
resource: ResourceKind::Library,
|
||
});
|
||
continue;
|
||
}
|
||
if library.coordinate().is_err() {
|
||
warnings.push(format!("库坐标不合法,已跳过:{}", library.name));
|
||
continue;
|
||
}
|
||
let Ok(relative) = library.relative_path() else {
|
||
warnings.push(format!("库路径推导失败,已跳过:{}", library.name));
|
||
continue;
|
||
};
|
||
let dest = library_dest(layout, version_id, library, &relative);
|
||
|
||
match library.artifact() {
|
||
Some(artifact) if artifact.has_url() => files.push(PlanFile {
|
||
role: FileRole::Library,
|
||
name: library.name.clone(),
|
||
url: artifact.url.clone().unwrap_or_default(),
|
||
sha1: artifact.sha1.clone(),
|
||
size: artifact.size,
|
||
dest,
|
||
resource: ResourceKind::Library,
|
||
}),
|
||
// natives-only 库(1.6 时代的 `lwjgl-platform` / `jinput-platform`)**没有主构件**,
|
||
// 只要 `downloads.classifiers`。这里若按坐标硬拼 URL 会拼出一个不存在的 jar
|
||
// (实测:官方与 BMCLAPI 双双 404)。判据是 `natives` 在场。
|
||
None if library.natives.is_some() => {}
|
||
// 魔改/现代 loader 的库常见"只给 name 不给 downloads",按坐标拼仓库地址。
|
||
_ => files.push(PlanFile {
|
||
role: FileRole::Library,
|
||
name: library.name.clone(),
|
||
url: maven_url(library, &relative),
|
||
sha1: None,
|
||
size: None,
|
||
dest,
|
||
resource: ResourceKind::Library,
|
||
}),
|
||
}
|
||
|
||
// natives 分类器构件(存在才下;解压发生在启动期,不归安装管)。
|
||
let Some(classifier) = library.native_classifier(environment) else {
|
||
continue;
|
||
};
|
||
let Ok(native_relative) = library.native_relative_path(&classifier) else {
|
||
warnings.push(format!("{} 的 natives 路径推导失败,已跳过", library.name));
|
||
continue;
|
||
};
|
||
let dest = layout.libraries_dir().join(&native_relative);
|
||
// Linux arm64:Mojang 清单没有 arm64 分类器,兜底到 Maven 镜像(见常量注释)。
|
||
let arm64_fallback = classifier == LINUX_ARM64_CLASSIFIER;
|
||
let (url, sha1, size) = match library.native_artifact(&classifier) {
|
||
Some(artifact) if artifact.has_url() => (
|
||
artifact.url.clone().unwrap_or_default(),
|
||
artifact.sha1.clone(),
|
||
artifact.size,
|
||
),
|
||
_ if arm64_fallback => (arm64_native_url(&native_relative), None, None),
|
||
Some(_) => {
|
||
warnings.push(format!(
|
||
"{} 的 natives 构件 {classifier} 没有下载地址,已跳过",
|
||
library.name
|
||
));
|
||
continue;
|
||
}
|
||
None => {
|
||
warnings.push(format!(
|
||
"{} 的 natives 分类器 {classifier} 没有下载信息,已跳过",
|
||
library.name
|
||
));
|
||
continue;
|
||
}
|
||
};
|
||
files.push(PlanFile {
|
||
role: FileRole::Native,
|
||
name: format!("{}:{classifier}", library.name),
|
||
url,
|
||
sha1,
|
||
size,
|
||
dest,
|
||
resource: ResourceKind::Library,
|
||
});
|
||
}
|
||
|
||
(files, warnings)
|
||
}
|
||
|
||
/// 库文件落点:实例私有(`hint=local`)或恒共享 `libraries/`。
|
||
fn library_dest(
|
||
layout: &GameLayout,
|
||
version_id: &str,
|
||
library: &Library,
|
||
relative: &Path,
|
||
) -> PathBuf {
|
||
if library.is_local() {
|
||
layout.version_libraries_dir(version_id).join(relative)
|
||
} else {
|
||
layout.libraries_dir().join(relative)
|
||
}
|
||
}
|
||
|
||
/// 现代清单里 LWJGL3 natives 条目的改写结果。
|
||
struct Arm64Native {
|
||
name: String,
|
||
relative: PathBuf,
|
||
url: String,
|
||
}
|
||
|
||
/// Linux arm64:把 `org.lwjgl:<module>:<version>:natives-linux` 改写成 arm64。
|
||
fn linux_arm64_native_entry(library: &Library, env: &Environment) -> Option<Arm64Native> {
|
||
let name = library.linux_arm64_native_name(env)?;
|
||
let base = name.strip_suffix(":natives-linux-arm64")?;
|
||
let mut parts = base.split(':');
|
||
let group = parts.next()?;
|
||
let artifact = parts.next()?;
|
||
let version = parts.next()?;
|
||
if parts.next().is_some() {
|
||
return None;
|
||
}
|
||
let relative = PathBuf::from(format!(
|
||
"{}/{artifact}/{version}/{artifact}-{version}-natives-linux-arm64.jar",
|
||
group.replace('.', "/")
|
||
));
|
||
let url = format!(
|
||
"{LINUX_ARM64_NATIVES_BASE}{}",
|
||
relative.to_string_lossy().replace('\\', "/")
|
||
);
|
||
Some(Arm64Native {
|
||
name,
|
||
relative,
|
||
url,
|
||
})
|
||
}
|
||
|
||
/// 按库声明(或默认)的 Maven 基址拼下载地址。
|
||
fn maven_url(library: &Library, relative: &Path) -> String {
|
||
let base = library
|
||
.url
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.filter(|base| !base.is_empty())
|
||
.unwrap_or(DEFAULT_MAVEN_BASE);
|
||
let path = relative.to_string_lossy().replace('\\', "/");
|
||
if base.ends_with('/') {
|
||
format!("{base}{path}")
|
||
} else {
|
||
format!("{base}/{path}")
|
||
}
|
||
}
|
||
|
||
/// Linux arm64 的 natives 兜底仓库:Mojang 的 `libraries.minecraft.net` 对
|
||
/// `natives-linux-arm64` 一律 404(实测 3.3.2 的 lwjgl/glfw/opengl/stb/tinyfd/openal),
|
||
/// 同一坐标在 Maven Central / 阿里云镜像有。
|
||
pub const LINUX_ARM64_NATIVES_BASE: &str = "https://maven.aliyun.com/repository/public/";
|
||
|
||
/// Linux arm64 的 natives 分类器(见 `Library::native_classifier`)。
|
||
pub const LINUX_ARM64_CLASSIFIER: &str = "natives-linux-arm64";
|
||
|
||
/// arm64 natives 的兜底 URL(按相对路径拼镜像基址)。
|
||
fn arm64_native_url(relative: &Path) -> String {
|
||
format!(
|
||
"{LINUX_ARM64_NATIVES_BASE}{}",
|
||
relative.to_string_lossy().replace('\\', "/")
|
||
)
|
||
}
|
||
|
||
/// 供上层判错用:库展开不产生硬失败,缺地址一律降级成告警。
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::plan;
|
||
use crate::game::arguments::Environment;
|
||
use crate::game::layout::GameLayout;
|
||
use crate::game::library::Library;
|
||
|
||
fn environment() -> Environment {
|
||
Environment {
|
||
os: "windows".to_string(),
|
||
os_version: String::new(),
|
||
arch: "x86_64".to_string(),
|
||
..Environment::host()
|
||
}
|
||
}
|
||
|
||
fn library_with_artifact(json: &str) -> Library {
|
||
serde_json::from_str(json).expect("库 JSON 应可解析")
|
||
}
|
||
|
||
#[test]
|
||
fn plans_artifact_at_maven_path() {
|
||
let library = library_with_artifact(
|
||
r#"{"name":"org.ow2.asm:asm:9.6","downloads":{"artifact":{"path":"org/ow2/asm/asm/9.6/asm-9.6.jar","sha1":"aa","size":122,"url":"https://libraries.minecraft.net/org/ow2/asm/asm/9.6/asm-9.6.jar"}}}"#,
|
||
);
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, warnings) = plan(&layout, "1.20.1", &[library], &environment());
|
||
assert!(warnings.is_empty());
|
||
assert_eq!(files.len(), 1);
|
||
assert_eq!(files[0].sha1.as_deref(), Some("aa"));
|
||
assert_eq!(files[0].size, Some(122));
|
||
assert!(files[0]
|
||
.dest
|
||
.ends_with("libraries/org/ow2/asm/asm/9.6/asm-9.6.jar"));
|
||
}
|
||
|
||
#[test]
|
||
fn natives_only_library_plans_no_artifact() {
|
||
// 1.6 时代的 lwjgl-platform / jinput-platform:有 natives 表、只有 classifiers,
|
||
// **没有 downloads.artifact**。按坐标硬拼主构件会拼出不存在的 jar(实测双源 404)。
|
||
let library = library_with_artifact(
|
||
r#"{"name":"org.lwjgl.lwjgl:lwjgl-platform:2.9.0",
|
||
"natives":{"windows":"natives-windows","linux":"natives-linux","osx":"natives-osx"},
|
||
"downloads":{"classifiers":{"natives-windows":{"path":"org/lwjgl/lwjgl/lwjgl-platform/2.9.0/lwjgl-platform-2.9.0-natives-windows.jar","sha1":"3f11","size":609967,"url":"https://libraries.minecraft.net/natives-windows.jar"}}}}"#,
|
||
);
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, warnings) = plan(&layout, "1.6.4", &[library], &environment());
|
||
assert!(warnings.is_empty(), "不该有告警:{warnings:?}");
|
||
assert_eq!(files.len(), 1, "只应有 natives 构件:{files:?}");
|
||
assert_eq!(files[0].role.as_str(), "native");
|
||
assert_eq!(files[0].sha1.as_deref(), Some("3f11"));
|
||
}
|
||
|
||
#[test]
|
||
fn plans_both_artifact_and_native_jar() {
|
||
let library = library_with_artifact(
|
||
r#"{"name":"org.lwjgl.lwjgl:lwjgl-platform:2.9.4","natives":{"windows":"natives-windows"},
|
||
"downloads":{"artifact":{"path":"org/lwjgl/lwjgl/lwjgl-platform/2.9.4/lwjgl-platform-2.9.4.jar","sha1":"bb","size":1,"url":"https://libraries.minecraft.net/a.jar"},
|
||
"classifiers":{"natives-windows":{"path":"org/lwjgl/lwjgl/lwjgl-platform/2.9.4/lwjgl-platform-2.9.4-natives-windows.jar","sha1":"cc","size":2,"url":"https://libraries.minecraft.net/b.jar"}}}}"#,
|
||
);
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, _) = plan(&layout, "1.7.10", &[library], &environment());
|
||
assert_eq!(files.len(), 2);
|
||
let native = files
|
||
.iter()
|
||
.find(|f| f.role.as_str() == "native")
|
||
.expect("应有 natives 构件");
|
||
assert_eq!(native.sha1.as_deref(), Some("cc"));
|
||
assert!(native
|
||
.dest
|
||
.ends_with("lwjgl-platform-2.9.4-natives-windows.jar"));
|
||
}
|
||
|
||
#[test]
|
||
fn derives_url_for_libraries_without_downloads() {
|
||
let library = library_with_artifact(
|
||
r#"{"name":"net.minecraftforge:forge:1.20.1-47.2.0:universal","url":"https://maven.minecraftforge.net/"}"#,
|
||
);
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, warnings) = plan(&layout, "1.20.1-forge", &[library], &environment());
|
||
assert!(warnings.is_empty());
|
||
assert_eq!(files.len(), 1);
|
||
assert_eq!(
|
||
files[0].url,
|
||
"https://maven.minecraftforge.net/net/minecraftforge/forge/1.20.1-47.2.0/forge-1.20.1-47.2.0-universal.jar"
|
||
);
|
||
assert!(files[0].sha1.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn skips_libraries_filtered_by_rules() {
|
||
let library = library_with_artifact(
|
||
r#"{"name":"org.lwjgl:lwjgl:3.3.2","rules":[{"action":"allow","os":{"name":"linux"}}],
|
||
"downloads":{"artifact":{"url":"https://libraries.minecraft.net/x.jar","sha1":"dd"}}}"#,
|
||
);
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, _) = plan(&layout, "1.20.1", &[library], &environment());
|
||
assert!(files.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn local_hint_libraries_land_in_instance_dir() {
|
||
let library = library_with_artifact(
|
||
r#"{"name":"net.minecraftforge:forge:1.20.1-47.2.0","hint":"local",
|
||
"downloads":{"artifact":{"path":"net/minecraftforge/forge/1.20.1-47.2.0.jar","url":"https://libraries.minecraft.net/y.jar"}}}"#,
|
||
);
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, _) = plan(&layout, "inst", &[library], &environment());
|
||
assert_eq!(files.len(), 1);
|
||
assert!(files[0]
|
||
.dest
|
||
.ends_with("versions/inst/libraries/net/minecraftforge/forge/1.20.1-47.2.0.jar"));
|
||
}
|
||
|
||
#[test]
|
||
fn linux_arm64_lwjgl3_natives_fall_back_to_maven_mirror() {
|
||
// Mojang 的清单只有 `natives-linux`(x86_64)且没有 arm64 分类器下载信息;
|
||
// Linux arm64 要改写成 `natives-linux-arm64` 并兜底到 Maven 镜像。
|
||
let library = library_with_artifact(
|
||
r#"{"name":"org.lwjgl:lwjgl:3.3.2","natives":{"linux":"natives-linux"}}"#,
|
||
);
|
||
let env = Environment {
|
||
os: "linux".to_string(),
|
||
os_version: String::new(),
|
||
arch: "arm64".to_string(),
|
||
..environment()
|
||
};
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, warnings) = plan(&layout, "1.20.4", &[library], &env);
|
||
assert!(warnings.is_empty(), "不该有告警:{warnings:?}");
|
||
assert_eq!(files.len(), 1);
|
||
assert_eq!(files[0].name, "org.lwjgl:lwjgl:3.3.2:natives-linux-arm64");
|
||
assert_eq!(
|
||
files[0].url,
|
||
"https://maven.aliyun.com/repository/public/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux-arm64.jar"
|
||
);
|
||
assert!(
|
||
files[0].sha1.is_none(),
|
||
"镜像兜底没有 sha1,只能靠大小/存在性"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn modern_lwjgl_native_entry_is_rewritten_to_arm64_maven() {
|
||
// 1.20.x 的清单把 natives 作为独立条目,分类器写在 name 里。
|
||
let library = library_with_artifact(
|
||
r#"{"name":"org.lwjgl:lwjgl:3.3.2:natives-linux",
|
||
"rules":[{"action":"allow","os":{"name":"linux"}}],
|
||
"downloads":{"artifact":{"path":"org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux.jar","url":"https://libraries.minecraft.net/x.jar","sha1":"aa"}}}"#,
|
||
);
|
||
let env = Environment {
|
||
os: "linux".to_string(),
|
||
os_version: String::new(),
|
||
arch: "arm64".to_string(),
|
||
..environment()
|
||
};
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, warnings) = plan(&layout, "1.20.4", &[library], &env);
|
||
assert!(warnings.is_empty(), "不该有告警:{warnings:?}");
|
||
assert_eq!(files.len(), 1);
|
||
assert_eq!(files[0].role.as_str(), "native");
|
||
assert_eq!(files[0].name, "org.lwjgl:lwjgl:3.3.2:natives-linux-arm64");
|
||
assert_eq!(
|
||
files[0].url,
|
||
"https://maven.aliyun.com/repository/public/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux-arm64.jar"
|
||
);
|
||
assert!(files[0]
|
||
.dest
|
||
.ends_with("libraries/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux-arm64.jar"));
|
||
}
|
||
|
||
#[test]
|
||
fn linux_x86_64_keeps_the_manifest_classifier() {
|
||
let library = library_with_artifact(
|
||
r#"{"name":"org.lwjgl:lwjgl:3.3.2","natives":{"linux":"natives-linux"}}"#,
|
||
);
|
||
let env = Environment {
|
||
os: "linux".to_string(),
|
||
os_version: String::new(),
|
||
arch: "x86_64".to_string(),
|
||
..environment()
|
||
};
|
||
let layout = GameLayout::new("/g/.minecraft");
|
||
let (files, warnings) = plan(&layout, "1.20.4", &[library], &env);
|
||
assert!(files.is_empty(), "x86_64 没有清单下载信息,不该凭空拼 URL");
|
||
assert!(
|
||
warnings.iter().any(|w| w.contains("natives-linux")),
|
||
"{warnings:?}"
|
||
);
|
||
}
|
||
}
|