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
+18 -6
View File
@@ -3,7 +3,7 @@ use crate::sys::{
ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG,
EXIT_PROG_TERMINATED, execute_elevated,
};
use mirror_core::{Layout, LuaRuntime, register_basics};
use mirror_core::{Layout, LuaRuntime, TargetConfig, TargetExecutionError, register_basics};
use std::env;
use std::ffi::OsString;
use std::process::exit;
@@ -45,14 +45,26 @@ pub fn run(layout: &Layout) -> i32 {
return EXIT_FAILED_LOAD_SHIM;
}
};
// 1. 提取生效路径,缺失版本时直接抛出 UnselectedVersion
let target_path = match mr.target.executable_path() {
Some(path) => path,
None => {
warn!(
"[mirror] 工具 '{}' 当前未指定生效版本。\n提示: 请先运行 'mr use {}' 选择版本。",
target_name, target_name
);
return EXIT_FAILED_LOAD_SHIM;
}
};
// 3. 解析调用参数与代理 Mirror 配置
let calling_args: Vec<_> = env::args_os().skip(1).collect();
let combined_args = mr.args.iter().chain(calling_args.iter());
// 构建 Command:复用 ShimConfig::to_command(含 args/env 注入),避免重复逻辑
let mut cmd = mr.to_command(combined_args);
let mut cmd = mr.to_command(target_path, combined_args);
let mut child = match cmd.spawn() {
Ok(v) => v,
@@ -60,12 +72,12 @@ pub fn run(layout: &Layout) -> i32 {
// 提权回退时需要完整参数:配置默认参数 + 调用方透传参数
let elevated_args: Vec<OsString> = cmd.get_args().map(|s| s.to_os_string()).collect();
exit(execute_elevated(&mr.target, &elevated_args, Some(&mr.env)))
exit(execute_elevated(target_path, &elevated_args, Some(&mr.env)))
}
Err(e) => {
error!(
"启动目标程序 [{}] 时发生错误: {}",
mr.target.to_string_lossy(),
target_path.to_string_lossy(),
e
);
return EXIT_FAILED_SPAWN_PROG;
@@ -77,7 +89,7 @@ pub fn run(layout: &Layout) -> i32 {
Err(e) => {
error!(
"等待目标程序 [{}] 执行完毕时发生错误: {}",
mr.target.to_string_lossy(),
target_path.to_string_lossy(),
e
);
EXIT_FAILED_WAIT_PROG
+13 -9
View File
@@ -1,13 +1,13 @@
use crate::validators::LuaValidator;
use anyhow::{Context, Result, bail};
use mirror_core::validate_command_name;
use mirror_core::{Layout, LuaRuntime};
use mirror_core::{conversion_err, validation_error};
use mirror_core::{Layout, LuaRuntime, TargetConfig};
use mirror_core::{TargetExecutionError, conversion_err, validation_error};
use mlua::{FromLua, Lua, Table, Value};
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{debug, trace};
@@ -60,7 +60,7 @@ impl FromLua for MirrorRegistry {
#[derive(Debug, Clone, Default)]
pub struct MirrorTarget {
/// 目标程序可执行文件路径
pub target: PathBuf,
pub target: TargetConfig,
/// 默认预设参数列表
pub args: Vec<OsString>,
/// 参数别名映射表(用于展开以 mr: 为前缀的别名)
@@ -104,12 +104,13 @@ impl MirrorTarget {
expanded_args
}
pub fn to_command<I, S>(&self, combined_args: I) -> Command
pub fn to_command<P, I, S>(&self, target: P, combined_args: I) -> Command
where
P: AsRef<Path>,
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
let mut cmd = Command::new(&self.target);
let mut cmd = Command::new(target.as_ref());
// 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名
let final_args = self.resolve_args(combined_args);
@@ -128,7 +129,7 @@ impl MirrorTarget {
}
impl FromLua for MirrorTarget {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
// 脚本返回必须是一个 Table 变体
let table = match value {
Value::Table(t) => t,
@@ -143,9 +144,12 @@ impl FromLua for MirrorTarget {
// 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误)
let target = match table.get::<Option<Value>>("target")? {
None | Some(Value::Nil) => {
return Err(validation_error("缺少必填字段 target(应为字符串路径)"));
return Err(validation_error(
"缺少必填字段 target(应为字符串路径或多版本配置表)",
));
}
Some(target_val) => LuaValidator::parse_target(&target_val)?,
// Some(target_val) => LuaValidator::parse_target(&target_val)?,
Some(target_val) => TargetConfig::from_lua(target_val, lua)?,
};
// 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
+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 段