cargo_mobile2/util/
cli.rs

1use colored::Colorize as _;
2use std::fmt::{Debug, Display};
3
4pub use interface::*;
5
6pub static VERSION_SHORT: &str = concat!("v", env!("CARGO_PKG_VERSION"));
7
8#[derive(Clone)]
9pub struct TextWrapper(pub textwrap::Options<'static>);
10
11impl Default for TextWrapper {
12    fn default() -> Self {
13        Self(
14            textwrap::Options::with_termwidth()
15                .word_splitter(textwrap::word_splitters::WordSplitter::NoHyphenation),
16        )
17    }
18}
19
20impl TextWrapper {
21    pub fn fill(&self, text: &str) -> String {
22        textwrap::fill(text, &self.0)
23    }
24}
25
26pub mod colors {
27    use colored::Color::{self, *};
28
29    pub const ERROR: Color = BrightRed;
30    pub const WARNING: Color = BrightYellow;
31    pub const ACTION_REQUEST: Color = BrightMagenta;
32    pub const VICTORY: Color = BrightGreen;
33}
34
35#[derive(Clone, Copy, Debug)]
36pub enum Label {
37    Error,
38    ActionRequest,
39    Victory,
40}
41
42impl Label {
43    pub fn color(&self) -> colored::Color {
44        match self {
45            Self::Error => colors::ERROR,
46            Self::ActionRequest => colors::ACTION_REQUEST,
47            Self::Victory => colors::VICTORY,
48        }
49    }
50
51    pub fn exit_code(&self) -> i8 {
52        match self {
53            Self::Victory => 0,
54            _ => 1,
55        }
56    }
57
58    pub fn as_str(&self) -> &'static str {
59        match self {
60            Self::Error => "error",
61            Self::ActionRequest => "action request",
62            Self::Victory => "victory",
63        }
64    }
65}
66
67#[derive(Debug)]
68pub struct Report {
69    label: Label,
70    msg: String,
71    details: String,
72}
73
74impl Report {
75    pub fn new(label: Label, msg: impl Display, details: impl Display) -> Self {
76        Self {
77            label,
78            msg: format!("{}", msg),
79            details: format!("{}", details),
80        }
81    }
82
83    pub fn error(msg: impl Display, details: impl Display) -> Self {
84        Self::new(Label::Error, msg, details)
85    }
86
87    pub fn action_request(msg: impl Display, details: impl Display) -> Self {
88        Self::new(Label::ActionRequest, msg, details)
89    }
90
91    pub fn victory(msg: impl Display, details: impl Display) -> Self {
92        Self::new(Label::Victory, msg, details)
93    }
94
95    pub fn exit_code(&self) -> i8 {
96        self.label.exit_code()
97    }
98
99    fn format(&self, wrapper: &TextWrapper) -> String {
100        static INDENT: &str = "    ";
101        let head = if colored::control::SHOULD_COLORIZE.should_colorize() {
102            wrapper.fill(&format!(
103                "{} {}",
104                format!("{}:", self.label.as_str())
105                    .color(self.label.color())
106                    .bold(),
107                self.msg.color(self.label.color())
108            ))
109        } else {
110            wrapper.fill(&format!("{}: {}", self.label.as_str(), &self.msg))
111        };
112        let wrapper = TextWrapper(
113            wrapper
114                .clone()
115                .0
116                .initial_indent(INDENT)
117                .subsequent_indent(INDENT),
118        );
119        format!("{}\n{}\n", head, wrapper.fill(&self.details))
120    }
121
122    pub fn print(&self, wrapper: &TextWrapper) {
123        let s = self.format(wrapper);
124        if matches!(self.label, Label::Error) {
125            eprint!("{}", s)
126        } else {
127            print!("{}", s)
128        }
129    }
130}
131
132pub trait Reportable: Debug {
133    fn report(&self) -> Report;
134}
135
136#[cfg(not(feature = "cli"))]
137mod interface {}
138
139#[cfg(feature = "cli")]
140mod interface {
141    use std::fmt::Debug;
142
143    use crate::{opts, util};
144    use once_cell_regex::exports::once_cell::sync::Lazy;
145    use structopt::{
146        clap::{self, AppSettings},
147        StructOpt,
148    };
149
150    use super::*;
151
152    pub static GLOBAL_SETTINGS: &[AppSettings] = &[
153        AppSettings::ColoredHelp,
154        AppSettings::DeriveDisplayOrder,
155        AppSettings::VersionlessSubcommands,
156    ];
157
158    pub static SETTINGS: &[AppSettings] = &[AppSettings::SubcommandRequiredElseHelp];
159
160    pub fn bin_name(name: &str) -> String {
161        format!("cargo {}", name)
162    }
163
164    pub static VERSION_LONG: Lazy<String> = Lazy::new(|| match util::installed_commit_msg() {
165        Ok(Some(msg)) => format!("{}\n{}", VERSION_SHORT, util::format_commit_msg(msg)),
166        Ok(None) => VERSION_SHORT.to_owned(),
167        Err(err) => {
168            log::error!("failed to get current commit msg: {}", err);
169            VERSION_SHORT.to_owned()
170        }
171    });
172
173    #[derive(Clone, Copy, Debug, StructOpt)]
174    pub struct GlobalFlags {
175        #[structopt(
176        short = "v",
177        long = "verbose",
178        help = "Vomit out extensive logging (-vv for more)",
179        global = true,
180        multiple = true,
181        parse(from_occurrences = opts::NoiseLevel::from_occurrences),
182    )]
183        pub noise_level: opts::NoiseLevel,
184        #[structopt(
185            short = "y",
186            long = "non-interactive",
187            help = "Never prompt for input",
188            global = true
189        )]
190        pub non_interactive: bool,
191    }
192
193    #[derive(Clone, Copy, Debug, StructOpt)]
194    pub struct SkipDevTools {
195        #[structopt(
196            long = "skip-dev-tools",
197            help = "Skip optional tools that help when writing code"
198        )]
199        pub skip_dev_tools: bool,
200    }
201
202    #[derive(Clone, Copy, Debug, StructOpt)]
203    pub struct SkipTargetsInstall {
204        #[structopt(
205            long = "skip-targets-install",
206            help = "Skip installing android/ios targets for rust through rustup "
207        )]
208        pub skip_targets_install: bool,
209    }
210
211    #[derive(Clone, Copy, Debug, StructOpt)]
212    pub struct ReinstallDeps {
213        #[structopt(long = "reinstall-deps", help = "Reinstall dependencies")]
214        pub reinstall_deps: bool,
215    }
216
217    #[derive(Clone, Copy, Debug, StructOpt)]
218    pub struct Profile {
219        #[structopt(
220        long = "release",
221        help = "Build with release optimizations",
222        parse(from_flag = opts::Profile::from_flag),
223    )]
224        pub profile: opts::Profile,
225    }
226
227    #[derive(Clone, Copy, Debug, StructOpt)]
228    pub struct Filter {
229        #[structopt(
230        short = "f",
231        long = "filter",
232        help = "Filter logs by level",
233        possible_values = &opts::FilterLevel::variants(),
234        case_insensitive = true,
235    )]
236        pub filter: Option<opts::FilterLevel>,
237    }
238
239    pub trait Exec: Debug + StructOpt {
240        type Report: Reportable;
241
242        fn global_flags(&self) -> GlobalFlags;
243
244        fn exec(self, wrapper: &TextWrapper) -> Result<(), Self::Report>;
245    }
246
247    fn get_args(name: &str) -> Vec<String> {
248        let mut args: Vec<String> = std::env::args().collect();
249        // Running this as a cargo subcommand gives us our name as an argument,
250        // so let's just discard that...
251        if args.get(1).map(String::as_str) == Some(name) {
252            args.remove(1);
253        }
254        args
255    }
256
257    fn init_logging(noise_level: opts::NoiseLevel) {
258        use env_logger::{Builder, Env};
259        let default_level = match noise_level {
260            opts::NoiseLevel::Polite => "warn",
261            opts::NoiseLevel::LoudAndProud => {
262                "cargo_mobile=info,cargo_android=info,cargo_apple=info,hit=info"
263            }
264            opts::NoiseLevel::FranklyQuitePedantic => {
265                "info,cargo_mobile=debug,cargo_android=debug,cargo_apple=debug,hit=debug"
266            }
267        };
268        let env = Env::default().default_filter_or(default_level);
269        Builder::from_env(env).init();
270    }
271
272    #[derive(Debug)]
273    pub enum Exit {
274        Report(Report),
275        Clap(clap::Error),
276    }
277
278    impl Exit {
279        fn report(reportable: impl Reportable) -> Self {
280            log::info!("exiting with {:#?}", reportable);
281            Self::Report(reportable.report())
282        }
283
284        fn do_the_thing(self, wrapper: TextWrapper) -> ! {
285            match self {
286                Self::Report(report) => {
287                    report.print(&wrapper);
288                    std::process::exit(report.label.exit_code().into())
289                }
290                Self::Clap(err) => err.exit(),
291            }
292        }
293
294        pub fn main(inner: impl FnOnce(&TextWrapper) -> Result<(), Self>) {
295            let wrapper = TextWrapper::default();
296            if let Err(exit) = inner(&wrapper) {
297                exit.do_the_thing(wrapper)
298            }
299        }
300    }
301
302    pub fn exec<E: Exec>(name: &str) {
303        Exit::main(|wrapper| {
304            let args = get_args(name);
305            let input = E::from_iter_safe(&args).map_err(Exit::Clap)?;
306            init_logging(input.global_flags().noise_level);
307            log::debug!("raw args: {:#?}", args);
308            input.exec(wrapper).map_err(Exit::report)
309        })
310    }
311}