exonum_build/
lib.rs

1// Copyright 2020 The Exonum Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! This crate simplifies writing build scripts (`build.rs`) for Exonum and Exonum services.
16//!
17//! Since Protobuf is the Exonum default serialization format, `build.rs` is mostly used
18//! to compile Protobuf files and generate a corresponding code. This code is used later by
19//! the Exonum core and services.
20//!
21//! In order to use the crate, call `ProtobufGenerator` with the required params.
22//! See [`ProtobufGenerator`] docs for an example.
23//!
24//! # File Sets
25//!
26//! There are several predefined sets of Protobuf sources available for use, split according
27//! to the crate the sources are defined in. These sets are described by [`ProtoSources`]:
28//!
29//! - **Crypto sources:** cryptographic types used in services and the code.
30//! - **Common sources:** types that can be used by various parts of Exonum.
31//! - **MerkleDB sources:** types representing proofs of existence of element in database.
32//! - **Core sources:** types used in core and in system services such as supervisor.
33//!
34//! | File path | Set | Description |
35//! |-----------|-----|-------------|
36//! | `exonum/crypto/types.proto` | Crypto | Basic types: `Hash`, `PublicKey` and `Signature` |
37//! | `exonum/common/bit_vec.proto` | Common | Protobuf mapping for `BitVec` |
38//! | `exonum/proof/list_proof.proto` | MerkleDB | `ListProof` and related helpers |
39//! | `exonum/proof/map_proof.proto` | MerkleDB | `MapProof` and related helpers |
40//! | `exonum/blockchain.proto` | Core | Basic core types (e.g., `Block`) |
41//! | `exonum/key_value_sequence.proto` | Core | Key-value sequence used to store additional headers in `Block` |
42//! | `exonum/messages.proto` | Core | Base types for Ed25519-authenticated messages |
43//! | `exonum/proofs.proto` | Core | Block and index proofs |
44//! | `exonum/runtime/auth.proto` | Core | Authorization-related types |
45//! | `exonum/runtime/base.proto` | Core | Basic runtime types (e.g., artifact ID) |
46//! | `exonum/runtime/errors.proto` | Core | Execution errors |
47//! | `exonum/runtime/lifecycle.proto` | Core | Advanced types used in service lifecycle |
48//!
49//! Each file is placed in the Protobuf package matching its path, similar to well-known Protobuf
50//! types. For example, `exonum/runtime/auth.proto` types are in the `exonum.runtime` package.
51//!
52//! [`ProtobufGenerator`]: struct.ProtobufGenerator.html
53//! [`ProtoSources`]: enum.ProtoSources.html
54
55#![deny(unsafe_code, bare_trait_objects)]
56#![warn(missing_docs, missing_debug_implementations)]
57
58use proc_macro2::{Ident, Span, TokenStream};
59use protoc_rust::Customize;
60use quote::{quote, ToTokens};
61use walkdir::WalkDir;
62
63use std::collections::HashSet;
64use std::{
65    env,
66    fs::File,
67    io::{Read, Write},
68    path::{Path, PathBuf},
69};
70
71/// Enum representing various sources of Protobuf files.
72#[derive(Debug, Copy, Clone)]
73pub enum ProtoSources<'a> {
74    /// Path to core Protobuf files.
75    Exonum,
76    /// Path to crypto Protobuf files.
77    Crypto,
78    /// Path to common Protobuf files.
79    Common,
80    /// Path to database-related Protobuf files.
81    Merkledb,
82    /// Manually specified path.
83    Path(&'a str),
84}
85
86impl<'a> ProtoSources<'a> {
87    /// Returns path to protobuf files.
88    pub fn path(&self) -> String {
89        match self {
90            ProtoSources::Exonum => get_exonum_protobuf_files_path(),
91            ProtoSources::Common => get_exonum_protobuf_common_files_path(),
92            ProtoSources::Crypto => get_exonum_protobuf_crypto_files_path(),
93            ProtoSources::Merkledb => get_exonum_protobuf_merkledb_files_path(),
94            ProtoSources::Path(path) => (*path).to_string(),
95        }
96    }
97}
98
99impl<'a> From<&'a str> for ProtoSources<'a> {
100    fn from(path: &'a str) -> Self {
101        ProtoSources::Path(path)
102    }
103}
104
105#[derive(Debug, PartialEq, Eq, Hash)]
106struct ProtobufFile {
107    full_path: PathBuf,
108    relative_path: String,
109}
110
111/// Finds all .proto files in `path` and sub-directories and returns a vector
112/// with metadata on found files.
113fn get_proto_files<P: AsRef<Path>>(path: &P) -> Vec<ProtobufFile> {
114    WalkDir::new(path)
115        .into_iter()
116        .filter_map(|e| {
117            let entry = e.ok()?;
118            if entry.file_type().is_file() && entry.path().extension()?.to_str() == Some("proto") {
119                let full_path = entry.path().to_owned();
120                let relative_path = full_path.strip_prefix(path).unwrap().to_owned();
121                let relative_path = relative_path
122                    .to_str()
123                    .expect("Cannot convert relative path to string");
124
125                Some(ProtobufFile {
126                    full_path,
127                    relative_path: canonicalize_protobuf_path(&relative_path),
128                })
129            } else {
130                None
131            }
132        })
133        .collect()
134}
135
136#[cfg(windows)]
137fn canonicalize_protobuf_path(path_str: &str) -> String {
138    path_str.replace('\\', "/")
139}
140
141#[cfg(not(windows))]
142fn canonicalize_protobuf_path(path_str: &str) -> String {
143    path_str.to_owned()
144}
145
146/// Includes all .proto files with their names into generated file as array of tuples,
147/// where tuple content is (file_name, file_content).
148fn include_proto_files(proto_files: HashSet<&ProtobufFile>, name: &str) -> impl ToTokens {
149    let proto_files_len = proto_files.len();
150    // TODO Think about syn crate and token streams instead of dirty strings.
151    let proto_files = proto_files.iter().map(|file| {
152        let name = &file.relative_path;
153
154        let mut content = String::new();
155        File::open(&file.full_path)
156            .expect("Unable to open .proto file")
157            .read_to_string(&mut content)
158            .expect("Unable to read .proto file");
159
160        quote! {
161            (#name, #content),
162        }
163    });
164
165    let name = Ident::new(name, Span::call_site());
166
167    quote! {
168        /// Original proto files which were be used to generate this module.
169        /// First element in tuple is file name, second is proto file content.
170        #[allow(dead_code)]
171        #[allow(clippy::unseparated_literal_suffix)]
172        pub const #name: [(&str, &str); #proto_files_len] = [
173            #( #proto_files )*
174        ];
175    }
176}
177
178fn get_mod_files(proto_files: &[ProtobufFile]) -> impl Iterator<Item = TokenStream> + '_ {
179    proto_files.iter().map(|file| {
180        let mod_name = file
181            .full_path
182            .file_stem()
183            .unwrap()
184            .to_str()
185            .expect(".proto file name is not convertible to &str");
186
187        let mod_name = Ident::new(mod_name, Span::call_site());
188        if mod_name == "tests" {
189            quote! {
190                #[cfg(test)] pub mod #mod_name;
191            }
192        } else {
193            quote! {
194                pub mod #mod_name;
195            }
196        }
197    })
198}
199
200/// Collects .rs files generated by the rust-protobuf into single module.
201///
202/// - If module name is `tests` it adds `#[cfg(test)]` to declaration.
203/// - Also this method includes source files as `PROTO_SOURCES` constant.
204fn generate_mod_rs(
205    out_dir: impl AsRef<Path>,
206    proto_files: &[ProtobufFile],
207    includes: &[ProtobufFile],
208    mod_file: impl AsRef<Path>,
209) {
210    let mod_files = get_mod_files(proto_files);
211
212    // To avoid cases where input sources are also added as includes, use only
213    // unique paths.
214    let includes = includes
215        .iter()
216        .filter(|file| !proto_files.contains(file))
217        .collect();
218
219    let proto_files = include_proto_files(proto_files.iter().collect(), "PROTO_SOURCES");
220    let includes = include_proto_files(includes, "INCLUDES");
221
222    let content = quote! {
223        #( #mod_files )*
224        #proto_files
225        #includes
226    };
227
228    let dest_path = out_dir.as_ref().join(mod_file);
229    let mut file = File::create(dest_path).expect("Unable to create output file");
230    file.write_all(content.into_token_stream().to_string().as_bytes())
231        .expect("Unable to write data to file");
232}
233
234fn generate_mod_rs_without_sources(
235    out_dir: impl AsRef<Path>,
236    proto_files: &[ProtobufFile],
237    mod_file: impl AsRef<Path>,
238) {
239    let mod_files = get_mod_files(proto_files);
240    let content = quote! {
241        #( #mod_files )*
242    };
243    let dest_path = out_dir.as_ref().join(mod_file);
244    let mut file = File::create(dest_path).expect("Unable to create output file");
245    file.write_all(content.into_token_stream().to_string().as_bytes())
246        .expect("Unable to write data to file");
247}
248
249/// Generates Rust modules from Protobuf files.
250///
251/// The `protoc` executable (i.e., the Protobuf compiler) should be in `$PATH`.
252///
253/// # Examples
254///
255/// Specify in the build script (`build.rs`) of your crate:
256///
257/// ```no_run
258/// use exonum_build::ProtobufGenerator;
259///
260/// ProtobufGenerator::with_mod_name("example_mod.rs")
261///     .with_input_dir("src/proto")
262///     .with_crypto()
263///     .with_common()
264///     .with_merkledb()
265///     .generate();
266/// ```
267///
268/// After the successful run, `$OUT_DIR` will contain a module for each Protobuf file in
269/// `src/proto` and `example_mod.rs` which will include all generated modules
270/// as submodules.
271///
272/// To use the generated Rust types corresponding to Protobuf messages, specify
273/// in `src/proto/mod.rs`:
274///
275/// ```ignore
276/// include!(concat!(env!("OUT_DIR"), "/example_mod.rs"));
277///
278/// // If you use types from `exonum` .proto files.
279/// use exonum::proto::schema::*;
280/// ```
281#[derive(Debug)]
282pub struct ProtobufGenerator<'a> {
283    includes: Vec<ProtoSources<'a>>,
284    mod_name: &'a str,
285    input_dir: &'a str,
286    include_sources: bool,
287}
288
289impl<'a> ProtobufGenerator<'a> {
290    /// Name of the rust module generated from input proto files.
291    ///
292    /// # Panics
293    ///
294    /// If the `mod_name` is empty.
295    pub fn with_mod_name(mod_name: &'a str) -> Self {
296        assert!(!mod_name.is_empty(), "Mod name is not specified");
297        Self {
298            includes: Vec::new(),
299            input_dir: "",
300            mod_name,
301            include_sources: true,
302        }
303    }
304
305    /// A directory containing input protobuf files.
306    /// For single `mod_name` you can provide only one input directory,
307    /// If proto-files in the input directory have dependencies located in another
308    /// directories, you must specify them using `add_path` method.
309    ///
310    /// Predefined dependencies can be specified using corresponding methods
311    /// `with_common`, `with_crypto`, `with_exonum`.
312    ///
313    /// # Panics
314    ///
315    /// If the input directory is already specified.
316    pub fn with_input_dir(mut self, path: &'a str) -> Self {
317        assert!(
318            self.input_dir.is_empty(),
319            "Input directory is already specified"
320        );
321        self.input_dir = path;
322        self.includes.push(ProtoSources::Path(path));
323        self
324    }
325
326    /// An additional directory containing dependent proto-files, can be used
327    /// multiple times.
328    pub fn add_path(mut self, path: &'a str) -> Self {
329        self.includes.push(ProtoSources::Path(path));
330        self
331    }
332
333    /// Common types for all crates.
334    pub fn with_common(mut self) -> Self {
335        self.includes.push(ProtoSources::Common);
336        self
337    }
338
339    /// Proto files from `exonum-crypto` crate (`Hash`, `PublicKey`, etc.).
340    pub fn with_crypto(mut self) -> Self {
341        self.includes.push(ProtoSources::Crypto);
342        self
343    }
344
345    /// Proto files from `exonum-merkledb` crate (`MapProof`, `ListProof`).
346    pub fn with_merkledb(mut self) -> Self {
347        self.includes.push(ProtoSources::Merkledb);
348        self
349    }
350
351    /// Exonum core related proto files,
352    pub fn with_exonum(mut self) -> Self {
353        self.includes.push(ProtoSources::Exonum);
354        self
355    }
356
357    /// Add multiple include directories.
358    pub fn with_includes(mut self, includes: &'a [ProtoSources<'_>]) -> Self {
359        self.includes.extend_from_slice(includes);
360        self
361    }
362
363    /// Switches off inclusion of source Protobuf files into the generated output.
364    pub fn without_sources(mut self) -> Self {
365        self.include_sources = false;
366        self
367    }
368
369    /// Generate proto files from specified sources.
370    ///
371    /// # Panics
372    ///
373    /// If the `input_dir` or `includes` are empty.
374    pub fn generate(self) {
375        assert!(!self.input_dir.is_empty(), "Input dir is not specified");
376        assert!(!self.includes.is_empty(), "Includes are not specified");
377        protobuf_generate(
378            self.input_dir,
379            &self.includes,
380            self.mod_name,
381            self.include_sources,
382        );
383    }
384}
385
386fn protobuf_generate(
387    input_dir: &str,
388    includes: &[ProtoSources<'_>],
389    mod_file_name: &str,
390    include_sources: bool,
391) {
392    let out_dir = env::var("OUT_DIR")
393        .map(PathBuf::from)
394        .expect("Unable to get OUT_DIR");
395
396    // Converts paths to strings and adds input dir to includes.
397    let includes: Vec<_> = includes.iter().map(ProtoSources::path).collect();
398    let mut includes: Vec<&str> = includes.iter().map(String::as_str).collect();
399    includes.push(input_dir);
400
401    let proto_files = get_proto_files(&input_dir);
402
403    if include_sources {
404        let included_files = get_included_files(&includes);
405        generate_mod_rs(&out_dir, &proto_files, &included_files, mod_file_name);
406    } else {
407        generate_mod_rs_without_sources(&out_dir, &proto_files, mod_file_name);
408    }
409
410    protoc_rust::Codegen::new()
411        .out_dir(out_dir)
412        .inputs(proto_files.into_iter().map(|f| f.full_path))
413        .includes(&includes)
414        .customize(Customize {
415            serde_derive: Some(true),
416            ..Default::default()
417        })
418        .run()
419        .expect("protoc")
420}
421
422fn get_included_files(includes: &[&str]) -> Vec<ProtobufFile> {
423    includes
424        .iter()
425        .flat_map(|path| get_proto_files(path))
426        .collect()
427}
428
429/// Get path to the folder containing `exonum` protobuf files.
430///
431/// Needed for code generation of .proto files which import `exonum` provided .proto files.
432fn get_exonum_protobuf_files_path() -> String {
433    env::var("DEP_EXONUM_PROTOBUF_PROTOS").expect("Failed to get exonum protobuf path")
434}
435
436/// Get path to the folder containing `exonum-crypto` protobuf files.
437fn get_exonum_protobuf_crypto_files_path() -> String {
438    env::var("DEP_EXONUM_PROTOBUF_CRYPTO_PROTOS")
439        .expect("Failed to get exonum crypto protobuf path")
440}
441
442/// Get path to the folder containing `exonum-proto` protobuf files.
443fn get_exonum_protobuf_common_files_path() -> String {
444    env::var("DEP_EXONUM_PROTOBUF_COMMON_PROTOS")
445        .expect("Failed to get exonum common protobuf path")
446}
447
448/// Get path to the folder containing `exonum-merkledb` protobuf files.
449fn get_exonum_protobuf_merkledb_files_path() -> String {
450    env::var("DEP_EXONUM_PROTOBUF_MERKLEDB_PROTOS")
451        .expect("Failed to get exonum merkledb protobuf path")
452}