Rust工作空间跨二进制调用时如何安全获取目标可执行文件路径
问题解决方案
你当前基于current_dir()拼接路径的方案天生脆弱:程序启动时的工作目录由调用方决定,从其他路径启动程序、运行中修改工作目录都会导致路径解析失败,且错误只能在运行时触发。
下面两种方案都可以实现编译期路径校验,目录结构错误、目标文件不存在时会直接在cargo build阶段报错,和include_bytes!的行为一致。
方案一:构建脚本嵌入编译期确定的B可执行路径(推荐)
这个方案不需要把B打包进A,运行时直接调用编译产物里的B,体积无冗余,适配debug/release等不同编译剖面。
- 先在a crate的配置里开启构建脚本,修改
a/Cargo.toml:[package] # 保留原有name、version、edition等配置 build = "build.rs" - 在
a/目录下新建build.rs构建脚本,编译阶段会自动执行:use std::path::PathBuf; fn main() { // 编译期固定拿到a crate的根目录路径,和运行时环境无关 let a_root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); // 拼接b crate的根目录,目录结构不对时下面的校验会直接中断编译 let b_root = a_root.parent().unwrap().join("b"); assert!( b_root.join("Cargo.toml").exists(), "未找到b crate的配置文件,请检查工作空间目录结构" ); // 注册依赖:b的配置变动时自动重编a,避免路径失效 println!("cargo:rerun-if-changed={}", b_root.join("Cargo.toml").display()); // 拿到Cargo统一的编译输出目录 let target_dir = match std::env::var("CARGO_TARGET_DIR") { Ok(p) => PathBuf::from(p), Err(_) => { let mut dir = a_root.clone(); loop { let candidate = dir.join("target"); if candidate.exists() { break candidate; } dir = dir.parent().expect("找不到工作空间target编译目录").to_path_buf(); } } }; // 自动匹配当前编译剖面(debug/release) let profile = std::env::var("PROFILE").unwrap(); // 拼接b的可执行文件名,自动适配windows的.exe后缀 let b_exe_name = if cfg!(windows) { "b.exe" } else { "b" }; let b_exe_path = target_dir.join(profile).join(b_exe_name); // 将最终路径作为编译期常量注入a的编译环境 println!("cargo:rustc-env=B_EXE_PATH={}", b_exe_path.display()); } - 在
a/src/main.rs里直接使用编译期注入的路径即可,不需要任何运行时拼接:use std::process::Command; fn main() { // env!是编译期求值宏,读取失败直接编译报错,不会留到运行时 let b_path = env!("B_EXE_PATH"); let status = Command::new(b_path) .args(["需要传递给B的参数1", "参数2"]) .status() .expect("调用B程序失败"); println!("B执行退出码:{}", status); }
方案二:将B二进制直接嵌入A的可执行文件
适合需要单文件分发的场景,编译期直接把B的二进制内容打进A的可执行段,运行时不需要依赖外部的B文件。
- 先确保编译A之前会先编译B,可以在工作空间根的
Cargo.toml里配置默认编译成员,或者在a的构建脚本里主动触发b的编译。 - 在
a/src/main.rs里直接用include_bytes!嵌入B的二进制,路径错误会直接编译失败:use std::io::Write; use std::process::Command; fn main() { // 编译期读取B的二进制,路径不对直接编译报错 // 路径是相对于当前main.rs文件的,可结合build.rs动态适配debug/release路径 const B_BINARY: &[u8] = include_bytes!("../../target/debug/b"); // 运行时把B写到临时目录再执行 let mut temp_file = tempfile::NamedTempFile::new().expect("创建临时文件失败"); temp_file.write_all(B_BINARY).expect("写入临时文件失败"); let temp_path = temp_file.into_temp_path(); // Unix系统需要给临时文件加执行权限 #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&temp_path, std::fs::Permissions::from_mode(0o755)) .expect("设置执行权限失败"); } let output = Command::new(&temp_path) .output() .expect("执行B失败"); }注意:这个方案会让A的二进制体积增加B的文件大小,如果B体积较大优先选方案一。
注意事项
- 禁止用
current_dir()返回值拼接程序依赖的固定路径,该值由启动A的父进程决定,从其他工作目录启动、桌面双击启动时都会和预期不一致。 CARGO_MANIFEST_DIR是Cargo在编译期提供的固定路径,指向当前crate的Cargo.toml所在目录,和运行时环境完全无关,适合做编译期路径拼接的基准。- 所有用
env!、include_bytes!读取的内容都是编译期常量,只要编译通过,就不会出现运行时找不到路径的问题。
内容的提问来源于stack exchange,提问作者J. Doe
相关产品推荐
相关产品推荐

