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}