feat(m203): CLI 三条下载命令改吃 download.* 设置(并发/重试/gh-proxy)
`ull install` / `ull java` / `ull pack` 此前各自 `DownloadEngine::new(EngineConfig::default(), …)`: 用户在设置里配的 `download.concurrency` / `retry_limit` / `gh_proxy` 一概不生效;M193 把出厂并发 从 64 改成 8 之后,它们还静默跟着降了。现在三处都经 `crates/cli/src/download.rs::engine(ctx)`: - core:`shared/tuned.rs` 新增 `engine_with_tuning(cache_root, tune)` = `set_tuning` + `engine_for`, 由 `shared.rs` 重导出;`engine_for` 的签名与行为一字未改(presenter 的两步语义原样保留)。 放子模块是为了不动 `engine.rs`(那是 400 行零余量),`shared.rs` 仍 398 行。 - CLI:`download::tuning(ctx)` 用 `SettingStore::load(ctx.launcher_config(), launcher_schema())` 读 三个键(与 `ull config set` 同一份文件),默认值/夹取/回落在 `DownloadTuning::from_settings`; 键缺失或坏值静默回落出厂值,`engine()` 没有错误通道,安装不会因此中断。 - 顺带:`ull update` 复用 `install::build_engine`,也随之吃到设置。 - 测试:`crates/cli/src/download/tests.rs` 三条命令各一条(读 `config().max_concurrency` 与 `concurrency_probe()` 的真实值)+ 一条「缺失/越界/负数/类型不对 ⇒ 静默回落出厂值」; `shared/tuned.rs` 两条守住「灌调优与取引擎是一步」。变异实测:pack 换回裸 `EngineConfig::default()` ⇒ 只有 pack 那条红(left 8 / right 2)。
This commit is contained in:
1 parent
c97630d59e
commit
b31ca636d1
9 files changed
+368
-81
No files matched your search
@@ -19,12 +19,7 @@ mod vanilla;
|
||||
use std::sync::Arc;
|
||||
|
||||
use unlucky_core::addon::loader::LoaderKind;
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
|
||||
use unlucky_core::download::region_system::detect_signals;
|
||||
use unlucky_core::download::{
|
||||
ContentCache, DownloadEngine, DownloadErrorKind, EngineConfig, HealthRegistry, MirrorTable,
|
||||
};
|
||||
use unlucky_core::download::{DownloadEngine, DownloadErrorKind};
|
||||
use unlucky_core::game::install::{InstallError, InstallReport, Registration};
|
||||
|
||||
use crate::cli::InstallArgs;
|
||||
@@ -78,27 +73,10 @@ pub(super) fn register_instance(
|
||||
.map_err(|error| CliError::user("instance_register", format!("登记实例失败:{error}")))
|
||||
}
|
||||
|
||||
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,地区判定走 24h 缓存 + 环境信号。
|
||||
///
|
||||
/// 地区判定**不做网络探测**(P2 首版;探测开关属设置层接线),
|
||||
/// 因此中文环境默认镜像优先、其它环境直连——与 D7 的"不探测时"约定一致。
|
||||
pub(super) fn build_engine(ctx: &Ctx) -> DownloadEngine {
|
||||
let layout = ctx.layout();
|
||||
let cache_root = layout.cache();
|
||||
let now = now_ms();
|
||||
let region_path = cache_root.join("region.json");
|
||||
let region = load_cached(®ion_path, now).unwrap_or_else(|| {
|
||||
let profile = decide_from(&detect_signals(), now);
|
||||
let _ = store_cached(®ion_path, &profile);
|
||||
profile
|
||||
});
|
||||
DownloadEngine::new(
|
||||
EngineConfig::default(),
|
||||
Arc::new(MirrorTable::builtin()),
|
||||
Arc::new(region),
|
||||
Arc::new(HealthRegistry::open_in(&cache_root, now)),
|
||||
Arc::new(ContentCache::open(&cache_root)),
|
||||
)
|
||||
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,装配参数来自设置(并发 / 重试 / gh-proxy,
|
||||
/// 读数口径见 [`crate::download`])——不再用裸 `EngineConfig::default()`(M203)。
|
||||
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
crate::download::engine(ctx)
|
||||
}
|
||||
|
||||
/// 同名但绑定别的版本 → 冲突;同名同版本视为重装(复用实例,允许)。
|
||||
|
||||
@@ -17,12 +17,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
|
||||
use unlucky_core::download::region_system::detect_signals;
|
||||
use unlucky_core::download::{
|
||||
ContentCache, DownloadEngine, EngineConfig, HealthRegistry, MirrorTable, RegionProfile,
|
||||
};
|
||||
use unlucky_core::download::{DownloadEngine, RegionProfile};
|
||||
use unlucky_core::game::java::{
|
||||
self, location, vendor, InstallRequest, JavaOrigin, JavaRuntime, JdkVendor, LocationMode,
|
||||
DEFAULT_MAJOR,
|
||||
@@ -312,24 +307,10 @@ fn install_root_of(runtime: &JavaRuntime) -> Option<String> {
|
||||
.map(|dir| dir.display().to_string())
|
||||
}
|
||||
|
||||
/// 建下载引擎:内容寻址缓存落 `.ULL/cache/`,地区判定走 24h 缓存 + 环境信号。
|
||||
fn build_engine(ctx: &Ctx) -> DownloadEngine {
|
||||
let layout = ctx.layout();
|
||||
let cache_root = layout.cache();
|
||||
let now = now_ms();
|
||||
let region_path = cache_root.join("region.json");
|
||||
let region = load_cached(®ion_path, now).unwrap_or_else(|| {
|
||||
let profile = decide_from(&detect_signals(), now);
|
||||
let _ = store_cached(®ion_path, &profile);
|
||||
profile
|
||||
});
|
||||
DownloadEngine::new(
|
||||
EngineConfig::default(),
|
||||
Arc::new(MirrorTable::builtin()),
|
||||
Arc::new(region),
|
||||
Arc::new(HealthRegistry::open_in(&cache_root, now)),
|
||||
Arc::new(ContentCache::open(&cache_root)),
|
||||
)
|
||||
/// 建下载引擎:装配参数来自设置(并发 / 重试 / gh-proxy,读数口径见 [`crate::download`]),
|
||||
/// 引擎本体与 `install` / `pack` 共用同一个(core 按缓存根共享)。
|
||||
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
crate::download::engine(ctx)
|
||||
}
|
||||
|
||||
/// JDK 下载/解压错误 → CLI 错误(保留稳定码与提示)。
|
||||
|
||||
@@ -30,12 +30,7 @@ use unlucky_core::addon::import::{
|
||||
ImportReport, ImportRequest, ImportStatus,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
|
||||
use unlucky_core::download::region_system::detect_signals;
|
||||
use unlucky_core::download::{
|
||||
ContentCache, DownloadEngine, EngineConfig, HealthRegistry, MirrorTable,
|
||||
};
|
||||
use unlucky_core::download::DownloadEngine;
|
||||
|
||||
use crate::cli::PackImportArgs;
|
||||
use crate::context::Ctx;
|
||||
@@ -286,24 +281,10 @@ fn count_action(report: &ImportReport, action: FileAction) -> usize {
|
||||
.count()
|
||||
}
|
||||
|
||||
/// 与 `install` 共用同一套引擎构造(镜像 / 地区 / 健康度 / 内容缓存)。
|
||||
fn build_engine(ctx: &Ctx) -> DownloadEngine {
|
||||
let layout = ctx.layout();
|
||||
let cache_root = layout.cache();
|
||||
let now = now_ms();
|
||||
let region_path = cache_root.join("region.json");
|
||||
let region = load_cached(®ion_path, now).unwrap_or_else(|| {
|
||||
let profile = decide_from(&detect_signals(), now);
|
||||
let _ = store_cached(®ion_path, &profile);
|
||||
profile
|
||||
});
|
||||
DownloadEngine::new(
|
||||
EngineConfig::default(),
|
||||
Arc::new(MirrorTable::builtin()),
|
||||
Arc::new(region),
|
||||
Arc::new(HealthRegistry::open_in(&cache_root, now)),
|
||||
Arc::new(ContentCache::open(&cache_root)),
|
||||
)
|
||||
/// 与 `install` / `java` 共用同一套引擎装配(镜像 / 地区 / 健康度 / 内容缓存),
|
||||
/// 参数来自设置:读数口径见 [`crate::download`]。
|
||||
pub(crate) fn build_engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
crate::download::engine(ctx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
//! 去掉 `--dest` 会把它真改名换掉(`crates/cli/tests/update_e2e_test.rs` 的注释写了这件事)。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use unlucky_core::update::{default_source, Applied, CheckOutcome, Job, UpdateError, Version};
|
||||
@@ -57,7 +56,7 @@ fn current_version() -> Result<Version, CliError> {
|
||||
/// 造一个内核 job(源 = GitHub / `ULL_UPDATE_DIR`,暂存 = 数据根缓存,目标 = 本进程 exe)。
|
||||
fn job(ctx: &Ctx, dest: Option<PathBuf>) -> Job {
|
||||
let engine = super::install::build_engine(ctx);
|
||||
let job = Job::for_process(default_source(Arc::new(engine)), &ctx.layout().cache());
|
||||
let job = Job::for_process(default_source(engine), &ctx.layout().cache());
|
||||
match dest {
|
||||
Some(dest) => job.with_dest(dest),
|
||||
None => job,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! CLI 侧的下载引擎:把 CLI 自己的 `launcher.json` 接到 core 的下载调优上(M203)。
|
||||
//!
|
||||
//! 三条下载命令(`install` / `java` / `pack`)此前各自 `DownloadEngine::new(EngineConfig::default(), …)`:
|
||||
//! 用户在设置里配的 `download.concurrency` / `download.retry_limit` / `download.gh_proxy` 一概不生效。
|
||||
//! M193 把出厂并发从 64 改成 8 之后,它们还**静默**跟着降了——「设置项说假话」的同期病灶
|
||||
//! (`docs/download-settings.md` §3.1 记着这三条 ❌)。
|
||||
//!
|
||||
//! 现在三处一律经 [`engine`] 取引擎(进程内共享,见 `core::download::shared`):
|
||||
//!
|
||||
//! ```text
|
||||
//! <数据根>/config/launcher.json(与 `ull config set` 同一份) → setting store → DownloadTuning
|
||||
//! (默认值/夹取/回落在 core)
|
||||
//! → shared::engine_with_tuning(<cache 根>, tuning) → 引擎的 Semaphore / attempts_for()
|
||||
//! ```
|
||||
//!
|
||||
//! **回落是静默的**:`SettingStore::load` 永不失败(坏值只进 `problems()`);类型不对 → `None`,
|
||||
//! 越界 → `DownloadTuning::from_settings` 的区间过滤。两种情况都落成出厂值,**不报错、不中断安装**。
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use unlucky_core::download::config::{
|
||||
DownloadTuning, KEY_CONCURRENCY, KEY_GH_PROXY, KEY_RETRY_LIMIT,
|
||||
};
|
||||
use unlucky_core::download::{shared, DownloadEngine};
|
||||
use unlucky_core::setting::keys::launcher_schema;
|
||||
use unlucky_core::setting::store::SettingStore;
|
||||
|
||||
use crate::context::Ctx;
|
||||
|
||||
/// 从 CLI 自己的 setting store 读下载调优(`launcher.json` 的三个 `download.*` 键)。
|
||||
///
|
||||
/// 读的是 `ctx.launcher_config()`——与 `ull config set download.concurrency` 的落点同一份文件
|
||||
/// (设置键的落点分流见 [`Ctx::setting_path_for`])。gh-proxy 空串 = 跟随内置默认,**不是**关。
|
||||
#[must_use]
|
||||
pub(crate) fn tuning(ctx: &Ctx) -> DownloadTuning {
|
||||
let store = SettingStore::load(ctx.launcher_config(), launcher_schema());
|
||||
let doc = store.doc();
|
||||
let gh_proxy = doc
|
||||
.effective_str(KEY_GH_PROXY)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
DownloadTuning::from_settings(
|
||||
doc.effective_i64(KEY_CONCURRENCY),
|
||||
doc.effective_i64(KEY_RETRY_LIMIT),
|
||||
Some(gh_proxy.as_str()),
|
||||
)
|
||||
}
|
||||
|
||||
/// 建该数据根的下载引擎:参数来自 [`tuning`],引擎本体由 core 按缓存根共享。
|
||||
#[must_use]
|
||||
pub(crate) fn engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
shared::engine_with_tuning(ctx.layout().cache(), tuning(ctx))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,203 @@
|
||||
//! [`super`] 的单测(M203):`launcher.json` 的 `download.*` 真的进到三条命令拿到的引擎里。
|
||||
//!
|
||||
//! 断言读的是**引擎的真实字段**——`config().max_concurrency` 与 `Semaphore` 的
|
||||
//! `available_permits()`(`concurrency_probe`)——不是「函数被调用过」:后者在回落写错、
|
||||
//! 或者命令又自建了一个裸引擎时照样绿。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use unlucky_core::download::config::{
|
||||
DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY,
|
||||
};
|
||||
use unlucky_core::download::DownloadEngine;
|
||||
use unlucky_core::setting::keys::launcher_schema;
|
||||
use unlucky_core::setting::store::SettingStore;
|
||||
|
||||
use crate::cli::Globals;
|
||||
use crate::context::Ctx;
|
||||
|
||||
/// 一次性临时目录(cli 没有 dev-dependency 的 tempfile;与 `commands/config/tests.rs` 同一手法)。
|
||||
struct TempDir(PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
fn new(tag: &str) -> Self {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |duration| duration.as_nanos());
|
||||
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ull-cli-download-{tag}-{}-{seq}-{nanos}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&path).expect("建临时目录");
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 数据根钉在临时目录里:`launcher.json` 落在 `<dir>/.ULL/config/launcher.json`。
|
||||
fn ctx_at(dir: &TempDir) -> Ctx {
|
||||
let globals = Globals {
|
||||
data_dir: Some(dir.path().to_path_buf()),
|
||||
..Globals::default()
|
||||
};
|
||||
Ctx::resolve(&globals).expect("解析数据根")
|
||||
}
|
||||
|
||||
/// 直接写 `launcher.json`——**与 `ull config set` 的落点是同一份文件**(`ctx.launcher_config()`),
|
||||
/// 所以这条用例测的就是用户手改/命令改之后 CLI 读到的值。
|
||||
fn write_launcher_config(ctx: &Ctx, document: Value) {
|
||||
let path = ctx.launcher_config();
|
||||
std::fs::create_dir_all(path.parent().expect("配置目录")).expect("建配置目录");
|
||||
std::fs::write(&path, serde_json::to_vec(&document).expect("序列化"))
|
||||
.expect("写 launcher.json");
|
||||
}
|
||||
|
||||
/// 「引擎真的按这个并发跑」:配置字段 + 闸门许可数,两处都读真实值。
|
||||
fn assert_concurrency(engine: &DownloadEngine, expected: usize, case: &str) {
|
||||
assert_eq!(
|
||||
engine.config().max_concurrency,
|
||||
expected,
|
||||
"{case}:`EngineConfig` 里的并发"
|
||||
);
|
||||
assert_eq!(
|
||||
engine.concurrency_probe(),
|
||||
expected,
|
||||
"{case}:`Semaphore` 真实许可数"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`install` 吃设置**:`download.concurrency` 改了就变;同一次进程里再改一次也立刻变(不是只读一次)。
|
||||
///
|
||||
/// 抓得住的变异:`commands::install::build_engine` 换回裸 `EngineConfig::default()` ⇒ 3 / 5 两处红。
|
||||
/// 抓不住的:真实下载时「只有 3 条连接在飞」的时序(离线验不了,见报告「没验的部分」)。
|
||||
#[test]
|
||||
fn install_engine_takes_the_concurrency_from_the_setting_store() {
|
||||
let dir = TempDir::new("install");
|
||||
let ctx = ctx_at(&dir);
|
||||
// 没有 launcher.json = 没配过 ⇒ 出厂值(这也是「回落」的第一档)。
|
||||
assert_concurrency(
|
||||
&crate::commands::install::build_engine(&ctx),
|
||||
DEFAULT_MAX_CONCURRENCY,
|
||||
"未配置",
|
||||
);
|
||||
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 3 } }));
|
||||
assert_concurrency(
|
||||
&crate::commands::install::build_engine(&ctx),
|
||||
3,
|
||||
"concurrency=3",
|
||||
);
|
||||
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 5 } }));
|
||||
assert_concurrency(
|
||||
&crate::commands::install::build_engine(&ctx),
|
||||
5,
|
||||
"concurrency=5",
|
||||
);
|
||||
}
|
||||
|
||||
/// **`java` 吃设置**:并发与每个源的尝试次数都进引擎。
|
||||
///
|
||||
/// 抓得住的变异:`commands::java::build_engine` 换回裸 `EngineConfig::default()` ⇒ 探针停在 8、次数停在 3,红。
|
||||
#[test]
|
||||
fn java_engine_takes_the_concurrency_and_retry_from_the_setting_store() {
|
||||
let dir = TempDir::new("java");
|
||||
let ctx = ctx_at(&dir);
|
||||
write_launcher_config(
|
||||
&ctx,
|
||||
json!({ "download": { "concurrency": 3, "retry_limit": 4 } }),
|
||||
);
|
||||
|
||||
let engine = crate::commands::java::build_engine(&ctx);
|
||||
assert_concurrency(&engine, 3, "concurrency=3");
|
||||
assert_eq!(
|
||||
engine.config().max_attempts_per_source,
|
||||
4,
|
||||
"retry_limit=4 要真的落到引擎上(出厂是 3)"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`pack` 吃设置**(三条命令各有一条独立断言:把任意一处改回裸引擎,只有那一条红)。
|
||||
///
|
||||
/// 抓得住的变异:`commands::pack::build_engine` 换回裸 `EngineConfig::default()` ⇒ 2 处红。
|
||||
#[test]
|
||||
fn pack_engine_takes_the_concurrency_from_the_setting_store() {
|
||||
let dir = TempDir::new("pack");
|
||||
let ctx = ctx_at(&dir);
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 2 } }));
|
||||
assert_concurrency(
|
||||
&crate::commands::pack::build_engine(&ctx),
|
||||
2,
|
||||
"concurrency=2",
|
||||
);
|
||||
}
|
||||
|
||||
/// **反向断言**:键缺失 / 越界 / 负数 / 类型不对 ⇒ 回落出厂值,而且回落是**静默的**。
|
||||
///
|
||||
/// 「静默」在这里是结构性的:`crate::download::engine` 的返回类型没有错误通道,坏设置只进
|
||||
/// `SettingStore::problems()`,调用方(安装流程)连一个 `?` 都不用加——`ull install` 不会因为一条
|
||||
/// 写坏的设置而中断,只是按出厂值跑。
|
||||
///
|
||||
/// 抓得住的变异:删掉 `DownloadTuning::from_settings` 的区间过滤 ⇒ 9_999 那条把许可数变成 64,红;
|
||||
/// 把 CLI 的读数改成 `expect`/报错 ⇒ 坏值文件这几档 panic 或编译不过,红。
|
||||
/// 抓不住的:`problems()` 的内容与措辞(那是 core 的 `setting` 模块,本用例只借它证明文件真的坏)。
|
||||
#[test]
|
||||
fn missing_or_illegal_values_fall_back_to_the_factory_defaults_silently() {
|
||||
let dir = TempDir::new("fallback");
|
||||
let ctx = ctx_at(&dir);
|
||||
assert_eq!(
|
||||
super::tuning(&ctx),
|
||||
DownloadTuning::default(),
|
||||
"没有 launcher.json 就该是出厂调优"
|
||||
);
|
||||
|
||||
for (case, document) in [
|
||||
(
|
||||
"越界",
|
||||
json!({ "download": { "concurrency": 9_999, "retry_limit": 99 } }),
|
||||
),
|
||||
(
|
||||
"负数",
|
||||
json!({ "download": { "concurrency": -4, "retry_limit": -1 } }),
|
||||
),
|
||||
(
|
||||
"类型不对",
|
||||
json!({ "download": { "concurrency": "八", "retry_limit": "很多" } }),
|
||||
),
|
||||
] {
|
||||
write_launcher_config(&ctx, document);
|
||||
// 先证明这份文件**真的**是坏的(否则「回落」只是键不存在,用例就成了空断言)。
|
||||
let store = SettingStore::load(ctx.launcher_config(), launcher_schema());
|
||||
assert!(
|
||||
!store.problems().is_empty(),
|
||||
"{case}:这份文件该被判为有问题"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::tuning(&ctx),
|
||||
DownloadTuning::default(),
|
||||
"{case}:坏值必须回落出厂调优"
|
||||
);
|
||||
let engine = crate::commands::install::build_engine(&ctx);
|
||||
assert_concurrency(&engine, DEFAULT_MAX_CONCURRENCY, case);
|
||||
assert_eq!(
|
||||
engine.config().max_attempts_per_source,
|
||||
DEFAULT_MAX_ATTEMPTS_PER_SOURCE,
|
||||
"{case}:尝试次数也回落出厂值"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
//! 分层:
|
||||
//! - [`cli`]:手写参数解析(不引 clap)+ 命令模型 + 帮助文案;
|
||||
//! - [`commands`]:每个子命令一个函数,输入 [`context::Ctx`],输出 [`output::Report`];
|
||||
//! - [`download`]:CLI 侧的下载引擎(设置 store → core 调优,`install`/`java`/`pack` 共用);
|
||||
//! - [`context`]:数据根解析(三模式 / `--data-dir` 覆盖)与目录视图;
|
||||
//! - [`os`]:系统摘要与 D19/D21 支持级别(事实来自 core::platform::os_info,这里只做呈现);
|
||||
//! - [`output`]:人类可读 + `--json` 双格式输出;
|
||||
@@ -19,6 +20,7 @@
|
||||
pub mod cli;
|
||||
pub mod commands;
|
||||
pub mod context;
|
||||
pub mod download;
|
||||
pub mod error;
|
||||
pub mod os;
|
||||
pub mod output;
|
||||
|
||||
Reference in new issue
Block a user