如何构建并发布含生成代码的Rust crate(不附带proto文件)
概述
我有一个包含protobuf定义的git仓库,希望基于这些protobuf定义创建Python包(此部分已实现)和Rust crate(此部分存在问题),两者均包含生成的代码。
当前问题
Cargo不接受proto目录位于Cargo.toml目录树之外。由于这些proto文件需要与python目录共享,我不想将其移入Cargo.toml目录树,否则会造成混乱。
于是我在构建脚本中检查两个相对父目录以查找proto文件:一个是运行cargo check时的目录(代码当前所在位置),另一个是运行cargo package时的目录(代码会被放在target/内的某个位置)。
这种双目录检查的临时方案在本地可行,但发布crate时会失败,因为生成的代码未被打包。当尝试使用该crate时,构建脚本会再次运行,但此时已找不到proto定义。而我只想发布手写代码和生成的源代码,不想附带proto文件。
疑问
在不附带proto文件、且不将生成代码提交到git的前提下,打包并发布含生成代码的Rust crate的最佳方式是什么?
详情
目录结构
src ├── proto │ └── helloworld.proto ├── python │ ├── my-msgs │ │ ├── generated │ │ │ ├── <generated code> │ ├── requirements.txt │ └── setup.py └── rust └── my-msgs ├── build.rs ├── Cargo.lock ├── Cargo.toml └── src ├── lib.rs └── test ├── helloworld.rs └── mod.rs
当前构建脚本(build.rs)
use std::fs; use std::path::{Path, PathBuf}; fn find_proto_files(input_dir: &Path) -> impl Iterator<Item = PathBuf> { // given a directory, return the proto files in it let dir_read_result = fs::read_dir(input_dir); match dir_read_result { Ok(it) => it .map(|it| it.expect("Unable to read directory entry").path()) .filter(|it| match it.extension() { Some(e) => e == "proto", None => false, }) .map(|it| { Path::new(&it).to_owned() }), Err(err) => panic!( "Unable to read directory contents of {}: {:?}", input_dir.display(), err ), } } fn determine_input_dir() -> Option<PathBuf> { let candidates = ["../../proto", "../../../../../proto"]; match candidates.iter().map(Path::new).find(|it| it.is_dir()) { Some(it) => Some(it.to_owned()), None => { eprintln!( "Unable to find directory containing proto files. Checked for {:?}", candidates ); None } } } fn main() { if let Some(input_dir) = determine_input_dir() { println!("Reading proto files from {}", input_dir.display()); protobuf_codegen::Codegen::new() .protoc() .includes(&[ input_dir.to_owned(), Path::new("/usr/local/include/google/protobuf/").to_owned(), ]) .inputs(find_proto_files(&input_dir)) .cargo_out_dir("generated") .run_from_script(); } }
核心思路
在打包前预先生成代码,让Cargo将生成的代码包含进发布包,同时让构建脚本在使用已发布crate时跳过代码生成(因为此时无proto文件可用)。
具体步骤
1. 调整构建脚本逻辑
修改build.rs,仅在本地能找到proto文件时才生成代码,否则跳过:
fn main() { // 仅在本地开发环境(能找到proto文件)时生成代码 if let Some(input_dir) = determine_input_dir() { println!("Reading proto files from {}", input_dir.display()); protobuf_codegen::Codegen::new() .protoc() .includes(&[ input_dir.to_owned(), Path::new("/usr/local/include/google/protobuf/").to_owned(), ]) .inputs(find_proto_files(&input_dir)) .cargo_out_dir("generated") .run_from_script(); } else { // 已发布crate使用阶段,直接使用打包好的生成代码 println!("No proto directory found, skipping code generation"); } }
2. 预先生成代码并配置Cargo包含它
- 本地执行
cargo build,触发代码生成,此时生成的代码会在target/debug/build/<你的crate名>/out/generated目录下。 - 将生成的代码复制到rust crate的
src/generated目录下(创建该目录如果不存在)。 - 在
Cargo.toml中添加include字段,明确指定要打包的文件,确保生成的代码被包含:
include = [ "src/**/*", "build.rs", "Cargo.toml" ]
这样cargo package会把src下的所有文件(包括生成的代码)打包进crate。
3. 打包时跳过重新构建
执行cargo package --no-build,Cargo会直接使用已存在的src目录下的代码打包,不会重新运行构建脚本(避免找不到proto文件的问题)。
更灵活的进阶方案
如果不想手动复制代码,可以修改构建脚本,将生成的代码直接输出到src目录下的指定位置,同时在.gitignore中排除该目录:
- 修改
build.rs中的输出路径:
// 替换原来的cargo_out_dir("generated") .out_dir("src/generated")
- 在
.gitignore中添加:
src/generated/
这样本地开发时构建脚本会直接在src/generated生成代码,打包时Cargo.toml的include会包含这些文件,同时不会被提交到git。
关键注意事项
- 不要将
proto目录添加到Cargo.toml的include中,确保发布的crate不包含proto文件。 - 测试已打包的crate:可以用
cargo install --path .本地安装测试,验证是否能正常使用。
内容的提问来源于stack exchange,提问作者Patrick

