feat: 支持多版本 target 配置并优化执行前校验

- 将 MirrorTarget.target 改造为 TargetConfig 枚举(支持 Static 与 Versioned)
- TargetConfig 独立实现 FromLua,解耦反序列化与业务校验
- to_command 调整为显式接收执行路径,简化构建逻辑
- 增加未选择版本时的前置拦截与友好提示
This commit is contained in:
2026-09-10 20:15:37 +08:00
parent 38dabd2ace
commit 0bba69fd51
8 changed files with 355 additions and 22 deletions
+71 -6
View File
@@ -1,5 +1,5 @@
use anyhow::{Context, Result, anyhow};
use mirror_core::validation_error;
use mirror_core::{TargetConfig, validation_error};
use mlua::{LuaString, Table, Value};
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
@@ -212,17 +212,79 @@ impl LuaValidator {
}
impl LuaValidator {
/// 解析并校验 `target`
pub fn parse_target(value: &Value) -> mlua::Result<PathBuf> {
pub fn parse_target(value: &Value) -> mlua::Result<TargetConfig> {
let ctx = Self::Target;
match value {
Value::String(s) => {
let os_str = lua_string_2_os_string(&s)?;
Self::ensure_no_nul(&ctx, &os_str)?;
Ok(PathBuf::from(os_str))
Ok(TargetConfig::Static(PathBuf::from(os_str)))
}
Value::Nil => Err(validation_error("缺少必填字段 target(应为字符串路径)")),
// 分支 2:多版本动态工具 target = { home = "...", version_file = "...", current = "..." }
Value::Table(t) => {
// 1. 解析 home(必填)
let home_val = t.get::<Value>("home")?;
let home = match home_val {
Value::String(ref s) => {
let os_str = lua_string_2_os_string(s)?;
Self::ensure_no_nul(&"target.home", &os_str)?;
PathBuf::from(os_str)
}
Value::Nil => {
return Err(validation_error("多版本 target 表缺少必填字段 'home'"));
}
other => {
return Err(validation_error(format!(
"target.home 需为有效路径字符串,实际类型是 {}",
other.type_name()
)));
}
};
// 2. 解析 version_file(选填,默认为 None,后续业务可 fallback 到 .version)
let version_file = match t.get::<Option<Value>>("version_file")? {
Some(Value::String(ref s)) => {
let os_str = lua_string_2_os_string(s)?;
Self::ensure_no_nul(&"target.version_file", &os_str)?;
Some(os_str.to_string_lossy().into_owned())
}
Some(Value::Nil) | None => None,
Some(other) => {
return Err(validation_error(format!(
"target.version_file 应为文件名字符串,实际类型是 {}",
other.type_name()
)));
}
};
// 3. 解析 current(选填/可为 nil:未激活或找不到版本时允许为 None)
let current = match t.get::<Option<Value>>("current")? {
Some(Value::String(ref s)) => {
let os_str = lua_string_2_os_string(s)?;
Self::ensure_no_nul(&"target.current", &os_str)?;
Some(PathBuf::from(os_str))
}
// Lua 侧 ver 为 nil 时,current 计算结果即为 nil,合法放行
Some(Value::Nil) | None => None,
Some(other) => {
return Err(validation_error(format!(
"target.current 需为有效路径字符串或 nil,实际类型是 {}",
other.type_name()
)));
}
};
Ok(TargetConfig::Versioned {
home,
version_file,
current,
})
}
Value::Nil => Err(validation_error(
"缺少必填字段 target(应为字符串路径或多版本配置表)",
)),
other => Err(validation_error(format!(
"{} 需为有效的路径且类型必须是字符串,实际类型是 {}",
"{} 需为有效的路径且类型必须是字符串或多版本配置表,实际类型是 {}",
ctx,
other.type_name()
))),
@@ -534,7 +596,10 @@ mod tests {
"#,
)
.unwrap();
assert_eq!(cfg.target, PathBuf::from("C:/tools/git.exe"));
assert_eq!(
cfg.target.executable_path().unwrap().to_path_buf(),
PathBuf::from("C:/tools/git.exe")
);
// assert_eq!(cfg.args, vec!["--no-pager", "2"]);
assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home"));
// PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段