From e52ba67d7eaa67684e7db6950b832c6f9bf3a1d9 Mon Sep 17 00:00:00 2001 From: CNWei Date: Thu, 13 Aug 2026 20:11:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(env):=20=E6=94=AF=E6=8C=81=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F=E6=95=B0=E7=BB=84=E8=B7=A8=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E8=87=AA=E5=8A=A8=E6=8B=BC=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持在 Lua 中配置数组格式的环境变量(如 PATH),由 Rust 按 OS 自动使用 `;` 或 `:` 拼接 - 保持普通字符串格式环境变量的直接覆盖逻辑 - 新增 `ShimConfig::to_command()` 用于快速创建 Command 对象 - 新增 error.rs 明确错误类型 - 新增 build.rs 优化编译 --- Cargo.lock | 21 +++ Cargo.toml | 1 + build.rs | 47 +++++++ shims.lua | 23 +++- src/error.rs | 27 ++++ src/main.rs | 2 + src/shims.rs | 361 ++++++++++++++++++++++++++++++++++----------------- 7 files changed, 356 insertions(+), 126 deletions(-) create mode 100644 build.rs create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 281cec3..baf95a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,7 @@ version = "0.1.0" dependencies = [ "fs-err", "mlua", + "thiserror", "unicode-bom", "winapi", ] @@ -260,6 +261,26 @@ dependencies = [ "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]] name = "unicode-bom" version = "2.0.3" diff --git a/Cargo.toml b/Cargo.toml index f218510..1c4fdba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ panic = "abort" [dependencies] fs-err = "3.3.1" unicode-bom = "2.0.3" +thiserror={version = "2.0.20"} mlua = { version = "0.12.0", features = ["lua54", "vendored"] } winapi = { version = "0.3", features = [ "wincon", diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5eee996 --- /dev/null +++ b/build.rs @@ -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); + } +} diff --git a/shims.lua b/shims.lua index 0851c16..ff17b3d 100644 --- a/shims.lua +++ b/shims.lua @@ -2,6 +2,9 @@ -- __SHIM_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools) local base_dir = __SHIM_DIR__ +-- 1. 自定义局部变量,方便复用与后续维护 +local python_home = base_dir .. "/tools/python39" + return { --------------------------------------------------- -- 1. 标准相对路径 + 正斜杠拼接 (最推荐,绿色便携) @@ -9,10 +12,10 @@ return { ["git"] = { path = base_dir .. "/git/bin/git.exe", -- 追加参数 - args = {"--no-pager"}, + args = { "--no-pager" }, -- 注入环境变量,使用 get_env 获取宿主机当前值 env = { - PATH = base_dir .. "/git/bin;" .. get_env("PATH") + PATH = { base_dir .. "/git/bin", get_env("PATH") } } }, @@ -22,9 +25,21 @@ return { ["python"] = { -- 在 [[]] 内部,\ 不需要写成 \\,直接粘贴即可 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") + } + } + }, --------------------------------------------------- diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..1bb25c7 --- /dev/null +++ b/src/error.rs @@ -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), +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ddd6d1f..0984d12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ use std::{ ptr::null_mut, }; mod shims; +mod error; + use shims::Shim; use winapi::{ diff --git a/src/shims.rs b/src/shims.rs index e2201d5..38bd558 100644 --- a/src/shims.rs +++ b/src/shims.rs @@ -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::{ collections::HashMap, env, fs, @@ -6,38 +8,198 @@ use std::{ 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 { + 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 args: Option>, pub envs: Option>, } -impl Shim { - pub fn init() -> Result { - let (_exe_path, _exe_dir, root_dir, tools_dir, exe_name) = Self::get_exe_context()?; +impl ShimConfig { + /// 根据配置快速构建准备执行的 Command 对象 + 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 { + match value { + Value::Table(table) => { + let path_str: String = table.get("path")?; + let args: Option> = table.get("args")?; + // 解析 env Table + let mut envs_map = HashMap::new(); + if let Ok(env_table) = table.get::("env") { + // 获取当前系统的路径分隔符(Windows 为 ";",Unix 为 ":") + #[cfg(windows)] + let sep = ";"; + #[cfg(not(windows))] + let sep = ":"; + + for pair in env_table.pairs::() { + 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 = arr + // 将 arr 作为序列(数组)处理,每个元素转为 String + .sequence_values::() + .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 { + // 只加载安全的标准库,剥离 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(); - // 1. 将根目录 (Root) 注入为 __SHIM_DIR__,作为全局相对路径的锚点 - let root_dir_str = root_dir.to_string_lossy().replace('\\', "/"); - let tools_dir_str = tools_dir.to_string_lossy().replace('\\', "/"); + // 统一使用 POSIX 风格路径规范化路径字符串 + let root_dir_str = normalize_path_for_lua(&shim_env.root_dir); + let tools_dir_str = normalize_path_for_lua(&shim_env.tools_dir); + + // 1. 注入锚点变量 globals .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 - .create_function(|_, key: String| -> Result { + .create_function(|_, key: String| -> mlua::Result { 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 .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::
("package") { if let Ok(path) = package.get::("path") { 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> { - let current_exe = env::current_exe().map_err(|e| { - Error::new( - ErrorKind::Other, - format!("获取代理程序(shim)路径失败: {}", e), - ) - })?; + /// 执行指定脚本文件,直接返回完整的 Lua Table + pub fn evaluate_lua_script(&self, path: &Path) -> Result { + let code = fs::read_to_string(path)?; - let exe_dir = current_exe - .parent() - .unwrap_or_else(|| Path::new("")) - .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 { - let code = fs::read_to_string(config_file)?; // 这里不安全需要优化 - - lua.load(&code) - .set_name(config_file.to_string_lossy().into_owned()) + self.lua + .load(&code) + .set_name(path.to_string_lossy()) .eval::
() - .map_err(|e| { - Error::new( - ErrorKind::InvalidData, - format!("Lua 脚本解析错误 ({}): {}", config_file.display(), e), - ) + .map_err(|e| ShimError::LuaExecutionError { + file: path.display().to_string(), + source: e, }) } - /// 利用 mlua 原生类型转换,将 Lua Table 映射为 Rust Shim 结构体 - fn from_table(table: Table) -> Result { - let path_str: String = table - .get("path") - .map_err(|_| Error::new(ErrorKind::InvalidData, "配置文件中缺少必需的 'path' 字段"))?; - let target_path = PathBuf::from(path_str); - let args: Option> = table.get("args").ok(); - let envs: Option> = table.get("env").ok(); - - Ok(Self { - target_path, - args, - envs, - }) - } - fn resolve_config( - lua: &Lua, - root_dir: &Path, - tools_dir: &Path, - exe_name: &str, - ) -> Result { - // 1. 尝试全局统一入口 shims.lua - let entry_config = root_dir.join("shims.lua"); - if entry_config.exists() { - let root_table = Self::evaluate_lua_script(lua, &entry_config)?; - - // 如果 shims.lua 中存在以当前程序名命名的配置项,直接接管并返回 - if let Ok(Value::Table(v)) = root_table.get::(exe_name) { - return Self::from_table(v); - } - // 如果 shims.lua 存在但未接管 exe_name,代码会自然“穿透”流转到下方寻找独立配置 - } - - // 2. 降级探测特定同名独立配置文件(优先 tools/{exe_name}.lua,次选 root/{exe_name}.lua) - let specific_config_tools = tools_dir.join(format!("{}.lua", exe_name)); - let specific_config_root = root_dir.join(format!("{}.lua", exe_name)); - - let specific_config = if specific_config_tools.exists() { - Some(specific_config_tools) - } else if specific_config_root.exists() { - Some(specific_config_root) - } else { - None - }; - - if let Some(config_file) = specific_config { - let config_table = Self::evaluate_lua_script(lua, &config_file)?; - return if config_table.contains_key("path").unwrap_or(false) { - Self::from_table(config_table) - } else { - Err(Error::new( - ErrorKind::InvalidData, - format!( - "独立配置文件 [{}] 必须包含 'path' 字段", - config_file.display() - ), - )) - }; - } - - // 3. 两阶段探测均失败后的统一报错 - Err(Error::new( - ErrorKind::NotFound, - format!( - "未找到关于 '{}' 的配置。请确保在 shims.lua 中进行了定义,或者存在独立的 {}.lua 配置文件。", - exe_name, exe_name - ), - )) + /// 将 Lua Value 解析转化为 ShimConfig 数据对象 + pub fn parse_config(&self, value: Value) -> Result { + ShimConfig::from_lua(value, &self.lua).map_err(|e| ShimError::InvalidConfig(e.to_string())) + } +} +pub struct Shim; + +impl Shim { + pub fn init() -> Result { + let current_exe = env::current_exe() + .map_err(|e| Error::new(ErrorKind::Other, format!("获取代理程序路径失败: {}", e)))?; + println!("当前目录 {}", current_exe.display()); + let shim_env = ShimEnv::new(current_exe)?; + let runtime = LuaRuntime::new(&shim_env)?; + + Self::resolve_config(&runtime, &shim_env) + } + + fn resolve_config(engine: &LuaRuntime, env: &ShimEnv) -> Result { + // 策略 1: 尝试加载全局配置文件 shims.lua + let global_config = env.root_dir.join("shims.lua"); + if global_config.is_file() { + let root_table = engine.evaluate_lua_script(&global_config)?; + + // 检查 shims.lua 中是否存在以 target_name 命名的 Table 节点 + if let Ok(target_val) = root_table.get::(env.target_name.as_str()) { + if matches!(target_val, Value::Table(_)) { + return engine.parse_config(target_val); + } + } + // 穿透:若全局配置文件存在但未包含当前程序的 key,继续向下探查 + } + + // 策略 2: 降级寻找独立文件 ({exe}.lua),优先顺序:tools/ > root/ + let target_filename = format!("{}.lua", env.target_name); + let candidates = [ + env.tools_dir.join(&target_filename), + env.root_dir.join(&target_filename), + ]; + + for config_path in &candidates { + if config_path.is_file() { + let table = engine.evaluate_lua_script(config_path)?; + return engine.parse_config(Value::Table(table)); + } + } + + // 策略 3: 所有查找失败,抛出错误 + Err(ShimError::ConfigNotFound(format!( + "未找到关于 '{}' 的配置。请检查 shims.lua 或特定的 {}.lua 文件", + env.target_name, env.target_name + ))) } }