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
+39
View File
@@ -1,3 +1,4 @@
use std::fmt;
use std::path::PathBuf;
use thiserror::Error;
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
@@ -89,3 +90,41 @@ pub enum Error {
#[error("用户自定义扩展错误: {0}")]
Other(#[source] Box<dyn std::error::Error + Send + Sync>),
}
/// 将 MirrorTarget 构建为 Command 时可能触发的阻断性错误
#[derive(Debug, Clone)]
pub enum TargetExecutionError {
/// 多版本工具未激活版本 (current == None)
UnselectedVersion {
tool: String,
},
/// 目标可执行文件物理不存在
NotFound {
tool: String,
path: PathBuf,
},
}
impl fmt::Display for TargetExecutionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnselectedVersion { tool } => {
write!(
f,
"工具 '{}' 当前未指定有效版本。\n提示: 请先运行 'mr use {}' 选择版本。",
tool, tool
)
}
Self::NotFound { tool, path } => {
write!(
f,
"工具 '{}' 的目标程序不存在: {:?}",
tool, path
)
}
}
}
}
impl std::error::Error for TargetExecutionError {}
+3 -1
View File
@@ -5,6 +5,7 @@ mod plugins;
mod runtime;
mod utils;
mod validators;
mod model;
pub use layout::Layout;
// pub use mirror_shim::mirror::Mirror;
@@ -15,4 +16,5 @@ pub use plugins::register_basics;
pub use utils::normalize_path_for_lua;
pub use validators::validate_command_name;
// 把 Result 和 Error 提升到根命名空间,给外部带来极致便利
pub use error::{Error, Result,validation_error,conversion_err};
pub use error::{Error, Result,validation_error,conversion_err,TargetExecutionError};
pub use model::TargetConfig;
+127
View File
@@ -0,0 +1,127 @@
use std::ffi::OsString;
use mlua::{FromLua, Lua, LuaString, Value};
use std::path::PathBuf;
use crate::utils::lua_string_2_os_string;
use crate::validation_error;
use crate::validators::ensure_no_nul;
/// 目标可执行文件的定位配置(支持静态路径与多版本 Table)
#[derive(Debug, Clone)]
pub enum TargetConfig{
/// 静态单版本工具
Static(PathBuf),
/// 多版本管理
Versioned{
/// 工具主根目录(供 mr use 扫描候选版本)
home:PathBuf,
/// 版本真相源文件名(如 ".version",可选,默认 ".version")
version_file:Option<String>,
/// 当前激活版本的可执行文件完整路径(若未激活或不存在则为 None)
current:Option<PathBuf>,
}
}
impl Default for TargetConfig {
fn default() -> Self {
TargetConfig::Static(PathBuf::default())
}
}
impl TargetConfig {
/// 垫片程序(mirror-shim)专用快速通道:提取最终可执行路径
///
/// - Static: 返回自身路径
/// - Versioned: 返回 current 路径(若未激活则返回 None)
pub fn executable_path(&self) -> Option<&PathBuf> {
match self {
TargetConfig::Static(path) => Some(path),
TargetConfig::Versioned { current, .. } => current.as_ref(),
}
}
/// 管理程序(mr-cli)专用辅助方法:判断是否支持多版本
pub fn is_versioned(&self) -> bool {
matches!(self, TargetConfig::Versioned { .. })
}
}
impl FromLua for TargetConfig {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
match value {
// 分支 1:静态单版本工具 target = "..."
Value::String(s) => {
let os_str = lua_string_2_os_string(&s)?;
ensure_no_nul(&"target", &os_str)?;
Ok(TargetConfig::Static(PathBuf::from(os_str)))
}
// 分支 2:多版本管理工具 target = { home = "...", ... }
Value::Table(table) => {
// 1. home (必填字段)
let home = match table.get::<Option<Value>>("home")? {
Some(Value::String(s)) => {
let os_str = lua_string_2_os_string(&s)?;
ensure_no_nul(&"target.home", &os_str)?;
PathBuf::from(os_str)
}
None | Some(Value::Nil) => {
return Err(validation_error("多版本 target 表缺少必填字段 'home'"));
}
Some(other) => {
return Err(validation_error(format!(
"target.home 需为路径字符串,实际类型是 {}",
other.type_name()
)));
}
};
// 2. version_file (选填字段,未填或 nil 时为 None)
let version_file = match table.get::<Option<Value>>("version_file")? {
Some(Value::String(s)) => {
let os_str = lua_string_2_os_string(&s)?;
ensure_no_nul(&"target.version_file", &os_str)?;
Some(os_str.to_string_lossy().into_owned())
}
None | Some(Value::Nil) => None,
Some(other) => {
return Err(validation_error(format!(
"target.version_file 需为文件名字符串,实际类型是 {}",
other.type_name()
)));
}
};
// 3. current (选填/可为 nil,表示未激活或当前未指定版本)
let current = match table.get::<Option<Value>>("current")? {
Some(Value::String(s)) => {
let os_str = lua_string_2_os_string(&s)?;
ensure_no_nul(&"target.current", &os_str)?;
Some(PathBuf::from(os_str))
}
// Lua 侧 ver 为 nil 时,current 拼出的也是 nil,此时合法解析为 None
None | Some(Value::Nil) => 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!(
"target 需为有效路径字符串或多版本配置表,实际类型是 {}",
other.type_name()
))),
}
}
}
+60
View File
@@ -9,3 +9,63 @@ pub fn normalize_path_for_lua(path: &Path) -> String {
simplified.to_string_lossy().replace('\\', "/")
}
pub fn lua_string_2_os_string(s: &LuaString) -> mlua::Result<OsString> {
let raw_bytes = &s.as_bytes();
// 1. Unix 平台:直接零拷贝透传原始字节(无损支持任意编码)
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(OsStr::from_bytes(raw_bytes).to_os_string())
}
// 2. Windows 平台:优先按 UTF-8 解码,失败则按当前系统本地代码页 (ANSI/GBK) 转换
#[cfg(windows)]
{
// 先尝试标准的 UTF-8
if let Ok(utf8_str) = std::str::from_utf8(raw_bytes) {
return Ok(OsString::from(utf8_str));
}
unsafe {
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Globalization::{
CP_ACP, MB_ERR_INVALID_CHARS, MultiByteToWideChar,
};
if raw_bytes.is_empty() {
return Ok(OsString::new());
}
let len = MultiByteToWideChar(
CP_ACP,
MB_ERR_INVALID_CHARS,
raw_bytes.as_ptr(),
raw_bytes.len() as i32,
std::ptr::null_mut(),
0,
);
if len <= 0 {
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
return Err(mlua::Error::FromLuaConversionError {
from: "LuaString",
to: "OsString".to_string(),
message: Some(
format!("字符串{:?}包含无效或当前系统无法识别的编码字节", s).to_string(),
),
});
}
let mut buf = vec![0u16; len as usize];
MultiByteToWideChar(
CP_ACP,
MB_ERR_INVALID_CHARS,
raw_bytes.as_ptr(),
raw_bytes.len() as i32,
buf.as_mut_ptr(),
len,
);
Ok(OsString::from_wide(&buf))
}
}
}
+24
View File
@@ -1,3 +1,4 @@
use std::fmt;
use crate::error::validation_error;
use mlua::Value;
@@ -39,3 +40,26 @@ pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
}
Ok(name_str)
}
pub fn ensure_no_nul(context: &impl fmt::Display, os_str: &std::ffi::OsStr) -> mlua::Result<()> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
if os_str.as_bytes().contains(&0) {
return Err(conversion_error(format!(
"{} 的值不能包含 NUL 字符",
context
)));
}
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
if os_str.encode_wide().any(|c| c == 0) {
return Err(validation_error(format!(
"{} 的值不能包含 NUL 字符",
context
)));
}
}
Ok(())
}