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}