1 //! API to invoke `protoc` command. 2 //! 3 //! `protoc` command must be in `$PATH`, along with `protoc-gen-LANG` command. 4 //! 5 //! Note that to generate `rust` code from `.proto` files, `protoc-rust` crate 6 //! can be used, which does not require `protoc-gen-rust` present in `$PATH`. 7 8 #![deny(missing_docs)] 9 #![deny(rustdoc::broken_intra_doc_links)] 10 11 use std::ffi::OsStr; 12 use std::ffi::OsString; 13 use std::fmt; 14 use std::io; 15 use std::path::Path; 16 use std::path::PathBuf; 17 use std::process; 18 use std::process::Stdio; 19 20 use log::info; 21 22 #[derive(Debug, thiserror::Error)] 23 enum Error { 24 #[error("protoc command exited with non-zero code")] 25 ProtocNonZero, 26 #[error("protoc command {0} exited with non-zero code")] 27 ProtocNamedNonZero(String), 28 #[error("protoc command {0} exited with non-zero code; stderr: {1:?}")] 29 ProtocNamedNonZeroStderr(String, String), 30 #[error("input is empty")] 31 InputIsEmpty, 32 #[error("output is empty")] 33 OutputIsEmpty, 34 #[error("output does not start with prefix")] 35 OutputDoesNotStartWithPrefix, 36 #[error("version is empty")] 37 VersionIsEmpty, 38 #[error("version does not start with digit")] 39 VersionDoesNotStartWithDigit, 40 #[error("failed to spawn command `{0}`")] 41 FailedToSpawnCommand(String, #[source] io::Error), 42 #[error("protoc output is not UTF-8")] 43 ProtocOutputIsNotUtf8, 44 } 45 46 /// `Protoc --descriptor_set_out...` args 47 #[derive(Debug)] 48 pub(crate) struct DescriptorSetOutArgs { 49 protoc: Protoc, 50 /// `--file_descriptor_out=...` param 51 out: Option<PathBuf>, 52 /// `-I` args 53 includes: Vec<PathBuf>, 54 /// List of `.proto` files to compile 55 inputs: Vec<PathBuf>, 56 /// `--include_imports` 57 include_imports: bool, 58 /// Extra command line flags (like `--experimental_allow_proto3_optional`) 59 extra_args: Vec<OsString>, 60 /// Capture stderr instead of inheriting it. 61 capture_stderr: bool, 62 } 63 64 impl DescriptorSetOutArgs { 65 /// Set `--file_descriptor_out=...` param out(&mut self, out: impl AsRef<Path>) -> &mut Self66 pub fn out(&mut self, out: impl AsRef<Path>) -> &mut Self { 67 self.out = Some(out.as_ref().to_owned()); 68 self 69 } 70 71 /// Append a path to `-I` args include(&mut self, include: impl AsRef<Path>) -> &mut Self72 pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self { 73 self.includes.push(include.as_ref().to_owned()); 74 self 75 } 76 77 /// Append multiple paths to `-I` args includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self78 pub fn includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self { 79 for include in includes { 80 self.include(include); 81 } 82 self 83 } 84 85 /// Append a `.proto` file path to compile input(&mut self, input: impl AsRef<Path>) -> &mut Self86 pub fn input(&mut self, input: impl AsRef<Path>) -> &mut Self { 87 self.inputs.push(input.as_ref().to_owned()); 88 self 89 } 90 91 /// Append multiple `.proto` file paths to compile inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self92 pub fn inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self { 93 for input in inputs { 94 self.input(input); 95 } 96 self 97 } 98 99 /// Set `--include_imports` include_imports(&mut self, include_imports: bool) -> &mut Self100 pub fn include_imports(&mut self, include_imports: bool) -> &mut Self { 101 self.include_imports = include_imports; 102 self 103 } 104 105 /// Add command line flags like `--experimental_allow_proto3_optional`. extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self106 pub fn extra_arg(&mut self, arg: impl Into<OsString>) -> &mut Self { 107 self.extra_args.push(arg.into()); 108 self 109 } 110 111 /// Add command line flags like `--experimental_allow_proto3_optional`. extra_args(&mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> &mut Self112 pub fn extra_args(&mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> &mut Self { 113 for arg in args { 114 self.extra_arg(arg); 115 } 116 self 117 } 118 119 /// Capture stderr instead of inheriting it. capture_stderr(&mut self, capture_stderr: bool) -> &mut Self120 pub(crate) fn capture_stderr(&mut self, capture_stderr: bool) -> &mut Self { 121 self.capture_stderr = capture_stderr; 122 self 123 } 124 125 /// Execute `protoc --descriptor_set_out=` write_descriptor_set(&self) -> anyhow::Result<()>126 pub fn write_descriptor_set(&self) -> anyhow::Result<()> { 127 if self.inputs.is_empty() { 128 return Err(Error::InputIsEmpty.into()); 129 } 130 131 let out = self.out.as_ref().ok_or_else(|| Error::OutputIsEmpty)?; 132 133 // -I{include} 134 let include_flags = self.includes.iter().map(|include| { 135 let mut flag = OsString::from("-I"); 136 flag.push(include); 137 flag 138 }); 139 140 // --descriptor_set_out={out} 141 let mut descriptor_set_out_flag = OsString::from("--descriptor_set_out="); 142 descriptor_set_out_flag.push(out); 143 144 // --include_imports 145 let include_imports_flag = match self.include_imports { 146 false => None, 147 true => Some("--include_imports".into()), 148 }; 149 150 let mut cmd_args = Vec::new(); 151 cmd_args.extend(include_flags); 152 cmd_args.push(descriptor_set_out_flag); 153 cmd_args.extend(include_imports_flag); 154 cmd_args.extend(self.inputs.iter().map(|path| path.as_os_str().to_owned())); 155 cmd_args.extend(self.extra_args.iter().cloned()); 156 self.protoc.run_with_args(cmd_args, self.capture_stderr) 157 } 158 } 159 160 /// Protoc command. 161 #[derive(Clone, Debug)] 162 pub(crate) struct Protoc { 163 exec: OsString, 164 } 165 166 impl Protoc { 167 /// New `protoc` command from `$PATH` from_env_path() -> Protoc168 pub(crate) fn from_env_path() -> Protoc { 169 match which::which("protoc") { 170 Ok(path) => Protoc { 171 exec: path.into_os_string(), 172 }, 173 Err(e) => { 174 panic!("protoc binary not found: {}", e); 175 } 176 } 177 } 178 179 /// New `protoc` command from specified path 180 /// 181 /// # Examples 182 /// 183 /// ```no_run 184 /// # mod protoc_bin_vendored { 185 /// # pub fn protoc_bin_path() -> Result<std::path::PathBuf, std::io::Error> { 186 /// # unimplemented!() 187 /// # } 188 /// # } 189 /// 190 /// // Use a binary from `protoc-bin-vendored` crate 191 /// let protoc = protoc::Protoc::from_path( 192 /// protoc_bin_vendored::protoc_bin_path().unwrap()); 193 /// ``` from_path(path: impl AsRef<OsStr>) -> Protoc194 pub(crate) fn from_path(path: impl AsRef<OsStr>) -> Protoc { 195 Protoc { 196 exec: path.as_ref().to_owned(), 197 } 198 } 199 200 /// Check `protoc` command found and valid _check(&self) -> anyhow::Result<()>201 pub(crate) fn _check(&self) -> anyhow::Result<()> { 202 self.version()?; 203 Ok(()) 204 } 205 spawn(&self, cmd: &mut process::Command) -> anyhow::Result<process::Child>206 fn spawn(&self, cmd: &mut process::Command) -> anyhow::Result<process::Child> { 207 info!("spawning command {:?}", cmd); 208 209 cmd.spawn() 210 .map_err(|e| Error::FailedToSpawnCommand(format!("{:?}", cmd), e).into()) 211 } 212 213 /// Obtain `protoc` version version(&self) -> anyhow::Result<Version>214 pub(crate) fn version(&self) -> anyhow::Result<Version> { 215 let child = self.spawn( 216 process::Command::new(&self.exec) 217 .stdin(process::Stdio::null()) 218 .stdout(process::Stdio::piped()) 219 .stderr(process::Stdio::piped()) 220 .args(&["--version"]), 221 )?; 222 223 let output = child.wait_with_output()?; 224 if !output.status.success() { 225 return Err(Error::ProtocNonZero.into()); 226 } 227 let output = String::from_utf8(output.stdout).map_err(|_| Error::ProtocOutputIsNotUtf8)?; 228 let output = match output.lines().next() { 229 None => return Err(Error::OutputIsEmpty.into()), 230 Some(line) => line, 231 }; 232 let prefix = "libprotoc "; 233 if !output.starts_with(prefix) { 234 return Err(Error::OutputDoesNotStartWithPrefix.into()); 235 } 236 let output = &output[prefix.len()..]; 237 if output.is_empty() { 238 return Err(Error::VersionIsEmpty.into()); 239 } 240 let first = output.chars().next().unwrap(); 241 if !first.is_digit(10) { 242 return Err(Error::VersionDoesNotStartWithDigit.into()); 243 } 244 Ok(Version { 245 version: output.to_owned(), 246 }) 247 } 248 249 /// Execute `protoc` command with given args, check it completed correctly. run_with_args(&self, args: Vec<OsString>, capture_stderr: bool) -> anyhow::Result<()>250 fn run_with_args(&self, args: Vec<OsString>, capture_stderr: bool) -> anyhow::Result<()> { 251 let mut cmd = process::Command::new(&self.exec); 252 cmd.stdin(process::Stdio::null()); 253 cmd.args(args); 254 255 if capture_stderr { 256 cmd.stderr(Stdio::piped()); 257 } 258 259 let mut child = self.spawn(&mut cmd)?; 260 261 if capture_stderr { 262 let output = child.wait_with_output()?; 263 if !output.status.success() { 264 let stderr = String::from_utf8_lossy(&output.stderr); 265 let stderr = stderr.trim_end().to_owned(); 266 return Err(Error::ProtocNamedNonZeroStderr(format!("{:?}", cmd), stderr).into()); 267 } 268 } else { 269 if !child.wait()?.success() { 270 return Err(Error::ProtocNamedNonZero(format!("{:?}", cmd)).into()); 271 } 272 } 273 274 Ok(()) 275 } 276 277 /// Get default DescriptorSetOutArgs for this command. descriptor_set_out_args(&self) -> DescriptorSetOutArgs278 pub(crate) fn descriptor_set_out_args(&self) -> DescriptorSetOutArgs { 279 DescriptorSetOutArgs { 280 protoc: self.clone(), 281 out: None, 282 includes: Vec::new(), 283 inputs: Vec::new(), 284 include_imports: false, 285 extra_args: Vec::new(), 286 capture_stderr: false, 287 } 288 } 289 } 290 291 /// Protobuf (protoc) version. 292 pub(crate) struct Version { 293 version: String, 294 } 295 296 impl Version { 297 /// `true` if the protoc major version is 3. _is_3(&self) -> bool298 pub fn _is_3(&self) -> bool { 299 self.version.starts_with("3") 300 } 301 } 302 303 impl fmt::Display for Version { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result304 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 305 fmt::Display::fmt(&self.version, f) 306 } 307 } 308 309 #[cfg(test)] 310 mod test { 311 use super::*; 312 313 #[test] version()314 fn version() { 315 Protoc::from_env_path().version().expect("version"); 316 } 317 } 318