Author SHA1 Message Date
CNWei 0bba69fd51 feat: 支持多版本 target 配置并优化执行前校验
- 将 MirrorTarget.target 改造为 TargetConfig 枚举(支持 Static 与 Versioned)
- TargetConfig 独立实现 FromLua,解耦反序列化与业务校验
- to_command 调整为显式接收执行路径,简化构建逻辑
- 增加未选择版本时的前置拦截与友好提示
2026-09-10 20:15:37 +08:00
CNWei 38dabd2ace refactor(core,shim): 统一基于 thiserror 的错误体系
- 移除 anyhow,使用 thiserror 定义强类型 Error 枚举
- 支持 #[from] 自动转换 mlua::Error,关键路径保留源码与上下文
- LuaRuntimeBuilder 采用惰性错误收集,简化链式初始化调用
- MirrorRegistry 实现规范化 FromLuaConversionError 错误转换
2026-09-10 14:33:54 +08:00
25 changed files with 1031 additions and 494 deletions
Generated
+1
View File
@@ -282,6 +282,7 @@ dependencies = [
"dunce", "dunce",
"mlua", "mlua",
"serde", "serde",
"thiserror",
"tinyjson", "tinyjson",
"tracing", "tracing",
"tracing-appender", "tracing-appender",
+4 -14
View File
@@ -1,37 +1,27 @@
-- commands/commands.lua -- commands/commands.lua
function command(mod_path)
-- 无论输入 "a/b/c" 还是 "a\b\c",统一替换为标准的 "a.b.c"
local normalized = mod_path:gsub("[/\\]", ".")
return {
__is_lazy_command = true,
module = normalized
}
end
return { return {
-- 1. 单文件子命令 (commands/use.lua) -- 1. 单文件子命令 (commands/use.lua)
["use"] = { ["use"] = {
description = "Switch the active version of a tool", description = "Switch the active version of a tool",
handler = command("use") module = mr.command("use")
}, },
-- 2. 单文件子命令 (commands/list.lua) -- 2. 单文件子命令 (commands/list.lua)
["list"] = { ["list"] = {
description = "List available and currently activated versions", description = "List available and currently activated versions",
handler = command("list") module = mr.command("list")
}, },
-- 3. 复杂多文件子命令 (加载 commands/log/init.lua) -- 3. 复杂多文件子命令 (加载 commands/log/init.lua)
["log"] = { ["log"] = {
description = "View or configure mirror runtime logs", description = "View or configure mirror runtime logs",
handler = command("log") module = mr.command("log")
}, },
-- 4. 支持别名快捷映射 (例如 mr ls 等价于 mr list) -- 4. 支持别名快捷映射 (例如 mr ls 等价于 mr list)
["ls"] = { ["ls"] = {
description = "Alias for list", description = "Alias for list",
handler = command("list") module = mr.command("list")
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@
name = "mirror-cli" name = "mirror-cli"
version = { workspace = true } version = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
description = "Mirror CLI Manager"
# 显式指定生成的二进制文件名 # 显式指定生成的二进制文件名
[[bin]] [[bin]]
name = "mr" name = "mr"
-40
View File
@@ -1,40 +0,0 @@
use crate::cli;
use crate::commands::{builtin, dynamic::MrCommand};
use anyhow::{Context, Result, bail};
use tracing::debug;
use mirror_core::{init_logging_from, Layout, LuaRuntime};
pub fn run() -> Result<()> {
let exe = std::env::current_exe().context("获取代理程序路径失败")?;
let layout = Layout::from(&exe)?;
let _guard = init_logging_from(&layout);
debug!("=== 管理程序启动 ===");
let registry_file = layout.base_dir.join("commands").join("commands.lua");
let runtime = LuaRuntime::new(&layout)?;
let commands: MrCommand = runtime
.eval_script(&registry_file)
.with_context(|| format!("加载命令配置失败: {}", registry_file.display()))?;
let app = cli::build(commands.clone());
let matches = app.get_matches();
match matches.subcommand() {
// 分支 A: 原生 Rust 实现的命令
Some(("log", sub_m)) => builtin::log_handle(&layout, sub_m),
// 分支 B: 动态 Lua 注册的命令
Some((cmd_name, sub_m)) => {
let sub_cmd = commands
.get(cmd_name)
.with_context(|| format!("未找到子命令 '{cmd_name}'"))?;
let raw_args = cli::extract_raw_args(sub_m);
runtime.require_and_run(&sub_cmd.module, raw_args)
}
// 分支 C: 未输入子命令(通常已被 Clap 的 arg_required_else_help 拦截)
None => Ok(()),
}
}
+8 -16
View File
@@ -5,8 +5,8 @@ use std::ffi::OsString;
// 引入强类型枚举 // 引入强类型枚举
pub fn build(dynamic_cmds: MrCommand) -> Command { pub fn build(dynamic_cmds: MrCommand) -> Command {
let mut app = Command::new("mr") let mut app = Command::new("mr")
.about("Mirror CLI Manager") .about(env!("CARGO_PKG_DESCRIPTION"))
.version("0.1.0") .version(env!("CARGO_PKG_VERSION"))
.arg_required_else_help(true) .arg_required_else_help(true)
.subcommand(log_subcommand()); .subcommand(log_subcommand());
@@ -28,28 +28,20 @@ pub fn build(dynamic_cmds: MrCommand) -> Command {
fn log_subcommand() -> Command { fn log_subcommand() -> Command {
Command::new("log") Command::new("log")
.about("查看或修改 mirror.ini 日志配置") .about("查看或修改 mirror.ini 日志配置")
.arg(
Arg::new("tail")
.short('t')
.long("tail")
.value_name("LINES")
.help("显示最近的 N 行日志")
.value_parser(clap::value_parser!(usize)),
)
.arg( .arg(
Arg::new("level") Arg::new("level")
.short('l') .short('l')
.long("level") .long("level")
.value_name("LEVEL") .value_name("LEVEL")
.help("设置运行时日志级别") .help("设置日志级别 (off, error, warn, info, debug, trace)")
.value_parser(clap::value_parser!(LogLevel)), .value_parser(clap::value_parser!(LogLevel)),
) )
.arg( .arg(
Arg::new("follow") Arg::new("dir")
.short('f') .short('d')
.long("follow") .long("dir")
.help("持续跟踪日志输出 (tail -f)") .value_name("PATH")
.action(ArgAction::SetTrue), .help("设置日志存放路径(绝对路径或相对于 base_dir 的相对路径)"),
) )
} }
+47 -23
View File
@@ -1,34 +1,58 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use mirror_core::{Layout, LogLevel}; use mirror_core::{Layout, LogConfig, LogLevel};
use std::fs; use std::fs;
use std::path::PathBuf;
// Rust 原生处理 log 命令逻辑 // Rust 原生处理 log 命令逻辑
pub(crate) fn log_handle(layout: &Layout, matches: &clap::ArgMatches) -> Result<()> { pub(crate) fn log_handle(layout: &Layout, matches: &clap::ArgMatches) -> Result<()> {
let log_ini_path = layout.base_dir.join("../../../mirror-core/mirror.ini"); let log_ini_path = layout.base_dir.join("mirror.ini");
// 1. 处理设置日志等级 let new_level = matches.get_one::<LogLevel>("level").copied();
if let Some(&level) = matches.get_one::<LogLevel>("level") { let new_dir = matches.get_one::<String>("dir").map(|s| s.trim());
let content = format!("level = \"{}\"\nlog_dir = \"logs\"\n", level.as_str()); // 1. 反序列化:读取并解析现有配置;若文件不存在则使用默认配置
fs::write(&log_ini_path, content) let mut config: LogConfig = if log_ini_path.is_file() {
.with_context(|| format!("写入日志配置文件失败: {}", log_ini_path.display()))?; let content = fs::read_to_string(&log_ini_path)
println!("日志级别已更新为: {level} ({})", log_ini_path.display());
return Ok(());
}
// 2. 处理 tail 查看
if let Some(&tail_lines) = matches.get_one::<usize>("tail") {
println!("正在检索最后 {tail_lines} 行日志...");
// 原生 Rust 高性能按行倒序读取日志文件
return Ok(());
}
// 3. 默认打印当前配置
if log_ini_path.is_file() {
let current_ini = fs::read_to_string(&log_ini_path)
.with_context(|| format!("读取配置文件失败: {}", log_ini_path.display()))?; .with_context(|| format!("读取配置文件失败: {}", log_ini_path.display()))?;
println!("当前 mirror.ini 配置:\n{current_ini}"); content.parse().unwrap_or_default()
} else { } else {
println!("未找到 mirror.ini,当前使用默认全局级别: warn"); LogConfig::default()
};
// 2. 修改配置并写盘(仅更新用户指定的字段,未指定的字段保留原值)
if new_level.is_some() || new_dir.is_some() {
if let Some(lvl) = new_level {
config.level = lvl;
}
if let Some(dir) = new_dir {
config.log_dir = Some(PathBuf::from(dir));
}
// 序列化写回
fs::write(&log_ini_path, config.to_string())
.with_context(|| format!("写入日志配置文件失败: {}", log_ini_path.display()))?;
let actual_dir = config.resolve_log_dir(&layout.base_dir);
println!("✔ 日志配置已更新:");
println!(" 日志级别: {}", config.level.as_str());
println!(" 日志目录: {}", actual_dir.display());
println!(" 配置文件: {}", log_ini_path.display());
return Ok(());
}
// 3. 无修改参数时,展示当前配置状态
println!("=== Mirror 日志配置 ===");
println!("配置文件: {}", log_ini_path.display());
println!("---------------------");
if log_ini_path.is_file() {
print!("{config}");
} else {
println!("状态: 配置文件尚未创建");
println!("当前生效级别: {} (默认)", config.level.as_str());
println!(
"默认日志目录: {}",
config.resolve_log_dir(&layout.base_dir).display()
);
println!("\n提示: 可使用 'mr log -l debug' 或 'mr log -d my_logs' 进行配置修改");
} }
Ok(()) Ok(())
+30 -20
View File
@@ -1,5 +1,5 @@
use crate::validators; use crate::validators;
use mirror_core::error::validation_error; use mirror_core::{conversion_err, validation_error};
use mlua::{FromLua, Lua, Value}; use mlua::{FromLua, Lua, Value};
use std::collections::HashMap; use std::collections::HashMap;
@@ -20,10 +20,14 @@ impl FromLua for MrCommand {
let root_tbl = match value { let root_tbl = match value {
Value::Table(t) => t, Value::Table(t) => t,
other => { other => {
return Err(validation_error(format!( return Err(conversion_err(
"commands 注册表的顶层配置必须是 Table,实际检测到: {}", other.type_name(),
other.type_name() "MrCommand",
))); format!(
"commands 注册表的顶层配置必须是 Table,实际检测到: {}",
other.type_name()
),
));
} }
}; };
@@ -35,10 +39,14 @@ impl FromLua for MrCommand {
let cmd_name = validators::validate_command_name(&cmd_name)?; let cmd_name = validators::validate_command_name(&cmd_name)?;
// 2. 校验 entry 是否是 Table // 2. 校验 entry 是否是 Table
if !cmd_entry.is_table() { if !cmd_entry.is_table() {
return Err(validation_error(format!( return Err(conversion_err(
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}", cmd_entry.type_name(),
cmd_entry.type_name() "MrCommand",
))); format!(
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}",
cmd_entry.type_name()
),
));
} }
// 3. 直接交由子命令解析,外层负责补充错误上下文 // 3. 直接交由子命令解析,外层负责补充错误上下文
let cmd_def = MrSubCommand::from_lua(cmd_entry, lua).map_err(|err| { let cmd_def = MrSubCommand::from_lua(cmd_entry, lua).map_err(|err| {
@@ -55,7 +63,7 @@ pub struct MrSubCommand {
/// 命令处理函数:必须是 Lua Function /// 命令处理函数:必须是 Lua Function
pub(crate) module: String, pub(crate) module: String,
/// 命令行说明:必须是普通文本字符串 /// 命令行说明:必须是普通文本字符串
pub(crate) description: String , pub(crate) description: String,
} }
impl FromLua for MrSubCommand { impl FromLua for MrSubCommand {
@@ -63,10 +71,11 @@ impl FromLua for MrSubCommand {
let table = match value { let table = match value {
Value::Table(t) => t, Value::Table(t) => t,
other => { other => {
return Err(validation_error(format!( return Err(conversion_err(
"配置必须是 Table,实际检测到: {}", other.type_name(),
other.type_name() "MrSubCommand",
))); format!("配置必须是 Table,实际检测到: {}", other.type_name()),
));
} }
}; };
// validators::LuaValidator::parse_command_entry(&tbl) // validators::LuaValidator::parse_command_entry(&tbl)
@@ -83,7 +92,7 @@ impl FromLua for MrSubCommand {
))); )));
} }
} }
let handler_val = table.get::<Value>("handler")?; let module_val = table.get::<Value>("module")?;
// let handler_func = match handler_val { // let handler_func = match handler_val {
// Value::Function(f) => f, // Value::Function(f) => f,
// Value::Boolean(true) => { // Value::Boolean(true) => {
@@ -105,14 +114,15 @@ impl FromLua for MrSubCommand {
// ))); // )));
// } // }
// }; // };
let module = match handler_val { let module = match module_val {
Value::Table(t) if t.get::<bool>("__is_lazy_command").unwrap_or(false) => { Value::String(s) => {
t.get::<String>("module")? // 兜底归一化(防止用户手写 module = "a/b" 时未走 mr.command)
// 同时兼容直接写字符串的情况
s.to_str()?.replace(['/', '\\'], ".")
} }
Value::String(s) => s.to_str()?.to_string(), // 同时兼容直接写字符串的情况
other => { other => {
return Err(mlua::Error::runtime(format!( return Err(mlua::Error::runtime(format!(
"handler 必须通过 command(\"xxx\") 声明,实际为 {}", "module 必须通过 mr.command(\"xxx\") 声明,实际为 {}",
other.type_name() other.type_name()
))); )));
} }
+14
View File
@@ -0,0 +1,14 @@
pub struct ExecuteError {
pub code: i32,
pub source: anyhow::Error,
}
impl ExecuteError {
pub fn new(code: i32, err: impl Into<anyhow::Error>) -> Self {
Self {
code,
source: err.into(),
}
}
}
+64
View File
@@ -0,0 +1,64 @@
use crate::cli;
use crate::commands::{builtin, dynamic::MrCommand};
use crate::error::ExecuteError;
use crate::plugins::register_mr_command;
use anyhow::{Context, Result};
use mirror_core::{Layout, LuaRuntime, register_basics};
// 成功的标准退出码
pub const EXIT_SUCCESS: i32 = 0;
// mr CLI 专有错误退出码
pub const EXIT_CONFIG_ERROR: i32 = 10; // 配置文件加载、解析失败
pub const EXIT_RUNTIME_ERROR: i32 = 11; // Lua 运行时初始化或脚本执行异常
pub const EXIT_COMMAND_NOT_FOUND: i32 = 12; // 找不到指定的子命令
pub const EXIT_BUILTIN_ERROR: i32 = 13; // 原生命令(如 log)执行失败
pub fn run(layout: &Layout) -> Result<(), ExecuteError> {
let registry_file = layout.base_dir.join("commands").join("commands.lua");
let runtime = LuaRuntime::builder()
.with(|lua| register_basics(lua, layout))
.with(register_mr_command)
.build()
.map_err(|e| ExecuteError::new(EXIT_RUNTIME_ERROR, e))?;
let commands: MrCommand = runtime
.eval_script(&registry_file)
.with_context(|| format!("加载命令配置失败: {}", registry_file.display()))
.map_err(|e| ExecuteError::new(EXIT_CONFIG_ERROR, e))?;
let app = cli::build(commands.clone());
let matches = app.get_matches();
match matches.subcommand() {
// 分支 A: 原生 Rust 实现的命令
Some(("log", sub_m)) => {
builtin::log_handle(&layout, sub_m)
.map_err(|e| ExecuteError::new(EXIT_BUILTIN_ERROR, e))?;
}
// 分支 B: 动态 Lua 注册的命令
Some((cmd_name, sub_m)) => {
let sub_cmd = commands.get(cmd_name).ok_or_else(|| {
ExecuteError::new(
EXIT_COMMAND_NOT_FOUND,
anyhow::anyhow!("未找到子命令 '{cmd_name}'"),
)
})?;
let raw_args = cli::extract_raw_args(sub_m);
runtime
.require_and_run(&sub_cmd.module, raw_args)
.map_err(|e| {
ExecuteError::new(
EXIT_COMMAND_NOT_FOUND,
anyhow::anyhow!("执行子命令 '{}' 失败: {:#}", cmd_name, e),
)
})?;
}
// 分支 C: 未输入子命令(通常已被 Clap 的 arg_required_else_help 拦截)
None => {}
}
Ok(())
}
+38 -8
View File
@@ -1,14 +1,44 @@
mod app; use crate::executor::EXIT_SUCCESS;
use anyhow::Context;
use mirror_core::{Layout, init_logging_from};
use std::process::exit;
use tracing::{debug, error};
mod cli; mod cli;
mod commands; mod commands;
mod error;
mod executor;
mod plugins;
mod validators; mod validators;
use std::process::ExitCode; fn main() {
let current_exe = match std::env::current_exe().context("获取代理程序路径失败") {
Ok(exe) => exe,
Err(e) => {
eprintln!("垫片启动失败: {:#}", e);
exit(1);
}
};
let layout = match Layout::from(&current_exe) {
Ok(l) => l,
Err(e) => {
eprintln!("解析程序目录布局失败: {:#}", e);
exit(1);
}
};
let _guard = init_logging_from(&layout);
fn main() -> ExitCode { debug!("=== 管理程序启动 ===");
if let Err(err) = app::run() { debug!(
eprintln!("\x1b[31m[mr error]\x1b[0m {:#}", err); exe = %current_exe.display(),
return ExitCode::FAILURE; // 返回退出码 1 target_name = %layout.target_name,
"日志系统与布局上下文已就位"
);
// 核心运行:成功退出 0,失败根据 AppError 里的 code 退出
if let Err(app_err) = executor::run(&layout) {
error!("{:#}", app_err.source);
exit(app_err.code);
} }
ExitCode::SUCCESS // 返回退出码 0
} exit(EXIT_SUCCESS);
}
+24
View File
@@ -0,0 +1,24 @@
use mirror_core::{Error, Result};
use mlua::{Lua, Table, Value};
/// 仅存在于 CLI 侧的 DSL 扩展函数
pub fn register_mr_command(lua: &Lua) -> Result<()> {
let globals = lua.globals();
let mr_table: Table = match globals.get("mr")? {
Value::Table(tbl) => tbl,
_ => {
let tbl = lua.create_table()?;
globals.set("mr", &tbl)?;
tbl
}
};
let command_fn = lua.create_function(|_lua, mod_path: String| {
let normalized = mod_path.replace(['/', '\\'], ".");
Ok(normalized)
})?;
mr_table.set("command", command_fn)?;
Ok(())
}
+1 -1
View File
@@ -1,4 +1,4 @@
use mirror_core::error::validation_error; use mirror_core::validation_error;
use mlua::Value; use mlua::Value;
pub fn validate_command_name(name: &Value) -> mlua::Result<String> { pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
+1
View File
@@ -14,3 +14,4 @@ serde = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
tracing-appender = { workspace = true } tracing-appender = { workspace = true }
thiserror = "2.0.20"
+106 -1
View File
@@ -1,8 +1,22 @@
use std::fmt;
use std::path::PathBuf;
use thiserror::Error;
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题 // 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
/// 配置校验错误 /// 配置校验错误
pub fn validation_error(message: impl Into<String>) -> mlua::Error { pub fn validation_error(message: impl Into<String>) -> mlua::Error {
mlua::Error::RuntimeError(message.into()) mlua::Error::RuntimeError(message.into())
} }
pub fn conversion_err(
from: &'static str,
to: impl Into<String>,
msg: impl Into<String>,
) -> mlua::Error {
mlua::Error::FromLuaConversionError {
from,
to: to.into(),
message: Some(msg.into()),
}
}
/// 将底层 Lua 语法错误转换为用户友好的提示 /// 将底层 Lua 语法错误转换为用户友好的提示
pub fn syntax_error(err: mlua::Error) -> mlua::Error { pub fn syntax_error(err: mlua::Error) -> mlua::Error {
match &err { match &err {
@@ -22,4 +36,95 @@ pub fn syntax_error(err: mlua::Error) -> mlua::Error {
_ => {} _ => {}
} }
err err
} }
/// 全局统一的 `Result` 别名,默认错误类型为 [`Error`]。
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Error, Debug)]
pub enum Error {
#[error("初始化 Lua 沙箱失败: {0}")]
LuaInit(#[source] mlua::Error),
#[error("构建 LuaRuntime 异常")]
BuildError,
///读取脚本文件失败
#[error("读取脚本文件失败 ({path}): {source}")]
ScriptRead {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("脚本 {path} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)")]
InvalidUtf8 { path: PathBuf },
// 收敛所有来自 mlua 的错误,附带来源上下文
#[error("Lua 执行异常 ({chunk}): {source}")]
LuaRuntime {
chunk: String,
#[source]
source: mlua::Error,
},
#[error("模块 '{module_name}' 必须返回一个函数作为入口,实际返回了 {actual_type}")]
InvalidModuleEntry {
module_name: String,
actual_type: &'static str,
},
// #[error("插件/上下文挂载失败 '{name}': {source}")]
// Plugin {
// name: String,
// #[source]
// source: mlua::Error,
// },
#[error("Lua 运行时未指定上下文错误: {0}")]
LuaError(#[from] mlua::Error),
// #[error("设置 {0} 环境变量失败")]
// SetMirrorDir(String),
/// 用户自定义扩展错误,用于包装第三方插件产生的错误。
#[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 {}
+10 -4
View File
@@ -1,14 +1,20 @@
pub mod error; mod error;
mod layout; mod layout;
mod logger; mod logger;
mod plugins;
mod runtime; mod runtime;
mod utils; mod utils;
mod validators; mod validators;
mod model;
pub use layout::Layout; pub use layout::Layout;
// pub use mirror_shim::mirror::Mirror; // pub use mirror_shim::mirror::Mirror;
pub use runtime::LuaRuntime; pub use runtime::LuaRuntime;
pub use logger::{init_logging_from, LogLevel, Logger}; pub use logger::{LogConfig, LogLevel, init_logging_from};
// pub use mirror_shim::utils::{lua_string_2_os_string, normalize_path_for_lua, parse_tokens}; pub use plugins::register_basics;
pub use validators::validate_command_name; pub use utils::normalize_path_for_lua;
pub use validators::validate_command_name;
// 把 Result 和 Error 提升到根命名空间,给外部带来极致便利
pub use error::{Error, Result,validation_error,conversion_err,TargetExecutionError};
pub use model::TargetConfig;
+65 -63
View File
@@ -1,7 +1,7 @@
use crate::Layout; use crate::Layout;
use serde::Deserialize; use std::convert::Infallible;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::layer::SubscriberExt;
@@ -61,41 +61,50 @@ impl FromStr for LogLevel {
} }
} }
#[derive(Debug, Deserialize, Default)] /// 日志配置模型(对应 mirror.ini 的纯强类型表示)
pub struct Logger { #[derive(Debug, Clone, PartialEq, Eq, Default)]
#[serde(default)] pub struct LogConfig {
pub level: LogLevel, pub level: LogLevel,
pub log_dir: Option<PathBuf>, pub log_dir: Option<PathBuf>,
} }
impl Logger { impl LogConfig {
/// 零依赖解析 ini / key-value 文本内容 /// 零依赖解析 ini / key-value 文本内容
pub fn parse_ini(content: &str) -> Self { /// 计算日志目录的实际物理路径(相对路径基于 base_dir 拼接)
let mut logger = Self::default(); pub fn resolve_log_dir(&self, base_dir: &Path) -> PathBuf {
match &self.log_dir {
Some(p) if p.is_absolute() => p.clone(),
Some(p) => base_dir.join(p),
None => base_dir.join("logs"),
}
}
}
// 协议 1: 反序列化 (INI 文本 -> LogConfig)
impl FromStr for LogConfig {
type Err = Infallible;
fn from_str(content: &str) -> Result<Self, Self::Err> {
let mut config = Self::default();
for line in content.lines() { for line in content.lines() {
let line = line.trim(); let line = line.trim();
// 跳过空行、注释行 (# 或 ;) 和 section 头 ([...]) // 跳过空行、注释 (#, ;) 与 section 标头 ([...])
if line.is_empty() if line.is_empty() || line.starts_with(['#', ';', '[']) {
|| line.starts_with('#')
|| line.starts_with(';')
|| line.starts_with('[')
{
continue; continue;
} }
if let Some((key, val)) = line.split_once('=') { if let Some((key, val)) = line.split_once('=') {
let key = key.trim(); let key = key.trim();
let val = val.trim().trim_matches('"').trim_matches('\''); let val = val.trim().trim_matches(['"', '\'']);
match key { match key {
"level" => { "level" => {
if let Ok(lvl) = LogLevel::from_str(val) { if let Ok(lvl) = LogLevel::from_str(val) {
logger.level = lvl; config.level = lvl;
} }
} }
"log_dir" => { "log_dir" => {
if !val.is_empty() { if !val.is_empty() {
logger.log_dir = Some(PathBuf::from(val)); config.log_dir = Some(PathBuf::from(val));
} }
} }
_ => {} _ => {}
@@ -103,9 +112,24 @@ impl Logger {
} }
} }
logger Ok(config)
} }
} }
// 协议 2: 序列化 (LogConfig -> INI 文本)
impl std::fmt::Display for LogConfig {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let dir_str = self
.log_dir
.as_deref()
.and_then(|p| p.to_str())
.unwrap_or("logs");
writeln!(f, "level = \"{}\"", self.level.as_str())?;
writeln!(f, "log_dir = \"{}\"", dir_str)
}
}
/// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。 /// 基于 Layout 自动寻址 mirror.toml 并初始化日志系统。
/// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。 /// 仅在开启详细日志(如 info/debug/trace)时激活异步文件记录并返回 `WorkerGuard`。
pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> { pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
@@ -113,16 +137,11 @@ pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
// let config_path = layout.base_dir.join("mirror.toml"); // let config_path = layout.base_dir.join("mirror.toml");
let config_path = layout.base_dir.join("mirror.ini"); let config_path = layout.base_dir.join("mirror.ini");
// 读取配置文件(如不存在或解析失败,降级回退到默认设置) // 1. 读取并反序列化配置文件(缺失或异常时安全降级为 Default)
let config = if config_path.exists() { let config: LogConfig = fs::read_to_string(&config_path)
fs::read_to_string(&config_path) .ok()
.map(|s| Logger::parse_ini(&s)) .and_then(|s| s.parse().ok())
// .ok() .unwrap_or_default();
// .and_then(|s| toml::from_str::<Logger>(&s).ok())
.unwrap_or_default()
} else {
Logger::default()
};
// 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level // 1. 优先读取环境变量 MIRROR_LOG_LEVEL,解析失败或未设置则退回到 config.level
let effective_level = std::env::var("MIRROR_LOG_LEVEL") let effective_level = std::env::var("MIRROR_LOG_LEVEL")
@@ -136,47 +155,30 @@ pub fn init_logging_from(layout: &Layout) -> Option<WorkerGuard> {
// 基础终端输出层 // 基础终端输出层
let stderr_layer = fmt::layer().with_writer(std::io::stderr).with_target(false); let stderr_layer = fmt::layer().with_writer(std::io::stderr).with_target(false);
// 2. 根据强类型枚举判断是否激活落盘模式 // 3. 动态配置落盘文件层(仅在 verbose 级别启用)
if effective_level.is_verbose() { let (file_layer, guard) = if effective_level.is_verbose() {
let (non_blocking, guard) = create_file_appender(layout, config.log_dir); let (non_blocking, guard) = create_file_appender(&config.resolve_log_dir(&layout.base_dir));
let file_layer = fmt::layer() let layer = fmt::layer()
.with_writer(non_blocking) .with_writer(non_blocking)
.with_ansi(false) .with_ansi(false)
.with_target(false); .with_target(false);
(Some(layer), Some(guard))
let _ = tracing_subscriber::registry()
.with(filter)
.with(stderr_layer)
.with(file_layer)
.try_init();
Some(guard)
} else { } else {
let _ = tracing_subscriber::registry() (None, None)
.with(filter)
.with(stderr_layer)
.try_init();
None
}
}
fn create_file_appender(
layout: &Layout,
log_dir_override: Option<PathBuf>,
) -> (NonBlocking, WorkerGuard) {
let output_dir = match log_dir_override {
Some(user_path) => {
if user_path.is_relative() {
layout.base_dir.join(user_path)
} else {
user_path
}
}
None => layout.base_dir.join("logs"),
}; };
if let Err(err) = fs::create_dir_all(&output_dir) { // 4. 统一组装并初始化 Subscriber(Option<Layer> 天然实现了 Layer trait)
let _ = tracing_subscriber::registry()
.with(filter)
.with(stderr_layer)
.with(file_layer)
.try_init();
guard
}
fn create_file_appender(output_dir: &Path) -> (NonBlocking, WorkerGuard) {
if let Err(err) = fs::create_dir_all(output_dir) {
eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err); eprintln!("[WARN] 创建日志目录 {} 失败: {}", output_dir.display(), err);
} }
+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()
))),
}
}
}
+111
View File
@@ -0,0 +1,111 @@
use crate::error::{Error, Result};
use crate::{Layout, normalize_path_for_lua};
use mlua::{Lua, Table, Value};
use std::env;
use tracing::{debug, warn};
const MIRROR_DIR: &str = "__MIRROR_DIR__";
// 采用形式 A:直接传引用的普通函数
pub fn register_basics(lua: &Lua, layout: &Layout) -> Result<()> {
let base_dir = normalize_path_for_lua(&layout.base_dir);
register_mirror_dir(lua, &base_dir)?;
register_get_env(lua)?;
setup_require(lua, &base_dir)?;
Ok(())
}
/// 注入全局锚点变量
pub fn register_mirror_dir(lua: &Lua, base_dir: &str) -> Result<()> {
debug!("设置 {MIRROR_DIR} 环境变量");
lua.globals().set(MIRROR_DIR, base_dir)?;
// .context(format!("设置 {MIRROR_DIR} 环境变量失败"))?;
Ok(())
}
///注入 get_env 供配置读取环境变量
pub fn register_get_env(lua: &Lua) -> Result<()> {
let globals = lua.globals();
let get_env = lua.create_function(|lua, key: String| -> mlua::Result<Table> {
// 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段)
let value = env::var_os(key).unwrap_or_default();
// 空输入返回空表;否则按平台分隔符拆分(split_paths 会剥离引号包裹)
if value.is_empty() {
return lua.create_table();
}
let table = lua.create_table()?;
for (i, p) in env::split_paths(&value).enumerate() {
// 3. 跨平台提取原始字节并转为 LuaString,保证 100% 无损
#[cfg(unix)]
let lua_str = {
use std::os::unix::ffi::OsStrExt;
lua.create_string(p.as_os_str().as_bytes())?
};
#[cfg(windows)]
let lua_str = {
// Windows 路径是 UTF-16,转成字符串或保持其字节表达
let s = p.to_string_lossy();
lua.create_string(s.as_bytes())?
};
table.set(i + 1, lua_str)?;
}
Ok(table)
})?;
// .context("注册 get_env 函数失败")?;
globals.set("get_env", get_env)?;
// .context("挂载 get_env 全局函数失败")?;
Ok(())
}
/// 配置 package.path 并包装 require,拦截加载失败以提高容错性
pub fn setup_require(lua: &Lua, base_dir: &str) -> Result<()> {
let globals = lua.globals();
// 1. 安全加固并拓展 package 搜索路径
let package = globals.get::<Table>("package")?;
let _ = package.set("cpath", "");
let _ = package.set("loadlib", Value::Nil);
let commands_dir = format!("{}/commands", base_dir);
let vendor_dir = format!("{}/vendor", base_dir);
let path = package.get::<String>("path")?;
let new_path = format!(
"{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;",
path, base_dir, base_dir, commands_dir, commands_dir, vendor_dir, vendor_dir
);
debug!("重新写入的 package 查找路径");
package.set("path", new_path)?;
// 2. 获取原生 require 并通过闭包直接持有(无需向全局表注入备份变量)
let original_require: mlua::Function = globals.get("require")?;
// .context("获取内置 require 函数失败")?;
let wrapped_require =
lua.create_function(move |_lua, module: String| -> mlua::Result<Value> {
match original_require.call::<Value>(module.as_str()) {
Ok(value) => Ok(value),
Err(e) => {
warn!(
module = %module,
error = %e,
"配置模块加载失败,已跳过该条目(可在独立配置文件中定义)"
);
Ok(Value::Nil)
}
}
})?;
// .context("创建包装版 require 函数失败")?;
// 3. 覆盖全局 require
globals.set("require", wrapped_require)?;
// .context("重载 require 函数失败")?;
Ok(())
}
+58 -166
View File
@@ -1,13 +1,34 @@
use crate::Layout; use crate::{Error, Result};
use crate::error::syntax_error; use mlua::{FromLua, Lua, StdLib, Value};
use crate::utils::normalize_path_for_lua; use std::fs;
use anyhow::{Context, Result};
use mlua::{FromLua, Lua, StdLib, Table, Value};
use std::path::Path; use std::path::Path;
use std::{env, fs}; use tracing::debug;
use tracing::{debug, trace, warn};
const MIRROR_DIR: &str = "__MIRROR_DIR__"; pub struct LuaRuntimeBuilder {
lua: Result<Lua>,
}
impl LuaRuntimeBuilder {
pub fn with<F>(mut self, extension: F) -> Self
where
F: FnOnce(&Lua) -> Result<()>,
{
// 如果前面已有错误,直接短路跳过后续操作
if let Ok(ref lua) = self.lua {
if let Err(e) = extension(lua) {
self.lua = Err(e);
}
}
self
}
/// 收敛构建终点:一次性输出统一的 Result
pub fn build(self) -> Result<LuaRuntime> {
let lua = self.lua?;
Ok(LuaRuntime { lua })
}
}
/// 将 Layout 相关的公共上下文组合为一个插件项
pub struct LuaRuntime { pub struct LuaRuntime {
lua: Lua, lua: Lua,
@@ -15,43 +36,28 @@ pub struct LuaRuntime {
impl LuaRuntime { impl LuaRuntime {
/// 初始化限定权限的 Lua 沙箱环境 /// 初始化限定权限的 Lua 沙箱环境
pub fn new(layout: &Layout) -> Result<Self> { pub fn builder() -> LuaRuntimeBuilder {
// 只加载安全的标准库,剥离 os / io 等风险模块 // 只加载安全的标准库,剥离 os / io 等风险模块
let lua = Lua::new_with( let lua = Lua::new_with(
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE, StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE,
mlua::LuaOptions::default(), mlua::LuaOptions::default(),
) )
.context("初始化 Lua 失败")?; .map_err(Error::LuaInit);
// 统一使用 POSIX 风格路径规范化路径字符串 LuaRuntimeBuilder { lua }
let base_dir = normalize_path_for_lua(&layout.base_dir);
// let tools_dir = normalize_path_for_lua(&layout.tools_dir);
// 1. 注入锚点变量 __MIRROR_DIR__(shim 安装根目录)
Self::register_mirror_dir(&lua, &base_dir)?;
// 2. 安全暴露 get_env 供配置读取环境变量
// 返回按平台路径分隔符拆分后的段数组(自动剥离引号包裹),
// 便于 PATH 等列表变量直接嵌入数组:PATH = { prefix, get_env("PATH") }
Self::register_get_env(&lua)?;
// 3. 初始化并配置安全/容错的 require 机制
Self::setup_require(&lua, &base_dir)?;
Ok(Self { lua })
} }
/// 执行指定脚本文件,直接返回完整的 Lua Table /// 执行指定脚本文件,直接返回完整的 Lua Table
pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T> { pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T> {
let path = path.as_ref(); let path = path.as_ref();
let bytes = let bytes = fs::read(path).map_err(|e| Error::ScriptRead {
fs::read(path).with_context(|| format!("无法读取配置文件: {}", path.display()))?; path: path.to_path_buf(),
source: e,
})?;
let code = String::from_utf8(bytes).with_context(|| { let code = String::from_utf8(bytes).map_err(|_| Error::InvalidUtf8 {
format!( path: path.to_path_buf(),
"{} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)",
path.display()
)
})?; })?;
debug!("code {:?}", code); debug!("code {:?}", code);
// 使用 @<path> 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。 // 使用 @<path> 格式标记 Chunk 名称,确保 Lua 报错时能精确回溯到对应的源文件名与行号。
@@ -61,150 +67,36 @@ impl LuaRuntime {
.load(&code) .load(&code)
.set_name(&chunk_name) .set_name(&chunk_name)
.eval::<T>() .eval::<T>()
.map_err(syntax_error) .map_err(|e| Error::LuaRuntime {
.with_context(|| format!("执行 {} 配置文件失败: {}", chunk_name,path.display())) chunk: chunk_name,
} source: e,
})
/// 传入注册表的 handler key 与命令行原始参数,内部完成调用
pub fn call_handler(
&self,
handler_key: &mlua::RegistryKey,
args: Vec<String>,
) -> Result<()> {
let handler: mlua::Function = self.lua.registry_value(handler_key)?;
let lua_args = self.lua.create_sequence_from(args)?;
if let Err(err) = handler.call::<()>(lua_args) {
let msg = err.to_string();
// 内部消化类似 argparse 的帮助退出信号
if msg.contains("__ARGPARSE_HELP__") {
return Ok(());
}
anyhow::bail!("{msg}");
}
Ok(())
} }
pub fn require_and_run(&self, module_name: &str, raw_args: Vec<String>) -> Result<()> { pub fn require_and_run(&self, module_name: &str, raw_args: Vec<String>) -> Result<()> {
let require: mlua::Function = self.lua.globals().get("require")?; let require: mlua::Function = self.lua.globals().get("require")?;
let handler_val: mlua::Value = require.call(module_name)?; let handler_val: Value = require.call(module_name)?;
let func = match handler_val { let func = match handler_val {
mlua::Value::Function(f) => f, Value::Function(f) => f,
other => anyhow::bail!("模块 '{module_name}' 必须返回一个函数,实际为 {}", other.type_name()), other => {
return Err(Error::InvalidModuleEntry {
module_name: module_name.to_string(),
actual_type: other.type_name(),
});
}
}; };
let lua_args = self.lua.create_sequence_from(raw_args)?; let lua_args = self.lua.create_sequence_from(raw_args)?;
func.call::<()>(lua_args)?; func.call::<()>(lua_args)?;
Ok(()) Ok(())
} }
} }
impl LuaRuntime {
/// 注入全局锚点变量
fn register_mirror_dir(lua: &Lua, base_dir: &str) -> Result<()> {
lua.globals()
.set(MIRROR_DIR, base_dir)
.context(format!("设置 {MIRROR_DIR} 环境变量失败"))?;
Ok(())
}
///注入 get_env 供配置读取环境变量
fn register_get_env(lua: &Lua) -> Result<()> {
let globals = lua.globals();
let get_env = lua
.create_function(|lua, key: String| -> mlua::Result<Table> {
// 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段)
let value = env::var_os(key).unwrap_or_default();
// 空输入返回空表;否则按平台分隔符拆分(split_paths 会剥离引号包裹)
if value.is_empty() {
return lua.create_table();
}
let table = lua.create_table()?;
for (i, p) in env::split_paths(&value).enumerate() {
// 3. 跨平台提取原始字节并转为 LuaString,保证 100% 无损
#[cfg(unix)]
let lua_str = {
use std::os::unix::ffi::OsStrExt;
lua.create_string(p.as_os_str().as_bytes())?
};
#[cfg(windows)]
let lua_str = {
// Windows 路径是 UTF-16,转成字符串或保持其字节表达
let s = p.to_string_lossy();
lua.create_string(s.as_bytes())?
};
table.set(i + 1, lua_str)?;
}
Ok(table)
})
.context("注册 get_env 函数失败")?;
globals
.set("get_env", get_env)
.context("挂载 get_env 全局函数失败")?;
Ok(())
}
/// 配置 package.path 并包装 require,拦截加载失败以提高容错性
fn setup_require(lua: &Lua, base_dir: &str) -> Result<()> {
let globals = lua.globals();
// 1. 安全加固并拓展 package 搜索路径
let package = globals.get::<Table>("package")?;
let _ = package.set("cpath", "");
let _ = package.set("loadlib", Value::Nil);
let commands_dir = format!("{}/commands", base_dir);
let vendor_dir = format!("{}/vendor", base_dir);
let path = package.get::<String>("path")?;
let new_path = format!(
"{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua;",
path, base_dir, base_dir, commands_dir, commands_dir, vendor_dir, vendor_dir
);
debug!("重新写入的 package 查找路径");
package.set("path", new_path)?;
// 2. 获取原生 require 并通过闭包直接持有(无需向全局表注入备份变量)
let original_require: mlua::Function = globals
.get("require")
.context("获取内置 require 函数失败")?;
let wrapped_require = lua
.create_function(move |_lua, module: String| -> mlua::Result<Value> {
match original_require.call::<Value>(module.as_str()) {
Ok(value) => Ok(value),
Err(e) => {
warn!(
module = %module,
error = %e,
"配置模块加载失败,已跳过该条目(可在独立配置文件中定义)"
);
Ok(Value::Nil)
}
}
})
.context("创建包装版 require 函数失败")?;
// 3. 覆盖全局 require
globals
.set("require", wrapped_require)
.context("重载 require 函数失败")?;
Ok(())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{Layout, }; use crate::Layout;
fn test_layout() -> Layout { fn test_layout() -> Layout {
let root = std::env::temp_dir().join("rshim-test-layout"); let root = std::env::temp_dir().join("rshim-test-layout");
@@ -217,7 +109,7 @@ mod tests {
#[test] #[test]
fn missing_module_require_returns_nil() { fn missing_module_require_returns_nil() {
let runtime = LuaRuntime::new(&test_layout()).unwrap(); let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime let value: Value = runtime
.lua .lua
.load(r#"return require("rshim_test_no_such_module")"#) .load(r#"return require("rshim_test_no_such_module")"#)
@@ -228,7 +120,7 @@ mod tests {
#[test] #[test]
fn builtin_module_require_still_works() { fn builtin_module_require_still_works() {
let runtime = LuaRuntime::new(&test_layout()).unwrap(); let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime let value: Value = runtime
.lua .lua
.load(r#"return pcall(require, "string")"#) .load(r#"return pcall(require, "string")"#)
@@ -239,7 +131,7 @@ mod tests {
#[test] #[test]
fn get_env_returns_split_table() { fn get_env_returns_split_table() {
let runtime = LuaRuntime::new(&test_layout()).unwrap(); let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime let value: Value = runtime
.lua .lua
.load(r#"return get_env("PATH")"#) .load(r#"return get_env("PATH")"#)
@@ -266,7 +158,7 @@ mod tests {
#[test] #[test]
fn get_env_missing_returns_empty_table() { fn get_env_missing_returns_empty_table() {
let runtime = LuaRuntime::new(&test_layout()).unwrap(); let runtime = LuaRuntime::builder().build().unwrap();
let value: Value = runtime let value: Value = runtime
.lua .lua
.load(r#"return get_env("RSHIM_TEST_NO_SUCH_VAR_12345")"#) .load(r#"return get_env("RSHIM_TEST_NO_SUCH_VAR_12345")"#)
@@ -298,7 +190,7 @@ mod tests {
// ) // )
// .eval() // .eval()
// .unwrap(); // .unwrap();
// //
// let path = cfg.env.get("PATH").unwrap().to_str().unwrap(); // let path = cfg.env.get("PATH").unwrap().to_str().unwrap();
// let prefix = std::env::temp_dir() // let prefix = std::env::temp_dir()
// .join("rshim-test-layout") // .join("rshim-test-layout")
@@ -306,7 +198,7 @@ mod tests {
// .replace('\\', "/") // .replace('\\', "/")
// + "/tools/numa;"; // + "/tools/numa;";
// assert!(path.starts_with(&prefix), "unexpected PATH: {path}"); // assert!(path.starts_with(&prefix), "unexpected PATH: {path}");
// //
// // 宿主 PATH 的段应被附加在配置前缀之后 // // 宿主 PATH 的段应被附加在配置前缀之后
// let host = std::env::var("PATH").unwrap_or_default(); // let host = std::env::var("PATH").unwrap_or_default();
// if !host.is_empty() { // if !host.is_empty() {
@@ -320,7 +212,7 @@ mod tests {
// "missing host PATH segment: {host_first}" // "missing host PATH segment: {host_first}"
// ); // );
// } // }
// //
// // 宿主 PATH 里引号包裹的畸形段(如 "D:\\...\\bin;")应被原样保留, // // 宿主 PATH 里引号包裹的畸形段(如 "D:\\...\\bin;")应被原样保留,
// // 而不是让整个配置加载失败 // // 而不是让整个配置加载失败
// if host.contains('"') { // if host.contains('"') {
+60
View File
@@ -9,3 +9,63 @@ pub fn normalize_path_for_lua(path: &Path) -> String {
simplified.to_string_lossy().replace('\\', "/") 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 crate::error::validation_error;
use mlua::Value; use mlua::Value;
@@ -39,3 +40,26 @@ pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
} }
Ok(name_str) 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(())
}
+98
View File
@@ -0,0 +1,98 @@
use crate::mirror::MirrorRegistry;
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, TargetConfig, TargetExecutionError, register_basics};
use std::env;
use std::ffi::OsString;
use std::process::exit;
use tracing::{error, warn};
pub fn run(layout: &Layout) -> i32 {
let global_config = &layout.base_dir.join("mirror.lua");
if !global_config.is_file() {
error!("主配置文件不存在: {}", global_config.display());
return EXIT_FAILED_LOAD_SHIM;
}
let runtime = match LuaRuntime::builder()
.with(|lua| register_basics(lua, &layout))
.build()
{
Ok(r) => r,
Err(e) => {
error!("初始化 Lua 失败: {:#}", e);
return EXIT_FAILED_LOAD_SHIM;
}
};
let registry: MirrorRegistry = match runtime.eval_script(&global_config) {
Ok(t) => t,
Err(e) => {
error!("加载命令配置 [{}] 失败: {:#}", global_config.display(), e);
return EXIT_FAILED_LOAD_SHIM;
}
};
let target_name = layout.target_name.as_str();
let mr = match registry.targets.get(target_name) {
Some(m) => m,
None => {
warn!("未在 mirror.lua 中找到目标配置 [{}]", target_name);
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(target_path, combined_args);
let mut child = match cmd.spawn() {
Ok(v) => v,
Err(e) if e.raw_os_error() == Some(ERROR_ELEVATION_REQUIRED) => {
// 提权回退时需要完整参数:配置默认参数 + 调用方透传参数
let elevated_args: Vec<OsString> = cmd.get_args().map(|s| s.to_os_string()).collect();
exit(execute_elevated(target_path, &elevated_args, Some(&mr.env)))
}
Err(e) => {
error!(
"启动目标程序 [{}] 时发生错误: {}",
target_path.to_string_lossy(),
e
);
return EXIT_FAILED_SPAWN_PROG;
}
};
// 5. 等待子进程退出
match child.wait() {
Ok(status) => status.code().unwrap_or(EXIT_PROG_TERMINATED),
Err(e) => {
error!(
"等待目标程序 [{}] 执行完毕时发生错误: {}",
target_path.to_string_lossy(),
e
);
EXIT_FAILED_WAIT_PROG
}
}
}
+12 -59
View File
@@ -1,25 +1,25 @@
use mirror_core::{init_logging_from, Layout, };
use crate::mirror::Mirror;
use std::ffi::OsString;
use std::{env, process::exit};
use anyhow::Context;
use tracing::{debug, error};
use crate::sys::*; use crate::sys::*;
use anyhow::Context;
use mirror_core::{Layout, init_logging_from};
use std::{env, process::exit};
use tracing::debug;
pub mod sys; mod executor;
pub mod mirror; pub mod mirror;
pub mod validators; pub mod sys;
pub mod utils; pub mod utils;
pub mod validators;
fn main() { fn main() {
let current_exe = match env::current_exe().context("获取代理程序路径失败"){ let current_exe = match env::current_exe().context("获取代理程序路径失败") {
Ok(exe) => exe, Ok(exe) => exe,
Err(e) => { Err(e) => {
eprintln!("垫片启动失败: {:#}", e); eprintln!("垫片启动失败: {:#}", e);
exit(EXIT_FAILED_LOAD_SHIM); exit(EXIT_FAILED_LOAD_SHIM);
} }
}; };
let layout =match Layout::from(&current_exe){
let layout = match Layout::from(&current_exe) {
Ok(l) => l, Ok(l) => l,
Err(e) => { Err(e) => {
eprintln!("解析程序目录布局失败: {:#}", e); eprintln!("解析程序目录布局失败: {:#}", e);
@@ -27,7 +27,6 @@ fn main() {
} }
}; };
//初始化日志:输出到 stderr,避免污染 shim 子进程的 stdout //初始化日志:输出到 stderr,避免污染 shim 子进程的 stdout
let _guard = init_logging_from(&layout); let _guard = init_logging_from(&layout);
@@ -41,52 +40,6 @@ fn main() {
// 2. 注册 Windows 控制台信号 // 2. 注册 Windows 控制台信号
set_console_ctrl_handler(); set_console_ctrl_handler();
// 3. 解析调用参数与代理 Mirror 配置 let code = executor::run(&layout);
let calling_args: Vec<_> = env::args_os().skip(1).collect(); exit(code);
let mr = match Mirror::load(&layout) {
Ok(v) => v,
Err(e) => {
error!("加载代理(mirror)配置时发生错误: {}", e);
exit(EXIT_FAILED_LOAD_SHIM);
}
};
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 child = match cmd.spawn() {
Ok(v) => v,
Err(e) if e.raw_os_error() == Some(ERROR_ELEVATION_REQUIRED) => {
// 提权回退时需要完整参数:配置默认参数 + 调用方透传参数
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),
))
}
Err(e) => {
error!(
"启动目标程序 [{}] 时发生错误: {}",
mr.target.to_string_lossy(),
e
);
exit(EXIT_FAILED_SPAWN_PROG);
}
};
// 5. 等待子进程退出
let status = match child.wait() {
Ok(v) => v,
Err(e) => {
error!(
"等待目标程序 [{}] 执行完毕时发生错误: {}",
mr.target.to_string_lossy(),
e
);
exit(EXIT_FAILED_WAIT_PROG);
}
};
exit(status.code().unwrap_or(EXIT_PROG_TERMINATED))
} }
+52 -68
View File
@@ -1,96 +1,75 @@
use mirror_core::error::validation_error;
use crate::validators::LuaValidator; use crate::validators::LuaValidator;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use mirror_core::validate_command_name;
use mirror_core::{Layout, LuaRuntime, TargetConfig};
use mirror_core::{TargetExecutionError, conversion_err, validation_error};
use mlua::{FromLua, Lua, Table, Value}; use mlua::{FromLua, Lua, Table, Value};
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use tracing::{debug, trace}; use tracing::{debug, trace};
use mirror_core::{Layout, LuaRuntime};
use mirror_core::validate_command_name;
struct MasterMirror{ pub struct MirrorRegistry {
master:HashMap<String,Mirror>, pub targets: HashMap<String, MirrorTarget>,
} }
impl FromLua for MasterMirror { impl FromLua for MirrorRegistry {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> { fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
let root_tbl = match value { let root_tbl = match value {
Value::Table(t) => t, Value::Table(t) => t,
other => { other => {
return Err(validation_error(format!( return Err(conversion_err(
"mirror 注册表的顶层配置必须是 Table,实际检测到: {}", other.type_name(),
other.type_name() "MirrorRegistry",
))); format!(
"mirror 全局注册表顶层必须是 Table,实际检测到: {}",
other.type_name()
),
));
} }
}; };
let mut master = HashMap::new(); let mut targets = HashMap::new();
for pair in root_tbl.pairs::<Value, Value>() { for pair in root_tbl.pairs::<Value, Value>() {
let (cmd_name, cmd_entry) = pair?; let (key, entry) = pair?;
let cmd_name = validate_command_name(&cmd_name)?; let target_name = validate_command_name(&key)?;
// 2. 校验 entry 是否是 Table // 2. 校验 entry 是否是 Table
if !cmd_entry.is_table() { if !entry.is_table() {
return Err(validation_error(format!( return Err(conversion_err(
"子命令 '{cmd_name}' 的配置必须是 Table,实际检测到: {}", entry.type_name(),
cmd_entry.type_name() "MirrorRegistry",
))); format!(
"目标工具 '{target_name}' 的配置必须是 Table,实际检测到: {}",
entry.type_name()
),
));
} }
// 3. 直接交由子命令解析,外层负责补充错误上下文 // 3. 直接交由子命令解析,外层负责补充错误上下文
let cmd_def = Mirror::from_lua(cmd_entry, lua).map_err(|err| { let target_spec = MirrorTarget::from_lua(entry, lua).map_err(|err| {
validation_error(format!("子命令 '{cmd_name}' 配置解析失败:\n{err}")) validation_error(format!("目标工具 '{target_name}' 配置解析失败:\n{err}"))
})?; })?;
master.insert(cmd_name, cmd_def); targets.insert(target_name, target_spec);
} }
Ok(Self { master }) Ok(Self { targets })
}} }
}
/// 单个目标程序的代理规格配置
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Mirror { pub struct MirrorTarget {
pub target: PathBuf, /// 目标程序可执行文件路径
pub target: TargetConfig,
/// 默认预设参数列表
pub args: Vec<OsString>, pub args: Vec<OsString>,
/// 参数别名映射表(用于展开以 mr: 为前缀的别名)
pub aliases: HashMap<String, Vec<OsString>>, pub aliases: HashMap<String, Vec<OsString>>,
/// 注入/覆盖的环境变量
pub env: HashMap<String, OsString>, pub env: HashMap<String, OsString>,
} }
impl Mirror { impl MirrorTarget {
pub fn load(layout: &Layout) -> Result<Self> {
// 策略 1: 尝试加载全局配置文件 mirror.lua
let global_config = layout.base_dir.join("mirror.lua");
if !global_config.is_file() {
bail!("主配置文件不存在: {}", global_config.display());
}
let runtime = LuaRuntime::new(layout)?;
let root_table: Table = runtime.eval_script(&global_config)?;
let target_name = layout.target_name.as_str();
// 检查 mirror.lua 中是否存在以 target_name 命名的 Table 节点
let target_val: Mirror = root_table
.get::<Option<Mirror>>(target_name)
.with_context(|| format!("读取目标配置 [{}] 失败", target_name))?
.ok_or_else(|| {
anyhow::anyhow!(
"在 'mirror.lua' 中未找到目标程序 [{}] 的配置注册",
target_name
)
})?;
debug!(
target = %target_name,
source = %global_config.display(),
"成功从全局配置文件中匹配到目标工具"
);
Ok(target_val)
}
/// 1. 核心参数路由解析(Delegation 到 Spec 的路由逻辑) /// 1. 核心参数路由解析(Delegation 到 Spec 的路由逻辑)
fn resolve_args<I, S>(&self, args: I) -> Vec<OsString> fn resolve_args<I, S>(&self, args: I) -> Vec<OsString>
where where
@@ -125,12 +104,13 @@ impl Mirror {
expanded_args 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 where
P: AsRef<Path>,
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>, S: AsRef<std::ffi::OsStr>,
{ {
let mut cmd = Command::new(&self.target); let mut cmd = Command::new(target.as_ref());
// 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名 // 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名
let final_args = self.resolve_args(combined_args); let final_args = self.resolve_args(combined_args);
@@ -147,8 +127,9 @@ impl Mirror {
cmd cmd
} }
} }
impl FromLua for Mirror {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> { impl FromLua for MirrorTarget {
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
// 脚本返回必须是一个 Table 变体 // 脚本返回必须是一个 Table 变体
let table = match value { let table = match value {
Value::Table(t) => t, Value::Table(t) => t,
@@ -163,9 +144,12 @@ impl FromLua for Mirror {
// 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误) // 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误)
let target = match table.get::<Option<Value>>("target")? { let target = match table.get::<Option<Value>>("target")? {
None | Some(Value::Nil) => { 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 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) // 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
+75 -10
View File
@@ -1,5 +1,5 @@
use mirror_core::error::validation_error;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use mirror_core::{TargetConfig, validation_error};
use mlua::{LuaString, Table, Value}; use mlua::{LuaString, Table, Value};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::ffi::OsString; use std::ffi::OsString;
@@ -7,8 +7,8 @@ use std::fmt;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
// use tinyjson::JsonValue; // use tinyjson::JsonValue;
use tracing::debug;
use crate::utils::{lua_string_2_os_string, parse_tokens}; use crate::utils::{lua_string_2_os_string, parse_tokens};
use tracing::debug;
/// Lua 值校验器:针对不同上下文定义校验规则 /// Lua 值校验器:针对不同上下文定义校验规则
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -212,17 +212,79 @@ impl LuaValidator {
} }
impl LuaValidator { impl LuaValidator {
/// 解析并校验 `target` /// 解析并校验 `target`
pub fn parse_target(value: &Value) -> mlua::Result<PathBuf> { pub fn parse_target(value: &Value) -> mlua::Result<TargetConfig> {
let ctx = Self::Target; let ctx = Self::Target;
match value { match value {
Value::String(s) => { Value::String(s) => {
let os_str = lua_string_2_os_string(&s)?; let os_str = lua_string_2_os_string(&s)?;
Self::ensure_no_nul(&ctx, &os_str)?; 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!( other => Err(validation_error(format!(
"{} 需为有效的路径且类型必须是字符串,实际类型是 {}", "{} 需为有效的路径且类型必须是字符串或多版本配置表,实际类型是 {}",
ctx, ctx,
other.type_name() other.type_name()
))), ))),
@@ -471,10 +533,10 @@ impl LuaValidator {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::Mirror; use crate::mirror::MirrorTarget;
use mlua::{FromLua, Lua}; use mlua::{FromLua, Lua};
fn parse(src: &str) -> mlua::Result<Mirror> { fn parse(src: &str) -> mlua::Result<MirrorTarget> {
let lua = Lua::new(); let lua = Lua::new();
// 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表) // 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表)
let get_env = lua let get_env = lua
@@ -493,7 +555,7 @@ mod tests {
lua.globals().set("get_env", get_env).unwrap(); lua.globals().set("get_env", get_env).unwrap();
let value = lua.load(src).eval::<Value>()?; let value = lua.load(src).eval::<Value>()?;
let t = Mirror::from_lua(value, &lua); let t = MirrorTarget::from_lua(value, &lua);
println!("读取出的数据:{:?}", t.clone()?); println!("读取出的数据:{:?}", t.clone()?);
t t
} }
@@ -534,7 +596,10 @@ mod tests {
"#, "#,
) )
.unwrap(); .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.args, vec!["--no-pager", "2"]);
assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home")); assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home"));
// PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段 // PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段