feat(env): 支持环境变量数组跨平台自动拼接

- 支持在 Lua 中配置数组格式的环境变量(如 PATH),由 Rust 按 OS 自动使用 `;` 或 `:` 拼接
- 保持普通字符串格式环境变量的直接覆盖逻辑
- 新增 `ShimConfig::to_command()` 用于快速创建 Command 对象
- 新增 error.rs 明确错误类型
- 新增 build.rs 优化编译
This commit is contained in:
2026-08-13 20:11:55 +08:00
parent 4aa619f07e
commit e52ba67d7e
7 changed files with 356 additions and 126 deletions

21
Cargo.lock generated
View File

@@ -201,6 +201,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"fs-err", "fs-err",
"mlua", "mlua",
"thiserror",
"unicode-bom", "unicode-bom",
"winapi", "winapi",
] ]
@@ -260,6 +261,26 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "unicode-bom" name = "unicode-bom"
version = "2.0.3" version = "2.0.3"

View File

@@ -13,6 +13,7 @@ panic = "abort"
[dependencies] [dependencies]
fs-err = "3.3.1" fs-err = "3.3.1"
unicode-bom = "2.0.3" unicode-bom = "2.0.3"
thiserror={version = "2.0.20"}
mlua = { version = "0.12.0", features = ["lua54", "vendored"] } mlua = { version = "0.12.0", features = ["lua54", "vendored"] }
winapi = { version = "0.3", features = [ winapi = { version = "0.3", features = [
"wincon", "wincon",

47
build.rs Normal file
View File

@@ -0,0 +1,47 @@
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
// CARGO_MANIFEST_DIR -> 项目根目录路径
// CARGO_TARGET_DIR -> 用户自定义了 target 目录,使用该变量
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let target_dir = env::var("CARGO_TARGET_DIR")
.ok()
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(&manifest_dir).join("target"));
// PROFILE -> 获取当前构建配置文件debug/release
let profile = env::var("PROFILE").unwrap();
// let output_dir = target_dir.join(&profile);
// 在 target/debug/ 或 target/release/ 下创建 bin2 目录
let output_dir = target_dir.join(&profile).join("bin2");
// cargo:rerun-if-changed -> 当指定文件变化时,重新运行 xxx
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=shims.lua");
println!("cargo:rustc-env=OUTPUT_DIR={}", output_dir.display());
// 创建目录
let dirs = ["bin", "tools"];
for dir in &dirs {
let path = output_dir.join(dir);
if !path.exists() {
fs::create_dir_all(&path).expect(&format!("Failed to create {} directory", dir));
println!("Created: {:?}", path);
}
}
// 复制文件
let source = PathBuf::from(&manifest_dir).join("shims.lua");
let destination = output_dir.join("shims.lua");
if source.exists() {
fs::copy(&source, &destination).expect("Failed to copy shims.lua");
println!("Copied shims.lua to: {:?}", destination);
} else {
panic!("shims.lua not found at: {:?}", source);
}
}

View File

@@ -2,6 +2,9 @@
-- __SHIM_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools) -- __SHIM_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools)
local base_dir = __SHIM_DIR__ local base_dir = __SHIM_DIR__
-- 1. 自定义局部变量,方便复用与后续维护
local python_home = base_dir .. "/tools/python39"
return { return {
--------------------------------------------------- ---------------------------------------------------
-- 1. 标准相对路径 + 正斜杠拼接 (最推荐,绿色便携) -- 1. 标准相对路径 + 正斜杠拼接 (最推荐,绿色便携)
@@ -12,7 +15,7 @@ return {
args = { "--no-pager" }, args = { "--no-pager" },
-- 注入环境变量,使用 get_env 获取宿主机当前值 -- 注入环境变量,使用 get_env 获取宿主机当前值
env = { env = {
PATH = base_dir .. "/git/bin;" .. get_env("PATH") PATH = { base_dir .. "/git/bin", get_env("PATH") }
} }
}, },
@@ -24,7 +27,19 @@ return {
path = [[C:\Python310\python.exe]], path = [[C:\Python310\python.exe]],
args = { "-B" }, args = { "-B" },
-- 空字典也是合法的,等同于不设置 -- 空字典也是合法的,等同于不设置
env = {} env = {
-- 普通字符串Rust 直接新建/覆盖该环境变量
PYTHON_HOME = python_home,
HOME = base_dir .. "/home",
-- 数组列表:将 python_home 以及 python_home/Scripts 依次拼接到 PATH 前面
PATH = {
python_home,
python_home .. "/Scripts",
get_env("PATH")
}
}
}, },
--------------------------------------------------- ---------------------------------------------------

27
src/error.rs Normal file
View File

@@ -0,0 +1,27 @@
use std::path::PathBuf;
use thiserror::Error; // 推荐引入 thiserror 库,若不使用可手动实现 std::fmt::Display
#[derive(Debug, Error)]
pub enum ShimError {
#[error("路径解析失败: {0}")]
PathResolutionError(String),
#[error("获取环境信息失败: {0}")]
EnvError(String),
#[error("配置文件未找到: {0}")]
ConfigNotFound(String),
#[error("Lua 运行时/语法错误 [{file}]: {source}")]
LuaExecutionError {
file: String,
#[source]
source: mlua::Error,
},
#[error("配置格式非法: {0}")]
InvalidConfig(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
}

View File

@@ -7,6 +7,8 @@ use std::{
ptr::null_mut, ptr::null_mut,
}; };
mod shims; mod shims;
mod error;
use shims::Shim; use shims::Shim;
use winapi::{ use winapi::{

View File

@@ -1,4 +1,6 @@
use mlua::{Error as LuaError, Lua, Table, Value}; use crate::error::ShimError;
use mlua::{Error as LuaError, FromLua, Lua, StdLib, Table, Value};
use std::process::Command;
use std::{ use std::{
collections::HashMap, collections::HashMap,
env, fs, env, fs,
@@ -6,38 +8,198 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
pub struct Shim { /// 将 Path 转换为适合 Lua 使用的安全字符串路径
fn normalize_path_for_lua(path: &Path) -> String {
let path_str = path.to_string_lossy();
// 1. 剥离 Windows UNC 规范路径前缀 (\\?\)
let clean_str = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
// 2. 将反斜杠转换成正斜杠(在非 UNC 路径下Windows 和 Lua 均完美支持 /
// 这样既避免了 Lua 字符串转义隐患,又不会破坏 Windows 路径
clean_str.replace('\\', "/")
}
pub struct ShimEnv {
pub bin_dir: PathBuf,
pub root_dir: PathBuf,
pub tools_dir: PathBuf,
pub target_name: String,
}
impl ShimEnv {
/// 提取当前代理程序的运行环境信息
pub fn new(current_exe: PathBuf) -> Result<Self, ShimError> {
let bin_dir = current_exe
.parent()
.ok_or_else(|| {
ShimError::PathResolutionError(format!(
"无法获取程序 [{}] 的父级 bin 目录",
current_exe.display()
))
})?
.to_path_buf();
println!("bin_dir目录 {}", bin_dir.display());
let root_dir = bin_dir
.parent()
.ok_or_else(|| {
ShimError::PathResolutionError(format!(
"无法获取 bin 目录 [{}] 的父级 root 目录",
bin_dir.display()
))
})?
.to_path_buf();
println!("root_dir 目录 {}", root_dir.display());
let tools_dir = root_dir.join("tools");
println!("tools_dir 目录 {}", tools_dir.display());
let target_name = current_exe
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| {
ShimError::PathResolutionError(format!(
"无法从路径 [{}] 提取有效的程序名称",
current_exe.display()
))
})?
.to_lowercase();
println!("程序名称 {}", target_name);
Ok(Self {
bin_dir,
root_dir,
tools_dir,
target_name,
})
}
}
#[derive(Debug, Clone)]
pub struct ShimConfig {
pub target_path: PathBuf, pub target_path: PathBuf,
pub args: Option<Vec<String>>, pub args: Option<Vec<String>>,
pub envs: Option<HashMap<String, String>>, pub envs: Option<HashMap<String, String>>,
} }
impl Shim { impl ShimConfig {
pub fn init() -> Result<Self, Error> { /// 根据配置快速构建准备执行的 Command 对象
let (_exe_path, _exe_dir, root_dir, tools_dir, exe_name) = Self::get_exe_context()?; pub fn to_command(&self) -> Command {
let mut cmd = Command::new(&self.target_path);
if let Some(args) = &self.args {
cmd.args(args);
}
if let Some(envs) = &self.envs {
for (key, val) in envs {
// 直接应用环境变量Lua 端已经处理好字符串拼接或列表合并)
cmd.env(key, val);
}
}
cmd
}
}
/// 实现 FromLua Trait由 mlua 自动处理 Table 转换
impl FromLua for ShimConfig {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
match value {
Value::Table(table) => {
let path_str: String = table.get("path")?;
let args: Option<Vec<String>> = table.get("args")?;
// 解析 env Table
let mut envs_map = HashMap::new();
if let Ok(env_table) = table.get::<Table>("env") {
// 获取当前系统的路径分隔符Windows 为 ";"Unix 为 ":"
#[cfg(windows)]
let sep = ";";
#[cfg(not(windows))]
let sep = ":";
for pair in env_table.pairs::<String, Value>() {
let (k, v) = pair?;
match v {
// 情况 1: 普通字符串,如 HOME = "C:/path" -> 直接覆盖
Value::String(s) => {
envs_map.insert(k, s.to_str()?.to_string());
}
// 情况 2: 数组 Table如 PATH = { bin_dir, get_env("PATH") }
Value::Table(arr) => {
let paths: Vec<String> = arr
// 将 arr 作为序列(数组)处理,每个元素转为 String
.sequence_values::<String>()
.filter_map(|r| r.ok())
.filter(|s| !s.is_empty()) // 过滤空串,防止生成不必要的连续 ;;
.collect();
let combined = paths.join(sep);
envs_map.insert(k, combined);
}
_ => {}
}
}
}
let envs = if envs_map.is_empty() {
None
} else {
Some(envs_map)
};
Ok(ShimConfig {
target_path: PathBuf::from(path_str),
args,
envs,
})
}
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "ShimConfig".into(),
message: Some("Expected a Lua table".to_string()),
}),
}
}
}
pub struct LuaRuntime {
lua: Lua,
}
impl LuaRuntime {
/// 初始化限定权限的 Lua 沙箱环境
pub fn new(shim_env: &ShimEnv) -> Result<Self, ShimError> {
// 只加载安全的标准库,剥离 os / io 等风险模块
let lua = Lua::new_with(
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE,
mlua::LuaOptions::default(),
)
.map_err(|e| ShimError::EnvError(format!("初始化 Lua 失败: {}", e)))?;
let lua = Lua::new();
let globals = lua.globals(); let globals = lua.globals();
// 1. 将根目录 (Root) 注入为 __SHIM_DIR__作为全局相对路径的锚点 // 统一使用 POSIX 风格路径规范化路径字符串
let root_dir_str = root_dir.to_string_lossy().replace('\\', "/"); let root_dir_str = normalize_path_for_lua(&shim_env.root_dir);
let tools_dir_str = tools_dir.to_string_lossy().replace('\\', "/"); let tools_dir_str = normalize_path_for_lua(&shim_env.tools_dir);
// 1. 注入锚点变量
globals globals
.set("__SHIM_DIR__", root_dir_str.clone()) .set("__SHIM_DIR__", root_dir_str.clone())
.map_err(|e| Error::new(ErrorKind::Other, format!("注入 __SHIM_DIR__ 失败: {}", e)))?; .map_err(|e| ShimError::EnvError(e.to_string()))?;
// 2. 注册 get_env 取环境变量的函数 // 2. 安全暴露 get_env 供配置读取环境变量
let get_env = lua let get_env = lua
.create_function(|_, key: String| -> Result<String, LuaError> { .create_function(|_, key: String| -> mlua::Result<String> {
Ok(env::var(key).unwrap_or_default()) Ok(env::var(key).unwrap_or_default())
}) })
.map_err(|e| Error::new(ErrorKind::Other, format!("注册 get_env 失败: {}", e)))?; .map_err(|e| ShimError::EnvError(e.to_string()))?;
globals globals
.set("get_env", get_env) .set("get_env", get_env)
.map_err(|e| Error::new(ErrorKind::Other, e.to_string()))?; .map_err(|e| ShimError::EnvError(e.to_string()))?;
// 3. 增强 package.path require 自动在当前目录及 conf.d 目录下寻找模块 // 3. 配置 package.path确保 require 行为正常
if let Ok(package) = globals.get::<Table>("package") { if let Ok(package) = globals.get::<Table>("package") {
if let Ok(path) = package.get::<String>("path") { if let Ok(path) = package.get::<String>("path") {
let new_path = format!( let new_path = format!(
@@ -48,119 +210,74 @@ impl Shim {
} }
} }
Self::resolve_config(&lua, &root_dir, &tools_dir, &exe_name) Ok(Self { lua })
} }
fn get_exe_context() -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, String), Error> { /// 执行指定脚本文件,直接返回完整的 Lua Table
let current_exe = env::current_exe().map_err(|e| { pub fn evaluate_lua_script(&self, path: &Path) -> Result<Table, ShimError> {
Error::new( let code = fs::read_to_string(path)?;
ErrorKind::Other,
format!("获取代理程序(shim)路径失败: {}", e),
)
})?;
let exe_dir = current_exe self.lua
.parent() .load(&code)
.unwrap_or_else(|| Path::new("")) .set_name(path.to_string_lossy())
.to_path_buf();
// root_dir 是 bin 的上一级(项目的根目录)
let root_dir = exe_dir
.parent()
.unwrap_or_else(|| Path::new(""))
.to_path_buf();
// tools_dir 是 root 下的 tools 目录
let tools_dir = root_dir.join("tools");
let exe_name = current_exe
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_lowercase();
Ok((current_exe, exe_dir, root_dir, tools_dir, exe_name))
}
/// 执行 Lua 脚本文件并返回 Table
fn evaluate_lua_script(lua: &Lua, config_file: &Path) -> Result<Table, Error> {
let code = fs::read_to_string(config_file)?; // 这里不安全需要优化
lua.load(&code)
.set_name(config_file.to_string_lossy().into_owned())
.eval::<Table>() .eval::<Table>()
.map_err(|e| { .map_err(|e| ShimError::LuaExecutionError {
Error::new( file: path.display().to_string(),
ErrorKind::InvalidData, source: e,
format!("Lua 脚本解析错误 ({}): {}", config_file.display(), e),
)
}) })
} }
/// 利用 mlua 原生类型转换,将 Lua Table 映射为 Rust Shim 结构体
fn from_table(table: Table) -> Result<Self, Error> {
let path_str: String = table
.get("path")
.map_err(|_| Error::new(ErrorKind::InvalidData, "配置文件中缺少必需的 'path' 字段"))?;
let target_path = PathBuf::from(path_str);
let args: Option<Vec<String>> = table.get("args").ok(); /// 将 Lua Value 解析转化为 ShimConfig 数据对象
let envs: Option<HashMap<String, String>> = table.get("env").ok(); pub fn parse_config(&self, value: Value) -> Result<ShimConfig, ShimError> {
ShimConfig::from_lua(value, &self.lua).map_err(|e| ShimError::InvalidConfig(e.to_string()))
Ok(Self { }
target_path, }
args, pub struct Shim;
envs,
}) impl Shim {
} pub fn init() -> Result<ShimConfig, ShimError> {
fn resolve_config( let current_exe = env::current_exe()
lua: &Lua, .map_err(|e| Error::new(ErrorKind::Other, format!("获取代理程序路径失败: {}", e)))?;
root_dir: &Path, println!("当前目录 {}", current_exe.display());
tools_dir: &Path, let shim_env = ShimEnv::new(current_exe)?;
exe_name: &str, let runtime = LuaRuntime::new(&shim_env)?;
) -> Result<Self, Error> {
// 1. 尝试全局统一入口 shims.lua Self::resolve_config(&runtime, &shim_env)
let entry_config = root_dir.join("shims.lua"); }
if entry_config.exists() {
let root_table = Self::evaluate_lua_script(lua, &entry_config)?; fn resolve_config(engine: &LuaRuntime, env: &ShimEnv) -> Result<ShimConfig, ShimError> {
// 策略 1: 尝试加载全局配置文件 shims.lua
// 如果 shims.lua 中存在以当前程序名命名的配置项,直接接管并返回 let global_config = env.root_dir.join("shims.lua");
if let Ok(Value::Table(v)) = root_table.get::<Value>(exe_name) { if global_config.is_file() {
return Self::from_table(v); let root_table = engine.evaluate_lua_script(&global_config)?;
}
// 如果 shims.lua 存在但未接管 exe_name代码会自然“穿透”流转到下方寻找独立配置 // 检查 shims.lua 中是否存在以 target_name 命名的 Table 节点
} if let Ok(target_val) = root_table.get::<Value>(env.target_name.as_str()) {
if matches!(target_val, Value::Table(_)) {
// 2. 降级探测特定同名独立配置文件(优先 tools/{exe_name}.lua次选 root/{exe_name}.lua return engine.parse_config(target_val);
let specific_config_tools = tools_dir.join(format!("{}.lua", exe_name)); }
let specific_config_root = root_dir.join(format!("{}.lua", exe_name)); }
// 穿透:若全局配置文件存在但未包含当前程序的 key继续向下探查
let specific_config = if specific_config_tools.exists() { }
Some(specific_config_tools)
} else if specific_config_root.exists() { // 策略 2: 降级寻找独立文件 ({exe}.lua)优先顺序tools/ > root/
Some(specific_config_root) let target_filename = format!("{}.lua", env.target_name);
} else { let candidates = [
None env.tools_dir.join(&target_filename),
}; env.root_dir.join(&target_filename),
];
if let Some(config_file) = specific_config {
let config_table = Self::evaluate_lua_script(lua, &config_file)?; for config_path in &candidates {
return if config_table.contains_key("path").unwrap_or(false) { if config_path.is_file() {
Self::from_table(config_table) let table = engine.evaluate_lua_script(config_path)?;
} else { return engine.parse_config(Value::Table(table));
Err(Error::new( }
ErrorKind::InvalidData, }
format!(
"独立配置文件 [{}] 必须包含 'path' 字段", // 策略 3: 所有查找失败,抛出错误
config_file.display() Err(ShimError::ConfigNotFound(format!(
), "未找到关于 '{}' 的配置。请检查 shims.lua 或特定的 {}.lua 文件",
)) env.target_name, env.target_name
}; )))
}
// 3. 两阶段探测均失败后的统一报错
Err(Error::new(
ErrorKind::NotFound,
format!(
"未找到关于 '{}' 的配置。请确保在 shims.lua 中进行了定义,或者存在独立的 {}.lua 配置文件。",
exe_name, exe_name
),
))
} }
} }