capnpc/lib.rs
1// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
2// Licensed under the MIT License:
3//
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10//
11// The above copyright notice and this permission notice shall be included in
12// all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20// THE SOFTWARE.
21
22//! # Cap'n Proto Schema Compiler Plugin Library
23//!
24//! This library allows you to do
25//! [Cap'n Proto code generation](https://capnproto.org/otherlang.html#how-to-write-compiler-plugins)
26//! within a Cargo build. You still need the `capnp` binary (implemented in C++).
27//! (If you use a package manager, try looking for a package called
28//! `capnproto`.)
29//!
30//! In your Cargo.toml:
31//!
32//! ```ignore
33//! [dependencies]
34//! capnp = "0.22" # Note this is a different library than capnp*c*
35//!
36//! [build-dependencies]
37//! capnpc = "0.22"
38//! ```
39//!
40//! In your build.rs:
41//!
42//! ```ignore
43//! fn main() {
44//! capnpc::CompilerCommand::new()
45//! .src_prefix("schema")
46//! .file("schema/foo.capnp")
47//! .file("schema/bar.capnp")
48//! .run().expect("schema compiler command");
49//! }
50//! ```
51//!
52//! In your lib.rs:
53//!
54//! ```ignore
55//! capnp::generated_code!(mod foo_capnp);
56//! capnp::generated_code!(mod bar_capnp);
57//! ```
58//!
59//! This will be equivalent to executing the shell command
60//!
61//! ```ignore
62//! capnp compile -orust:$OUT_DIR --src-prefix=schema schema/foo.capnp schema/bar.capnp
63//! ```
64
65pub mod codegen;
66pub mod codegen_types;
67mod pointer_constants;
68
69use std::{
70 collections::HashMap,
71 path::{Path, PathBuf},
72};
73
74// Copied from capnp/src/lib.rs, where this conversion lives behind the "std" feature flag,
75// which we don't want to depend on here.
76pub(crate) fn convert_io_err(err: std::io::Error) -> capnp::Error {
77 use std::io;
78 let kind = match err.kind() {
79 io::ErrorKind::TimedOut => capnp::ErrorKind::Overloaded,
80 io::ErrorKind::BrokenPipe
81 | io::ErrorKind::ConnectionRefused
82 | io::ErrorKind::ConnectionReset
83 | io::ErrorKind::ConnectionAborted
84 | io::ErrorKind::NotConnected => capnp::ErrorKind::Disconnected,
85 _ => capnp::ErrorKind::Failed,
86 };
87 capnp::Error {
88 extra: format!("{err}"),
89 kind,
90 }
91}
92
93fn run_command(
94 mut command: ::std::process::Command,
95 mut code_generation_command: codegen::CodeGenerationCommand,
96) -> ::capnp::Result<()> {
97 let mut p = command.spawn().map_err(convert_io_err)?;
98 code_generation_command.run(p.stdout.take().unwrap())?;
99 let exit_status = p.wait().map_err(convert_io_err)?;
100 if !exit_status.success() {
101 Err(::capnp::Error::failed(format!(
102 "Non-success exit status: {exit_status}"
103 )))
104 } else {
105 Ok(())
106 }
107}
108
109/// A builder object for schema compiler commands.
110#[derive(Default)]
111pub struct CompilerCommand {
112 files: Vec<PathBuf>,
113 src_prefixes: Vec<PathBuf>,
114 import_paths: Vec<PathBuf>,
115 no_standard_import: bool,
116 executable_path: Option<PathBuf>,
117 output_path: Option<PathBuf>,
118 default_parent_module: Vec<String>,
119 raw_code_generator_request_path: Option<PathBuf>,
120 crate_provides_map: HashMap<u64, String>,
121}
122
123impl CompilerCommand {
124 /// Creates a new, empty command.
125 pub fn new() -> Self {
126 Self::default()
127 }
128
129 /// Adds a file to be compiled.
130 pub fn file<P>(&mut self, path: P) -> &mut Self
131 where
132 P: AsRef<Path>,
133 {
134 self.files.push(path.as_ref().to_path_buf());
135 self
136 }
137
138 /// Adds a --src-prefix flag. For all files specified for compilation that start
139 /// with `prefix`, removes the prefix when computing output filenames.
140 pub fn src_prefix<P>(&mut self, prefix: P) -> &mut Self
141 where
142 P: AsRef<Path>,
143 {
144 self.src_prefixes.push(prefix.as_ref().to_path_buf());
145 self
146 }
147
148 /// Adds an --import_path flag. Adds `dir` to the list of directories searched
149 /// for absolute imports.
150 pub fn import_path<P>(&mut self, dir: P) -> &mut Self
151 where
152 P: AsRef<Path>,
153 {
154 self.import_paths.push(dir.as_ref().to_path_buf());
155 self
156 }
157
158 /// Specify that `crate_name` provides generated code for `files`.
159 ///
160 /// This means that when your schema refers to types defined in `files` we
161 /// will generate Rust code that uses identifiers in `crate_name`.
162 ///
163 /// # Arguments
164 ///
165 /// - `crate_name`: The Rust identifier of the crate
166 /// - `files`: the Capnp file ids the crate provides generated code for
167 ///
168 /// # When to use
169 ///
170 /// You only need this when your generated code needs to refer to types in
171 /// the external crate. If you just want to use an annotation and the
172 /// argument to that annotation is a builtin type (e.g. `$Json.name`) this
173 /// isn't necessary.
174 ///
175 /// # Example
176 ///
177 /// If you write a schema like so
178 ///
179 /// ```capnp
180 /// // my_schema.capnp
181 ///
182 /// using Json = import "/capnp/compat/json.capnp";
183 ///
184 /// struct Foo {
185 /// value @0 :Json.Value;
186 /// }
187 /// ```
188 ///
189 /// you'd look at [json.capnp][json.capnp] to see its capnp id.
190 ///
191 /// ```capnp
192 /// // json.capnp
193 ///
194 /// # Copyright (c) 2015 Sandstorm Development Group, Inc. and contributors ...
195 /// @0x8ef99297a43a5e34;
196 /// ```
197 ///
198 /// If you want the `foo::Builder::get_value` method generated for your
199 /// schema to return a `capnp_json::json_capnp::value::Reader` you'd add a
200 /// dependency on `capnp_json` to your `Cargo.toml` and specify it provides
201 /// `json.capnp` in your `build.rs`.
202 ///
203 /// ```rust,no_run
204 /// // build.rs
205 ///
206 /// capnpc::CompilerCommand::new()
207 /// .crate_provides("json_capnp", [0x8ef99297a43a5e34])
208 /// .file("my_schema.capnp")
209 /// .run()
210 /// .unwrap();
211 /// ```
212 ///
213 /// [json.capnp]:
214 /// https://github.com/capnproto/capnproto/blob/master/c%2B%2B/src/capnp/compat/json.capnp
215 pub fn crate_provides(
216 &mut self,
217 crate_name: impl Into<String>,
218 files: impl IntoIterator<Item = u64>,
219 ) -> &mut Self {
220 let crate_name = crate_name.into();
221 for file in files.into_iter() {
222 self.crate_provides_map.insert(file, crate_name.clone());
223 }
224 self
225 }
226
227 /// Adds the --no-standard-import flag, indicating that the default import paths of
228 /// /usr/include and /usr/local/include should not be included.
229 pub fn no_standard_import(&mut self) -> &mut Self {
230 self.no_standard_import = true;
231 self
232 }
233
234 /// Sets the output directory of generated code. Default is OUT_DIR
235 pub fn output_path<P>(&mut self, path: P) -> &mut Self
236 where
237 P: AsRef<Path>,
238 {
239 self.output_path = Some(path.as_ref().to_path_buf());
240 self
241 }
242
243 /// Specify the executable which is used for the 'capnp' tool. When this method is not called, the command looks for a name 'capnp'
244 /// on the system (e.g. in working directory or in PATH environment variable).
245 pub fn capnp_executable<P>(&mut self, path: P) -> &mut Self
246 where
247 P: AsRef<Path>,
248 {
249 self.executable_path = Some(path.as_ref().to_path_buf());
250 self
251 }
252
253 /// Internal function for starting to build a capnp command.
254 fn new_command(&self) -> ::std::process::Command {
255 if let Some(executable) = &self.executable_path {
256 ::std::process::Command::new(executable)
257 } else {
258 ::std::process::Command::new("capnp")
259 }
260 }
261
262 /// Sets the default parent module. This indicates the scope in your crate where you will
263 /// add a module containing the generated code. For example, if you set this option to
264 /// `vec!["foo".into(), "bar".into()]`, and you are generating code for `baz.capnp`, then your crate
265 /// should have this structure:
266 ///
267 /// ```ignore
268 /// pub mod foo {
269 /// pub mod bar {
270 /// pub mod baz_capnp {
271 /// include!(concat!(env!("OUT_DIR"), "/baz_capnp.rs"));
272 /// }
273 /// }
274 /// }
275 /// ```
276 ///
277 /// This option can be overridden by the `parentModule` annotation defined in `rust.capnp`.
278 ///
279 /// If this option is unset, the default is the crate root.
280 pub fn default_parent_module(&mut self, default_parent_module: Vec<String>) -> &mut Self {
281 self.default_parent_module = default_parent_module;
282 self
283 }
284
285 /// If set, the generator will also write a file containing the raw code generator request to the
286 /// specified path.
287 pub fn raw_code_generator_request_path<P>(&mut self, path: P) -> &mut Self
288 where
289 P: AsRef<Path>,
290 {
291 self.raw_code_generator_request_path = Some(path.as_ref().to_path_buf());
292 self
293 }
294
295 /// Runs the command.
296 /// Returns an error if `OUT_DIR` or a custom output directory was not set, or if `capnp compile` fails.
297 pub fn run(&mut self) -> ::capnp::Result<()> {
298 match self.new_command().arg("--version").output() {
299 Err(error) => {
300 return Err(::capnp::Error::failed(format!(
301 "Failed to execute `capnp --version`: {error}. \
302 Please verify that version 0.5.2 or higher of the capnp executable \
303 is installed on your system. See https://capnproto.org/install.html"
304 )))
305 }
306 Ok(output) => {
307 if !output.status.success() {
308 return Err(::capnp::Error::failed(format!(
309 "`capnp --version` returned an error: {:?}. \
310 Please verify that version 0.5.2 or higher of the capnp executable \
311 is installed on your system. See https://capnproto.org/install.html",
312 output.status
313 )));
314 }
315 // TODO Parse the version string?
316 }
317 }
318
319 let mut command = self.new_command();
320
321 // We remove PWD from the env to avoid the following warning.
322 // kj/filesystem-disk-unix.c++:1690:
323 // warning: PWD environment variable doesn't match current directory
324 command.env_remove("PWD");
325
326 command.arg("compile").arg("-o").arg("-");
327
328 if self.no_standard_import {
329 command.arg("--no-standard-import");
330 }
331
332 for import_path in &self.import_paths {
333 command.arg(format!("--import-path={}", import_path.display()));
334 }
335
336 for src_prefix in &self.src_prefixes {
337 command.arg(format!("--src-prefix={}", src_prefix.display()));
338 }
339
340 for file in &self.files {
341 std::fs::metadata(file).map_err(|error| {
342 let current_dir = match std::env::current_dir() {
343 Ok(current_dir) => format!("`{}`", current_dir.display()),
344 Err(..) => "<unknown working directory>".to_string(),
345 };
346
347 ::capnp::Error::failed(format!(
348 "Unable to stat capnp input file `{}` in working directory {}: {}. \
349 Please check that the file exists and is accessible for read.",
350 file.display(),
351 current_dir,
352 error
353 ))
354 })?;
355
356 command.arg(file);
357 }
358
359 let output_path = if let Some(output_path) = &self.output_path {
360 output_path.clone()
361 } else {
362 // Try `OUT_DIR` by default
363 PathBuf::from(::std::env::var("OUT_DIR").map_err(|error| {
364 ::capnp::Error::failed(format!(
365 "Could not access `OUT_DIR` environment variable: {error}. \
366 You might need to set it up or instead create your own output \
367 structure using `CompilerCommand::output_path`"
368 ))
369 })?)
370 };
371
372 command.stdout(::std::process::Stdio::piped());
373 command.stderr(::std::process::Stdio::inherit());
374
375 let mut code_generation_command = crate::codegen::CodeGenerationCommand::new();
376 code_generation_command
377 .output_directory(output_path)
378 .default_parent_module(self.default_parent_module.clone())
379 .crates_provide_map(self.crate_provides_map.clone());
380 if let Some(raw_code_generator_request_path) = &self.raw_code_generator_request_path {
381 code_generation_command
382 .raw_code_generator_request_path(raw_code_generator_request_path.clone());
383 }
384
385 run_command(command, code_generation_command).map_err(|error| {
386 ::capnp::Error::failed(format!(
387 "Error while trying to execute `capnp compile`: {error}."
388 ))
389 })
390 }
391}
392
393#[test]
394#[cfg_attr(miri, ignore)]
395fn compiler_command_new_no_out_dir() {
396 std::env::remove_var("OUT_DIR");
397 let error = CompilerCommand::new().run().unwrap_err().extra;
398 assert!(error.starts_with("Could not access `OUT_DIR` environment variable"));
399}