Files
Unlucky-Launcher/crates/core/src/game/java/source.rs
T
worker-javapin 1353c044c3 fix(java): Temurin 指定版本要翻页 + 三层「不静默回落」补上守卫(M280 r1)
复审 r1 判 p2-2items / fix-then-merge。两条都在这轮修掉。

## P2-1 Temurin 只读第一页 ⇒ 上游有的版本被报「没有」
`page_size` 上限 20 是上游硬约束,但 8/11/17 线各有 26 条 GA(实测 17/ga page=0 回
20 条、page=1 回 6 条)。原来只读第一页,`17.0.4` 这种「上游有、第一页没有」的版本
被报成「没有这个版本」,而且「等 20 个」是我们的窗口而不是上游总数。

- `releases_url(major, target, page)` 带页码;`collect_releases(fetch_page)` 是**纯**骨架
  (分页由闭包注入),翻到「本页不满一页」为止,上限 `MAX_PAGES = 5`;
- `fetch_version` 用 `pick_release(&releases, wanted)` 挑(不取最新、不取第一条);
- 文案:`catalog::VersionListScope::{Complete, Truncated}` +
  `available_versions_text(.., scope)`。完整时说「等 N 个」(= 上游总数),
  到上限时才说「只查了前 N 条,后面可能还有」——不再把窗口当全集。
- 真机复现(复审给的命令):`17 --version 17.0.4` 现在命中 `17.0.4+8`;
  `17 --version 17` 命中第二页最后一条 `17+35`;`17 --version 17.0.99` 报「等 26 个」。

## P2-2 三层保证一条守卫都没有(复审的变异 A/B/C)
现在每条都有能变红的用例:

| 守卫 | 位置 | 用例 |
| --- | --- | --- |
| Temurin 命中而非取最新 | `api/version.rs::pick_release` | `the_pinned_version_wins_over_the_newest_entry` + 两页用例 |
| 兜底闸(版本必须对上) | `catalog::version_mismatch` | `catalog/tests.rs::the_version_gate_rejects_...` |
| 装配层的「记失败继续走链」 | `install/chain.rs::resolve_one` / `chain_fetch` | `a_release_that_is_not_...`、`a_mismatch_records_a_failure_and_the_chain_moves_on`、`nothing_matching_...` |
| 钉死一家不换家 | `install()` 全链(`--vendor zulu --version`) | `a_pinned_vendor_that_cannot_pin_is_tried_alone`(断言「试过 1」+ `latest=true` + 一个字节没落盘) |

`install` 的循环与 `--dry-run` 的 `plan` 现在**共用同一份判据**(`resolve_one`):
`install` 逐家调它,`plan` 用 `chain_fetch` 驱动整条链。

## 拆分(守 400 行硬线)
- `catalog.rs` 400 → `catalog.rs` 278 + `catalog/tests.rs` 229(既有 7 条用例搬走,
  唯一差异是新 API 的 `VersionListScope::Complete` 参数)
- `install.rs` → `install/plan.rs`(`plan` 段落;文档与签名原样,函数体改成走
  `chain_fetch`,这正是「同一份判据」的要求)+ `install/chain.rs`(新骨架与守卫,
  `install.rs` 361 行)
- `api/version.rs` 417 → `api/version.rs` 214 + `api/version/tests.rs` 207

## 门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-280)
core --lib 870 / --test game 120 / --test launch 91、cli --all-targets、fmt --check、
clippy -D warnings 全部 EXIT=0。
2026-11-12 22:05:19 +08:00

258 lines
8.8 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.
//! 版本查询的发行版分派 + 回退链记录(D23)。
//!
//! 两件事:
//!
//! 1. [`fetch`]:把一次「某发行版某大版本的最新 GA」查询分派给对应实现——
//! Temurin 走 `api.adoptium.net`(v1 既有),Oracle / MS / Zulu 各走自己的源表;
//! 2. [`ChainLog`]:按用户优先级依次尝试时,**每一跳的结果与降级原因**都记下来。
//! 这份记录直接进 `--json` 与事件流,是「为什么最后装的是 MS 而不是 Temurin」的唯一解释。
//!
//! 回退的粒度是**发行版级**(D23 原话):版本不存在、镜像全 4xx、校验失败都算这一家失败,
//! 换下一家;同一家内部的镜像换源(TUNA→官方、华为云→南大)仍然归 `core::download` 引擎管。
use super::api;
use super::catalog;
use super::error::{JavaError, JavaErrorKind};
use super::microsoft;
use super::oracle;
use super::release::JdkRelease;
use super::target::AdoptiumTarget;
use super::vendor::JdkVendor;
use super::zulu;
use crate::download::{DownloadEngine, MirrorTable};
/// 查一次「某发行版某大版本」的归档。
///
/// `version = None` 是要该家**最新 GA**(D13 口径,行为与 M280 之前逐字节一致);
/// `Some(v)` 要求精确版本:能按版本取的只有 Temurin / Oracle,Microsoft 只认内置快照里那一个,
/// Zulu(只有 `latest=true`)与 Termux(系统包定版本)直接报错——错误文案由
/// [`catalog::version_pin_error`] 统一出,**这里不换家、不回落 latest**。
///
/// # Errors
///
/// 平台无产物、清单不可达、该版本不存在、校验缺失、这家不支持指定版本时返回错误——调用方据此降级。
pub async fn fetch(
engine: &DownloadEngine,
vendor: JdkVendor,
major: u32,
target: AdoptiumTarget,
version: Option<&str>,
) -> Result<JdkRelease, JavaError> {
let client = engine.client();
let table = engine.mirrors();
match version {
None => latest(client, table, vendor, major, target).await,
Some(wanted) => pinned(client, table, vendor, major, target, wanted).await,
}
}
/// 该家的最新 GA(`fetch` 的 `None` 那一路,M280 之前只有这一条)。
async fn latest(
client: &reqwest::Client,
table: &MirrorTable,
vendor: JdkVendor,
major: u32,
target: AdoptiumTarget,
) -> Result<JdkRelease, JavaError> {
match vendor {
JdkVendor::Temurin => api::fetch_latest(client, major, target).await,
JdkVendor::Oracle => oracle::fetch_latest(client, table, major, target).await,
JdkVendor::Microsoft => {
microsoft::fetch_latest(client, microsoft_table(table)?, major, target).await
}
JdkVendor::Zulu => zulu::fetch_latest(client, zulu_table(table)?, major, target).await,
JdkVendor::Termux => Err(termux_archive_error()),
}
}
/// 该家的指定版本(`fetch` 的 `Some` 那一路)。
async fn pinned(
client: &reqwest::Client,
table: &MirrorTable,
vendor: JdkVendor,
major: u32,
target: AdoptiumTarget,
wanted: &str,
) -> Result<JdkRelease, JavaError> {
match vendor {
JdkVendor::Temurin => api::fetch_version(client, major, target, wanted).await,
JdkVendor::Oracle => oracle::fetch_version(client, table, major, target, wanted).await,
JdkVendor::Microsoft => {
microsoft::fetch_version(client, microsoft_table(table)?, major, target, wanted).await
}
// 上游只给最新一份(Zulu)/ 由系统包管理器定版本(Termux):明确报错,
// 绝不换成别的版本假装装成了用户点名的那一版。
JdkVendor::Zulu | JdkVendor::Termux => Err(catalog::version_pin_error(
vendor,
major,
wanted,
&[],
catalog::VersionListScope::Complete,
)),
}
}
fn microsoft_table(table: &MirrorTable) -> Result<&crate::download::MsOpenJdkTable, JavaError> {
table.microsoft_openjdk().ok_or_else(|| {
JavaError::new(
JavaErrorKind::Metadata,
"镜像清单里没有 microsoft_openjdk 表",
)
.with_hint("升级 mirrors.json 到 v3,或改用 --vendor temurin")
})
}
fn zulu_table(table: &MirrorTable) -> Result<&crate::download::ZuluTable, JavaError> {
table.zulu().ok_or_else(|| {
JavaError::new(JavaErrorKind::Metadata, "镜像清单里没有 zulu 表")
.with_hint("升级 mirrors.json 到 v3,或改用 --vendor temurin")
})
}
fn termux_archive_error() -> JavaError {
JavaError::new(
JavaErrorKind::Unsupported,
"Termux 的 JDK 由系统包管理器安装,不走归档下载链",
)
.with_hint("在 Termux 里用 `ull java install <major>`(默认走 termux 源)")
}
/// 一跳的结果。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VendorAttempt {
/// 尝试的发行版。
pub vendor: JdkVendor,
/// 是否命中(成功拿到可下载的归档)。
pub ok: bool,
/// 命中/降级的一句话原因(`"命中"` / `"没有 8 的 GA(只发 11/17/21/25)"`)。
pub detail: String,
}
impl VendorAttempt {
/// `--json` 形状。
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"vendor": self.vendor.id(),
"ok": self.ok,
"detail": self.detail,
})
}
}
/// 一条回退链的执行记录。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ChainLog {
attempts: Vec<VendorAttempt>,
}
impl ChainLog {
/// 空记录。
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// 记一次「跳过」(该家不支持这个大版本,根本没查网络)。
pub fn skip(&mut self, vendor: JdkVendor, detail: impl Into<String>) {
self.attempts.push(VendorAttempt {
vendor,
ok: false,
detail: detail.into(),
});
}
/// 记一次失败(查到了但拿不到、或下载/校验失败)。
pub fn failed(&mut self, vendor: JdkVendor, detail: impl Into<String>) {
self.attempts.push(VendorAttempt {
vendor,
ok: false,
detail: detail.into(),
});
}
/// 记一次命中。
pub fn hit(&mut self, vendor: JdkVendor, detail: impl Into<String>) {
self.attempts.push(VendorAttempt {
vendor,
ok: true,
detail: detail.into(),
});
}
/// 是否已经命中(命中后不再往下试)。
#[must_use]
pub fn hit_vendor(&self) -> Option<JdkVendor> {
self.attempts
.iter()
.find(|attempt| attempt.ok)
.map(|attempt| attempt.vendor)
}
/// 全部记录。
#[must_use]
pub fn attempts(&self) -> &[VendorAttempt] {
&self.attempts
}
/// 降级说明(「已跳过」的那些;给事件流与人读告警用)。
#[must_use]
pub fn degradations(&self) -> Vec<String> {
self.attempts
.iter()
.filter(|attempt| !attempt.ok)
.map(|attempt| format!("{}:{}", attempt.vendor.id(), attempt.detail))
.collect()
}
/// 实际尝试了几家(不含跳过)。
#[must_use]
pub fn tried(&self) -> usize {
self.attempts.len()
}
/// `--json` 形状。
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"hit": self.hit_vendor().map(JdkVendor::id),
"attempts": self.attempts.iter().map(VendorAttempt::to_json).collect::<Vec<_>>(),
})
}
}
#[cfg(test)]
mod tests {
use super::ChainLog;
use crate::game::java::vendor::JdkVendor;
#[test]
fn chain_skips_unsupported_majors_and_records_the_reason() {
let mut log = ChainLog::new();
log.skip(JdkVendor::Oracle, "不支持 8(Oracle archive 从 9 起)");
log.skip(JdkVendor::Microsoft, "没有 8 的 GA(只发 11/17/21/25)");
log.hit(JdkVendor::Temurin, "命中 8.0.504+1");
assert_eq!(log.hit_vendor(), Some(JdkVendor::Temurin));
let degradations = log.degradations();
assert_eq!(degradations.len(), 2);
assert!(degradations[1].contains("microsoft"));
assert_eq!(log.attempts().len(), 3);
let json = log.to_json();
assert_eq!(json["hit"], "temurin");
assert_eq!(json["attempts"][0]["vendor"], "oracle");
}
#[test]
fn failure_reasons_are_kept_for_the_event_stream() {
let mut log = ChainLog::new();
log.failed(JdkVendor::Temurin, "所有镜像都 4xx");
log.hit(JdkVendor::Oracle, "命中 21.0.2");
assert_eq!(
log.degradations(),
vec!["temurin:所有镜像都 4xx".to_string()],
"降级原因必须原样可读(用户要能看懂为什么换了发行版)"
);
assert_eq!(log.tried(), 2);
}
}