Skip to main content

soil_cli/
lib.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! Substrate CLI library.
8//!
9//! To see a full list of commands available, see [`commands`].
10
11#![warn(missing_docs)]
12#![warn(unused_extern_crates)]
13#![warn(unused_imports)]
14
15use clap::{CommandFactory, FromArgMatches, Parser};
16use log::warn;
17use soil_service::Configuration;
18
19pub mod arg_enums;
20pub mod commands;
21mod config;
22mod error;
23mod params;
24mod runner;
25mod signals;
26
27pub use arg_enums::*;
28pub use clap;
29pub use commands::*;
30pub use config::*;
31pub use error::*;
32pub use params::*;
33pub use runner::*;
34pub use signals::Signals;
35pub use soil_client::tracing::logging::LoggerBuilder;
36pub use soil_service::{ChainSpec, Role};
37pub use subsoil::version::RuntimeVersion;
38
39/// Substrate client CLI
40///
41/// This trait needs to be implemented on the root CLI struct of the application. It will provide
42/// the implementation `name`, `version`, `executable name`, `description`, `author`, `support_url`,
43/// `copyright start year` and most importantly: how to load the chain spec.
44pub trait SubstrateCli: Sized {
45	/// Implementation name.
46	fn impl_name() -> String;
47
48	/// Implementation version.
49	///
50	/// By default, it will look like this:
51	///
52	/// `2.0.0-b950f731c`
53	///
54	/// Where the hash is the short hash of the commit in the Git repository.
55	fn impl_version() -> String;
56
57	/// Executable file name.
58	///
59	/// Extracts the file name from `std::env::current_exe()`.
60	/// Resorts to the env var `CARGO_PKG_NAME` in case of Error.
61	fn executable_name() -> String {
62		std::env::current_exe()
63			.ok()
64			.and_then(|e| e.file_name().map(|s| s.to_os_string()))
65			.and_then(|w| w.into_string().ok())
66			.unwrap_or_else(|| env!("CARGO_PKG_NAME").into())
67	}
68
69	/// Executable file description.
70	fn description() -> String;
71
72	/// Executable file author.
73	fn author() -> String;
74
75	/// Support URL.
76	fn support_url() -> String;
77
78	/// Copyright starting year (x-current year)
79	fn copyright_start_year() -> i32;
80
81	/// Chain spec factory
82	fn load_spec(&self, id: &str) -> std::result::Result<Box<dyn ChainSpec>, String>;
83
84	/// Helper function used to parse the command line arguments. This is the equivalent of
85	/// [`clap::Parser::parse()`].
86	///
87	/// To allow running the node without subcommand, it also sets a few more settings:
88	/// [`clap::Command::propagate_version`], [`clap::Command::args_conflicts_with_subcommands`],
89	/// [`clap::Command::subcommand_negates_reqs`].
90	///
91	/// Creates `Self` from the command line arguments. Print the
92	/// error message and quit the program in case of failure.
93	fn from_args() -> Self
94	where
95		Self: Parser + Sized,
96	{
97		<Self as SubstrateCli>::from_iter(&mut std::env::args_os())
98	}
99
100	/// Helper function used to parse the command line arguments. This is the equivalent of
101	/// [`clap::Parser::parse_from`].
102	///
103	/// To allow running the node without subcommand, it also sets a few more settings:
104	/// [`clap::Command::propagate_version`], [`clap::Command::args_conflicts_with_subcommands`],
105	/// [`clap::Command::subcommand_negates_reqs`].
106	///
107	/// Creates `Self` from any iterator over arguments.
108	/// Print the error message and quit the program in case of failure.
109	fn from_iter<I>(iter: I) -> Self
110	where
111		Self: Parser + Sized,
112		I: IntoIterator,
113		I::Item: Into<std::ffi::OsString> + Clone,
114	{
115		let app = <Self as CommandFactory>::command();
116		let app = Self::setup_command(app);
117
118		let matches = app.try_get_matches_from(iter).unwrap_or_else(|e| e.exit());
119
120		<Self as FromArgMatches>::from_arg_matches(&matches).unwrap_or_else(|e| e.exit())
121	}
122
123	/// Helper function used to parse the command line arguments. This is the equivalent of
124	/// [`clap::Parser::try_parse_from`]
125	///
126	/// To allow running the node without subcommand, it also sets a few more settings:
127	/// [`clap::Command::propagate_version`], [`clap::Command::args_conflicts_with_subcommands`],
128	/// [`clap::Command::subcommand_negates_reqs`].
129	///
130	/// Creates `Self` from any iterator over arguments.
131	/// Print the error message and quit the program in case of failure.
132	///
133	/// **NOTE:** This method WILL NOT exit when `--help` or `--version` (or short versions) are
134	/// used. It will return a [`clap::Error`], where the [`clap::Error::kind`] is a
135	/// [`clap::error::ErrorKind::DisplayHelp`] or [`clap::error::ErrorKind::DisplayVersion`]
136	/// respectively. You must call [`clap::Error::exit`] or perform a [`std::process::exit`].
137	fn try_from_iter<I>(iter: I) -> clap::error::Result<Self>
138	where
139		Self: Parser + Sized,
140		I: IntoIterator,
141		I::Item: Into<std::ffi::OsString> + Clone,
142	{
143		let app = <Self as CommandFactory>::command();
144		let app = Self::setup_command(app);
145
146		let matches = app.try_get_matches_from(iter)?;
147
148		<Self as FromArgMatches>::from_arg_matches(&matches)
149	}
150
151	/// Returns the client ID: `{impl_name}/v{impl_version}`
152	fn client_id() -> String {
153		format!("{}/v{}", Self::impl_name(), Self::impl_version())
154	}
155
156	/// Only create a Configuration for the command provided in argument
157	fn create_configuration<T: CliConfiguration<DVC>, DVC: DefaultConfigurationValues>(
158		&self,
159		command: &T,
160		tokio_handle: tokio::runtime::Handle,
161	) -> error::Result<Configuration> {
162		command.create_configuration(self, tokio_handle)
163	}
164
165	/// Create a runner for the command provided in argument. This will create a Configuration and
166	/// a tokio runtime
167	fn create_runner<T: CliConfiguration<DVC>, DVC: DefaultConfigurationValues>(
168		&self,
169		command: &T,
170	) -> Result<Runner<Self>> {
171		self.create_runner_with_logger_hook(command, |_, _| {})
172	}
173
174	/// Create a runner for the command provided in argument. The `logger_hook` can be used to setup
175	/// a custom profiler or update the logger configuration before it is initialized.
176	///
177	/// Example:
178	/// ```
179	/// use soil_client::tracing::{SpanDatum, TraceEvent};
180	/// struct TestProfiler;
181	///
182	/// impl soil_client::tracing::TraceHandler for TestProfiler {
183	///  	fn handle_span(&self, sd: &SpanDatum) {}
184	/// 		fn handle_event(&self, _event: &TraceEvent) {}
185	/// };
186	///
187	/// fn logger_hook() -> impl FnOnce(&mut soil_cli::LoggerBuilder, &soil_service::Configuration) -> () {
188	/// 	|logger_builder, config| {
189	/// 			logger_builder.with_custom_profiling(Box::new(TestProfiler{}));
190	/// 	}
191	/// }
192	/// ```
193	fn create_runner_with_logger_hook<
194		T: CliConfiguration<DVC>,
195		DVC: DefaultConfigurationValues,
196		F,
197	>(
198		&self,
199		command: &T,
200		logger_hook: F,
201	) -> Result<Runner<Self>>
202	where
203		F: FnOnce(&mut LoggerBuilder, &Configuration),
204	{
205		let tokio_runtime = build_runtime()?;
206
207		// `capture` needs to be called in a tokio context.
208		// Also capture them as early as possible.
209		let signals = tokio_runtime.block_on(async { Signals::capture() })?;
210
211		let config = command.create_configuration(self, tokio_runtime.handle().clone())?;
212
213		command.init(&Self::support_url(), &Self::impl_version(), |logger_builder| {
214			logger_hook(logger_builder, &config)
215		})?;
216
217		Runner::new(config, tokio_runtime, signals)
218	}
219	/// Augments a `clap::Command` with standard metadata like name, version, author, description,
220	/// etc.
221	///
222	/// This is used internally in `from_iter`, `try_from_iter` and can be used externally
223	/// to manually set up a command with Substrate CLI defaults.
224	fn setup_command(mut cmd: clap::Command) -> clap::Command {
225		let mut full_version = Self::impl_version();
226		full_version.push('\n');
227
228		cmd = cmd
229			.name(Self::executable_name())
230			.version(full_version)
231			.author(Self::author())
232			.about(Self::description())
233			.long_about(Self::description())
234			.after_help(format!("Support: {}", Self::support_url()))
235			.propagate_version(true)
236			.args_conflicts_with_subcommands(true)
237			.subcommand_negates_reqs(true);
238
239		cmd
240	}
241}