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}