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}