1#![deny(warnings)]
30#![warn(missing_docs)]
31extern crate grpcio_compiler;
35
36#[macro_use]
37extern crate failure;
38
39extern crate tempfile;
40
41extern crate protobuf;
42extern crate protobuf_codegen;
43extern crate protoc;
44
45use std::convert::AsRef;
46use std::fs::File;
47use std::io::{Read, Write};
48use std::iter::Iterator;
49use std::path::{Path, PathBuf};
50use std::vec::Vec;
51
52use failure::ResultExt;
53
54use tempfile::NamedTempFile;
55
56use protobuf::{compiler_plugin, descriptor};
57use protobuf_codegen::Customize;
58use protoc::{DescriptorSetOutArgs, Protoc};
59
60pub type CompileError = ::failure::Error;
62pub type CompileResult<T> = Result<T, CompileError>;
64
65fn stringify_paths<Paths>(paths: Paths) -> CompileResult<Vec<String>>
66where
67 Paths: IntoIterator,
68 Paths::Item: AsRef<Path>,
69{
70 paths
71 .into_iter()
72 .map(|input| match input.as_ref().to_str() {
73 Some(s) => Ok(s.to_owned()),
74 None => Err(format_err!(
75 "failed to convert {:?} to string",
76 input.as_ref()
77 )),
78 })
79 .collect()
80}
81
82fn write_out_generated_files<P>(
83 generation_results: Vec<compiler_plugin::GenResult>,
84 output_dir: P,
85) -> CompileResult<()>
86where
87 P: AsRef<Path>,
88{
89 for result in generation_results {
90 let file = output_dir.as_ref().join(result.name);
91 File::create(&file)
92 .context(format!("failed to create {:?}", &file))?
93 .write_all(&result.content)
94 .context(format!("failed to write {:?}", &file))?;
95 }
96
97 Ok(())
98}
99
100fn absolutize<P>(path: P) -> CompileResult<PathBuf>
101where
102 P: AsRef<Path>,
103{
104 let p = path.as_ref();
105 if p.is_relative() {
106 match std::env::current_dir() {
107 Ok(cwd) => Ok(cwd.join(p)),
108 Err(err) => Err(format_err!(
109 "Failed to determine CWD needed to absolutize a relative path: {:?}",
110 err
111 )),
112 }
113 } else {
114 Ok(PathBuf::from(p))
115 }
116}
117
118fn normalize<Paths, Bases>(
119 paths: Paths,
120 bases: Bases,
121) -> CompileResult<(Vec<PathBuf>, Vec<PathBuf>, Vec<PathBuf>)>
122where
123 Paths: IntoIterator,
124 Paths::Item: AsRef<Path>,
125 Bases: IntoIterator,
126 Bases::Item: AsRef<Path>,
127{
128 let absolutized_bases = bases
129 .into_iter()
130 .map(absolutize)
131 .collect::<CompileResult<Vec<PathBuf>>>()?;
132
133 let absolutized_paths = paths
142 .into_iter()
143 .map(|p| {
144 let rel_path = p.as_ref().to_path_buf();
145 let absolute_path = absolutize(&rel_path)?;
146 Ok((rel_path, absolute_path))
147 })
148 .flat_map(|r: CompileResult<(PathBuf, PathBuf)>| r)
150 .map(|(rel_path, abs_path)| {
151 if abs_path.exists() {
152 Ok(abs_path)
154 } else {
155 for b in &absolutized_bases {
157 let absolutized_path = b.join(&rel_path);
158 if absolutized_path.exists() {
159 return Ok(absolutized_path);
160 }
161 }
162 Err(format_err!(
163 "Failed to find the absolute path of input {:?}",
164 rel_path
165 ))
166 }
167 })
168 .collect::<CompileResult<Vec<PathBuf>>>()?;
169
170 let relativized_paths: Vec<PathBuf> = absolutized_paths
171 .iter()
172 .map(|p| {
173 for b in &absolutized_bases {
174 if let Ok(rel_path) = p.strip_prefix(&b) {
175 return Ok(PathBuf::from(rel_path));
176 }
177 }
178 Err(format_err!(
179 "The input path {:?} is not contained by any of the include paths {:?}",
180 p,
181 absolutized_bases
182 ))
183 })
184 .collect::<CompileResult<Vec<PathBuf>>>()?;
185
186 Ok((absolutized_bases, absolutized_paths, relativized_paths))
187}
188
189pub fn compile_grpc_protos<Inputs, Includes, Output>(
203 inputs: Inputs,
204 includes: Includes,
205 output: Output,
206 customizations: Option<Customize>,
207) -> CompileResult<()>
208where
209 Inputs: IntoIterator,
210 Inputs::Item: AsRef<Path>,
211 Includes: IntoIterator,
212 Includes::Item: AsRef<Path>,
213 Output: AsRef<Path>,
214{
215 let protoc = Protoc::from_env_path();
216
217 protoc
218 .check()
219 .context("failed to find `protoc`, `protoc` must be availabe in `PATH`")?;
220
221 let (absolutized_includes, absolutized_paths, relativized_inputs) =
222 normalize(inputs, includes)?;
223 let stringified_inputs_absolute = stringify_paths(absolutized_paths)?;
224 let stringified_inputs = stringify_paths(relativized_inputs)?;
225 let stringified_includes = stringify_paths(absolutized_includes)?;
226
227 let descriptor_set = NamedTempFile::new()?;
228
229 protoc
230 .write_descriptor_set(DescriptorSetOutArgs {
231 out: match descriptor_set.as_ref().to_str() {
232 Some(s) => s,
233 None => bail!("failed to convert descriptor set path to string"),
234 },
235 input: stringified_inputs_absolute
236 .iter()
237 .map(String::as_str)
238 .collect::<Vec<&str>>()
239 .as_slice(),
240 includes: stringified_includes
241 .iter()
242 .map(String::as_str)
243 .collect::<Vec<&str>>()
244 .as_slice(),
245 include_imports: true,
246 })
247 .context("failed to write descriptor set")?;
248
249 let mut serialized_descriptor_set = Vec::new();
250 File::open(&descriptor_set)
251 .context("failed to open descriptor set")?
252 .read_to_end(&mut serialized_descriptor_set)
253 .context("failed to read descriptor set")?;
254
255 let descriptor_set =
256 protobuf::parse_from_bytes::<descriptor::FileDescriptorSet>(&serialized_descriptor_set)
257 .context("failed to parse descriptor set")?;
258
259 let customize = customizations.unwrap_or_default();
260
261 write_out_generated_files(
262 grpcio_compiler::codegen::gen(descriptor_set.get_file(), stringified_inputs.as_slice()),
263 &output,
264 )
265 .context("failed to write generated grpc definitions")?;
266
267 write_out_generated_files(
268 protobuf_codegen::gen(
269 descriptor_set.get_file(),
270 stringified_inputs.as_slice(),
271 &customize,
272 ),
273 &output,
274 )
275 .context("failed to write out generated protobuf definitions")?;
276
277 Ok(())
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use std::path::PathBuf;
284 use tempfile::tempdir;
285
286 fn assert_compile_grpc_protos<Input, Output>(input: Input, expected_outputs: Output)
287 where
288 Input: AsRef<Path>,
289 Output: IntoIterator + Copy,
290 Output::Item: AsRef<Path>,
291 {
292 let rel_include_path = PathBuf::from("test/assets/protos");
293 let abs_include_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(&rel_include_path);
294 for include_path in &[&rel_include_path, &abs_include_path] {
295 for inputs in &[vec![input.as_ref()], vec![&include_path.join(&input)]] {
296 let temp_dir = tempdir().unwrap();
297 compile_grpc_protos(inputs, &[include_path], &temp_dir, None).unwrap();
298
299 for output in expected_outputs {
300 assert!(temp_dir.as_ref().join(output).is_file());
301 }
302 }
303 }
304 }
305
306 #[test]
307 fn test_compile_grpc_protos() {
308 assert_compile_grpc_protos("helloworld.proto", &["helloworld_grpc.rs", "helloworld.rs"])
309 }
310
311 #[test]
312 fn test_compile_grpc_protos_subdir() {
313 assert_compile_grpc_protos("foo/bar/baz.proto", &["baz_grpc.rs", "baz.rs"])
314 }
315}