miraland_install/
lib.rs

1#![allow(clippy::arithmetic_side_effects)]
2#[macro_use]
3extern crate lazy_static;
4
5use {
6    clap::{crate_description, crate_name, App, AppSettings, Arg, ArgMatches, SubCommand},
7    miraland_clap_utils::{
8        input_parsers::pubkey_of,
9        input_validators::{is_pubkey, is_url},
10    },
11};
12
13mod build_env;
14mod command;
15mod config;
16mod defaults;
17mod stop_process;
18mod update_manifest;
19
20pub fn is_semver(semver: &str) -> Result<(), String> {
21    match semver::Version::parse(semver) {
22        Ok(_) => Ok(()),
23        Err(err) => Err(format!("{err:?}")),
24    }
25}
26
27pub fn is_release_channel(channel: &str) -> Result<(), String> {
28    match channel {
29        "edge" | "beta" | "stable" => Ok(()),
30        _ => Err(format!("Invalid release channel {channel}")),
31    }
32}
33
34pub fn is_explicit_release(string: String) -> Result<(), String> {
35    if string.starts_with('v') && is_semver(string.split_at(1).1).is_ok() {
36        return Ok(());
37    }
38    is_semver(&string).or_else(|_| is_release_channel(&string))
39}
40
41pub fn explicit_release_of(
42    matches: &ArgMatches<'_>,
43    name: &str,
44) -> Option<config::ExplicitRelease> {
45    matches
46        .value_of(name)
47        .map(ToString::to_string)
48        .map(|explicit_release| {
49            if explicit_release.starts_with('v')
50                && is_semver(explicit_release.split_at(1).1).is_ok()
51            {
52                config::ExplicitRelease::Semver(explicit_release.split_at(1).1.to_string())
53            } else if is_semver(&explicit_release).is_ok() {
54                config::ExplicitRelease::Semver(explicit_release)
55            } else {
56                config::ExplicitRelease::Channel(explicit_release)
57            }
58        })
59}
60
61fn handle_init(matches: &ArgMatches<'_>, config_file: &str) -> Result<(), String> {
62    let json_rpc_url = matches.value_of("json_rpc_url").unwrap();
63    let update_manifest_pubkey = pubkey_of(matches, "update_manifest_pubkey");
64    let data_dir = matches.value_of("data_dir").unwrap();
65    let no_modify_path = matches.is_present("no_modify_path");
66    let explicit_release = explicit_release_of(matches, "explicit_release");
67
68    if update_manifest_pubkey.is_none() && explicit_release.is_none() {
69        Err(format!(
70            "Please specify the release to install for {}.  See --help for more",
71            build_env::TARGET
72        ))
73    } else {
74        command::init(
75            config_file,
76            data_dir,
77            json_rpc_url,
78            &update_manifest_pubkey.unwrap_or_default(),
79            no_modify_path,
80            explicit_release,
81        )
82    }
83}
84
85pub fn main() -> Result<(), String> {
86    miraland_logger::setup();
87
88    let matches = App::new(crate_name!())
89        .about(crate_description!())
90        .version(miraland_version::version!())
91        .setting(AppSettings::SubcommandRequiredElseHelp)
92        .arg({
93            let arg = Arg::with_name("config_file")
94                .short("c")
95                .long("config")
96                .value_name("PATH")
97                .takes_value(true)
98                .global(true)
99                .help("Configuration file to use");
100            match *defaults::CONFIG_FILE {
101                Some(ref config_file) => arg.default_value(config_file),
102                None => arg.required(true),
103            }
104        })
105        .subcommand(
106            SubCommand::with_name("init")
107                .about("initializes a new installation")
108                .setting(AppSettings::DisableVersion)
109                .arg({
110                    let arg = Arg::with_name("data_dir")
111                        .short("d")
112                        .long("data-dir")
113                        .value_name("PATH")
114                        .takes_value(true)
115                        .required(true)
116                        .help("Directory to store install data");
117                    match *defaults::DATA_DIR {
118                        Some(ref data_dir) => arg.default_value(data_dir),
119                        None => arg,
120                    }
121                })
122                .arg(
123                    Arg::with_name("json_rpc_url")
124                        .short("u")
125                        .long("url")
126                        .value_name("URL")
127                        .takes_value(true)
128                        .default_value(defaults::JSON_RPC_URL)
129                        .validator(is_url)
130                        .help("JSON RPC URL for the miraland cluster"),
131                )
132                .arg(
133                    Arg::with_name("no_modify_path")
134                        .long("no-modify-path")
135                        .help("Don't configure the PATH environment variable"),
136                )
137                .arg(
138                    Arg::with_name("update_manifest_pubkey")
139                        .short("p")
140                        .long("pubkey")
141                        .value_name("PUBKEY")
142                        .takes_value(true)
143                        .validator(is_pubkey)
144                        .help("Public key of the update manifest"),
145                )
146                .arg(
147                    Arg::with_name("explicit_release")
148                        .value_name("release")
149                        .index(1)
150                        .conflicts_with_all(&["json_rpc_url", "update_manifest_pubkey"])
151                        .validator(is_explicit_release)
152                        .help("The release version or channel to install"),
153                ),
154        )
155        .subcommand(
156            SubCommand::with_name("info")
157                .about("Displays information about the current installation")
158                .setting(AppSettings::DisableVersion)
159                .arg(
160                    Arg::with_name("local_info_only")
161                        .short("l")
162                        .long("local")
163                        .help("only display local information, don't check for updates"),
164                )
165                .arg(
166                    Arg::with_name("eval")
167                        .long("eval")
168                        .help("display information in a format that can be used with `eval`"),
169                ),
170        )
171        .subcommand(
172            SubCommand::with_name("deploy")
173                .about("Deploys a new update")
174                .setting(AppSettings::DisableVersion)
175                .arg({
176                    let arg = Arg::with_name("from_keypair_file")
177                        .short("k")
178                        .long("keypair")
179                        .value_name("PATH")
180                        .takes_value(true)
181                        .required(true)
182                        .help("Keypair file of the account that funds the deployment");
183                    match *defaults::USER_KEYPAIR {
184                        Some(ref config_file) => arg.default_value(config_file),
185                        None => arg,
186                    }
187                })
188                .arg(
189                    Arg::with_name("json_rpc_url")
190                        .short("u")
191                        .long("url")
192                        .value_name("URL")
193                        .takes_value(true)
194                        .default_value(defaults::JSON_RPC_URL)
195                        .validator(is_url)
196                        .help("JSON RPC URL for the miraland cluster"),
197                )
198                .arg(
199                    Arg::with_name("download_url")
200                        .index(1)
201                        .required(true)
202                        .validator(is_url)
203                        .help("URL to the miraland release archive"),
204                )
205                .arg(
206                    Arg::with_name("update_manifest_keypair_file")
207                        .index(2)
208                        .required(true)
209                        .help("Keypair file for the update manifest (/path/to/keypair.json)"),
210                ),
211        )
212        .subcommand(
213            SubCommand::with_name("gc")
214                .about("Delete older releases from the install cache to reclaim disk space")
215                .setting(AppSettings::DisableVersion),
216        )
217        .subcommand(
218            SubCommand::with_name("update")
219                .about("Checks for an update, and if available downloads and applies it")
220                .setting(AppSettings::DisableVersion),
221        )
222        .subcommand(
223            SubCommand::with_name("run")
224                .about("Runs a program while periodically checking and applying software updates")
225                .after_help("The program will be restarted upon a successful software update")
226                .setting(AppSettings::DisableVersion)
227                .arg(
228                    Arg::with_name("program_name")
229                        .index(1)
230                        .required(true)
231                        .help("program to run"),
232                )
233                .arg(
234                    Arg::with_name("program_arguments")
235                        .index(2)
236                        .multiple(true)
237                        .help("arguments to supply to the program"),
238                ),
239        )
240        .subcommand(SubCommand::with_name("list").about("List installed versions of solana cli"))
241        .get_matches();
242
243    let config_file = matches.value_of("config_file").unwrap();
244
245    match matches.subcommand() {
246        ("init", Some(matches)) => handle_init(matches, config_file),
247        ("info", Some(matches)) => {
248            let local_info_only = matches.is_present("local_info_only");
249            let eval = matches.is_present("eval");
250            command::info(config_file, local_info_only, eval).map(|_| ())
251        }
252        ("deploy", Some(matches)) => {
253            let from_keypair_file = matches.value_of("from_keypair_file").unwrap();
254            let json_rpc_url = matches.value_of("json_rpc_url").unwrap();
255            let download_url = matches.value_of("download_url").unwrap();
256            let update_manifest_keypair_file =
257                matches.value_of("update_manifest_keypair_file").unwrap();
258            command::deploy(
259                json_rpc_url,
260                from_keypair_file,
261                download_url,
262                update_manifest_keypair_file,
263            )
264        }
265        ("gc", Some(_matches)) => command::gc(config_file),
266        ("update", Some(_matches)) => command::update(config_file, false).map(|_| ()),
267        ("run", Some(matches)) => {
268            let program_name = matches.value_of("program_name").unwrap();
269            let program_arguments = matches
270                .values_of("program_arguments")
271                .map(Iterator::collect)
272                .unwrap_or_else(Vec::new);
273
274            command::run(config_file, program_name, program_arguments)
275        }
276        ("list", Some(_matches)) => command::list(config_file),
277        _ => unreachable!(),
278    }
279}
280
281pub fn main_init() -> Result<(), String> {
282    miraland_logger::setup();
283
284    let matches = App::new("miraland-install-init")
285        .about("Initializes a new installation")
286        .version(miraland_version::version!())
287        .arg({
288            let arg = Arg::with_name("config_file")
289                .short("c")
290                .long("config")
291                .value_name("PATH")
292                .takes_value(true)
293                .help("Configuration file to use");
294            match *defaults::CONFIG_FILE {
295                Some(ref config_file) => arg.default_value(config_file),
296                None => arg.required(true),
297            }
298        })
299        .arg({
300            let arg = Arg::with_name("data_dir")
301                .short("d")
302                .long("data-dir")
303                .value_name("PATH")
304                .takes_value(true)
305                .required(true)
306                .help("Directory to store install data");
307            match *defaults::DATA_DIR {
308                Some(ref data_dir) => arg.default_value(data_dir),
309                None => arg,
310            }
311        })
312        .arg(
313            Arg::with_name("json_rpc_url")
314                .short("u")
315                .long("url")
316                .value_name("URL")
317                .takes_value(true)
318                .default_value(defaults::JSON_RPC_URL)
319                .validator(is_url)
320                .help("JSON RPC URL for the miraland cluster"),
321        )
322        .arg(
323            Arg::with_name("no_modify_path")
324                .long("no-modify-path")
325                .help("Don't configure the PATH environment variable"),
326        )
327        .arg(
328            Arg::with_name("update_manifest_pubkey")
329                .short("p")
330                .long("pubkey")
331                .value_name("PUBKEY")
332                .takes_value(true)
333                .validator(is_pubkey)
334                .help("Public key of the update manifest"),
335        )
336        .arg(
337            Arg::with_name("explicit_release")
338                .value_name("release")
339                .index(1)
340                .conflicts_with_all(&["json_rpc_url", "update_manifest_pubkey"])
341                .validator(is_explicit_release)
342                .help("The release version or channel to install"),
343        )
344        .get_matches();
345
346    let config_file = matches.value_of("config_file").unwrap();
347    handle_init(&matches, config_file)
348}