Files
Unlucky-Launcher/crates/core/src/download/adoptium.rs
T
AskaEth bc41dfcf7d
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
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

205 lines
7.6 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.
//! Adoptium(Temurin)JDK 的院校镜像展开。
//!
//! D13 定案:国内模式下 JDK 按健康度在 TUNA / 华为云 等镜像之间选,
//! 全部不可达才回官方。难点是**镜像路径与官方下载链接不同形**:
//!
//! 官方(api.adoptium.net 会 302 到这里):
//! `https://github.com/adoptium/temurin17-binaries/releases/download/JDK17.0.13+11/OpenJDK17U-jdk_x64_windows_hotspot_17.0.13_11.zip`
//! 清华 TUNA:
//! `https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jdk/x64/windows/OpenJDK17U-jdk_x64_windows_hotspot_17.0.13_11.zip`
//!
//! 所以这里按产物文件名反解 `(major, flavor, arch, os, filename)`,再按各镜像的模板拼目标 URL。
//! **`flavor` 必须是 `jdk` 或 `jre`**:院校镜像的目录结构是 `/{major}/{jdk|jre}/{arch}/{os}/`,
//! JRE 请求若错拼成 `/jdk/` 会 404 打满所有镜像,最后退回官方直链(镜像加速白费)。
//! 解析失败不报错——只保留官方源,下载照常能完成。
//!
//! # 清单可用性(M33 实测,2026-10-30)
//!
//! 按 `{base}/{21}/jdk/x64/windows/<Temurin 文件名>` 逐个打点:
//!
//! | 镜像 | 结果 |
//! | --- | --- |
//! | 清华 TUNA `mirrors.tuna.tsinghua.edu.cn/Adoptium` | 200(真实下载 205 MB / 22 秒成功) |
//! | 中科大 USTC | **404**——`/adoptium/` 下只有 `deb/` `rpm/` `releases/`,没有这套目录树 |
//! | 上交 SJTU / 北外 BFSU | **404**(同样没有该目录树) |
//! | 华为云 | **假阳性**:门户对任意路径都回 `200` + 12 KB HTML(M34 实测,`/v1/repositories` 里没有 adoptium) |
//!
//! 因此 `mirrors.json` 里这四项全是 `enabled: false`:留着当「将来恢复」的登记,
//! 但不让它们每次安装都白跑一趟(健康度虽然会冷却,首轮仍要付代价)。
//! **判可用性别只看状态码**:真仓库目录页含 `<pre>`,门户兜底页没有——状态码 200 说明不了什么。
//!
//! # 4xx 不是「源坏」
//!
//! TUNA 每个 `{major}/{jdk|jre}/{arch}/{os}/` 目录只保留**最新一个补丁**(实测 21.0.12 → 404,
//! 21.0.13 → 200 且大小与 Adoptium API 一致)。所以 `NotFound` 在引擎里走
//! `HealthRegistry::record_missing`:不计失败、不进冷却,只留诊断痕迹。
use serde::{Deserialize, Serialize};
use super::mirrors::MirrorTable;
use super::types::Candidate;
const fn default_true() -> bool {
true
}
/// 一个 Adoptium 院校镜像。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdoptiumMirror {
/// 源标识(`tuna` / `ustc` / `sjtu` / `bfsu`)。
pub id: String,
/// 展示名。
pub name: String,
/// 镜像根(形如 `https://mirrors.tuna.tsinghua.edu.cn/Adoptium`)。
pub base: String,
/// 是否启用。
#[serde(default = "default_true")]
pub enabled: bool,
/// 该源只保留「最新补丁」(TUNA 实测:每个 `{major}/{jdk|jre}/{arch}/{os}/` 只有 1 个归档)。
///
/// 标记本身不改 URL 形态,只给诊断与 `java vendors` 提示用:旧补丁在这里必然 404,
/// 引擎侧把 4xx 当「正常降级」而不是「源坏」(见 `download::health::record_missing`)。
#[serde(default)]
pub latest_only: bool,
}
impl MirrorTable {
/// Adoptium 类 URL 展开成院校镜像候选(按清单顺序;健康度排序由引擎负责)。
#[must_use]
pub fn adoptium_candidates(&self, url: &str) -> Vec<Candidate> {
let Some(artifact) = parse_adoptium_artifact(url) else {
return Vec::new();
};
self.adoptium
.iter()
.filter(|mirror| mirror.enabled)
.map(|mirror| {
let base = mirror.base.trim_end_matches('/');
Candidate::new(mirror.id.clone(), artifact.mirror_url(base))
})
.collect()
}
}
/// Adoptium 产物形态:JDK 还是 JRE(决定镜像的 `{major}/jdk|jre/` 路径段)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JdkFlavor {
/// 完整开发包(`OpenJDK17U-jdk_...`)。
Jdk,
/// 运行时(`OpenJDK17U-jre_...`)。
Jre,
}
impl JdkFlavor {
/// 镜像路径里的段名。
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Jdk => "jdk",
Self::Jre => "jre",
}
}
/// 从产物文件名判形态:`OpenJDK17U-jdk_` / `OpenJDK17U-jre_` / `*-jre-17…` 兜底。
#[must_use]
fn from_filename(filename: &str) -> Self {
if filename.contains("-jre") || filename.contains("_jre") {
Self::Jre
} else {
Self::Jdk
}
}
}
/// 解析出来的 Adoptium 产物。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdoptiumArtifact {
/// 大版本号(`17` / `21`)。
pub major: String,
/// JDK 还是 JRE。
pub flavor: JdkFlavor,
/// CPU 架构(`x64` / `aarch64` / …)。
pub arch: String,
/// 目标系统(`windows` / `linux` / `mac` / …)。
pub os: String,
/// 产物文件名(含扩展名)。
pub filename: String,
}
impl AdoptiumArtifact {
/// 拼某个镜像根下的下载地址:`{base}/{major}/{jdk|jre}/{arch}/{os}/{filename}`。
#[must_use]
pub fn mirror_url(&self, base: &str) -> String {
format!(
"{}/{}/{}/{}/{}/{}",
base.trim_end_matches('/'),
self.major,
self.flavor.as_str(),
self.arch,
self.os,
self.filename
)
}
}
/// 解析 Adoptium 产物名。
///
/// 输入形如 `https://github.com/adoptium/temurin17-binaries/releases/download/JDK17.0.13%2B11/OpenJDK17U-jdk_x64_windows_hotspot_17.0.13_11.zip`。
/// 解析失败返回 `None`(此时只保留官方源,不影响下载)。
#[must_use]
pub fn parse_adoptium_artifact(url: &str) -> Option<AdoptiumArtifact> {
let filename = url.rsplit('/').next()?;
if filename.is_empty()
|| (!filename.contains("OpenJDK") && !filename.contains("jdk") && !filename.contains("jre"))
{
return None;
}
let mut major: Option<String> = None;
let mut arch: Option<&str> = None;
let mut os: Option<&str> = None;
for token in filename.split('_') {
if let Some(stripped) = token.strip_prefix("OpenJDK") {
let digits: String = stripped.chars().take_while(char::is_ascii_digit).collect();
if !digits.is_empty() {
major = Some(digits);
}
}
arch = arch.or(match token {
"x64" => Some("x64"),
"x86-32" | "x86" => Some("x86-32"),
"aarch64" => Some("aarch64"),
"arm" => Some("arm"),
"ppc64le" => Some("ppc64le"),
"s390x" => Some("s390x"),
_ => None,
});
os = os.or(match token {
"windows" => Some("windows"),
"linux" => Some("linux"),
"mac" => Some("mac"),
"alpine-linux" => Some("alpine-linux"),
"aix" => Some("aix"),
_ => None,
});
}
let major = match major {
Some(major) => major,
None => {
// 兜底形态:`jdk-17.0.13+11_...` 之类。
let rest = filename.split("jdk-").nth(1)?;
let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
if digits.is_empty() {
return None;
}
digits
}
};
Some(AdoptiumArtifact {
major,
flavor: JdkFlavor::from_filename(filename),
arch: arch?.to_string(),
os: os?.to_string(),
filename: filename.to_string(),
})
}