agave_install/
lib.rs

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