1mod artifact;
2mod ci;
3mod config;
4mod download;
5mod env;
6pub mod error;
7mod install;
8mod manfiest;
9mod optimize;
10mod tool;
11mod types;
12
13use crate::error::{err, Result};
14use crate::tool::expand_path;
15#[cfg(windows)]
16use crate::error::Context;
17use clap::{ArgAction, CommandFactory, Parser, Subcommand};
18use config::PersistentConfig;
19use github_proxy::Proxy;
20use guess_target::Target;
21use tool::add_output_to_path;
22
23#[derive(Debug, Clone)]
24pub struct InstallConfig {
25 pub dir: Option<String>,
26 pub name: Vec<String>,
27 pub alias: Option<String>,
28 pub target: Option<Target>,
29 pub retry: usize,
30 pub proxy: Proxy,
31 pub timeout: u64,
32 pub strip: bool,
33 pub upx: bool,
34 pub quiet: bool,
35 pub no_path: bool,
36 pub fuzzy: bool,
37 pub regex: Option<String>,
38}
39
40impl Default for InstallConfig {
41 fn default() -> Self {
42 Self {
43 dir: None,
44 name: Vec::new(),
45 alias: None,
46 target: None,
47 retry: 3,
48 proxy: Proxy::Github,
49 timeout: 600,
50 strip: false,
51 upx: false,
52 quiet: false,
53 no_path: false,
54 fuzzy: false,
55 regex: None,
56 }
57 }
58}
59
60impl InstallConfig {
61 pub fn load() -> Self {
64 let persistent_config = PersistentConfig::load();
65
66 Self {
67 dir: persistent_config.dir,
68 name: Vec::new(),
69 alias: None,
70 target: persistent_config.target,
71 retry: persistent_config.retry.unwrap_or(3) as usize,
72 proxy: persistent_config.proxy.unwrap_or(Proxy::Github),
73 timeout: persistent_config.timeout.unwrap_or(600),
74 strip: persistent_config.strip.unwrap_or(false),
75 upx: persistent_config.upx.unwrap_or(false),
76 quiet: false,
77 no_path: false,
78 fuzzy: false,
79 regex: None,
80 }
81 }
82
83 pub fn get_local_target(&self) -> Vec<Target> {
84 if let Some(t) = self.target {
85 return vec![t];
86 }
87 guess_target::get_local_target().to_vec()
88 }
89}
90
91#[derive(Debug, Clone, Subcommand)]
92pub enum ConfigSubcommand {
93 Proxy {
95 value: Option<Proxy>,
97 },
98 Dir {
100 value: Option<String>,
102 },
103 Target {
105 value: Option<Target>,
107 },
108 Timeout {
110 value: Option<u64>,
112 },
113 Retry {
115 value: Option<u64>,
117 },
118 Upx {
120 value: Option<bool>,
122 },
123 Strip {
125 value: Option<bool>,
127 },
128}
129
130#[derive(Debug, Clone, Subcommand)]
131pub enum Command {
132 Config {
134 #[command(subcommand)]
135 subcmd: Option<ConfigSubcommand>,
136 },
137 Completions {
139 #[arg(value_enum)]
141 shell: clap_complete::Shell,
142 },
143 Upgrade,
145}
146
147const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
148const GIT_HASH: &str = git_version::git_version!(fallback = "unknown");
149const BUILD_TARGET: &str = env!("BUILD_TARGET");
150const VERSION: &str = const_str::concat!(CARGO_PKG_VERSION, " ", GIT_HASH, " ", BUILD_TARGET);
151
152#[derive(Parser, Debug, Clone)]
153#[command(name="ei", version=VERSION, about, long_about)]
154pub struct Args {
155 #[command(subcommand)]
156 pub cmd: Option<Command>,
157
158 #[arg(default_value_t = String::new())]
160 pub url: String,
161
162 #[arg(short, long, help = "Installation directory")]
164 pub dir: Option<String>,
165
166 #[arg(long, global = true, default_value_t = false, action = ArgAction::SetTrue, help = "Skip adding installed binaries to PATH")]
168 pub no_path: bool,
169
170 #[arg(
176 long,
177 value_delimiter = ',',
178 help = "Filter artifacts by name (comma-separated, word-boundary match)"
179 )]
180 pub name: Vec<String>,
181
182 #[arg(long, help = "Rename the installed binary")]
184 pub alias: Option<String>,
185
186 #[arg(long, help = "Target platform (auto-detected if not specified)")]
188 pub target: Option<Target>,
189
190 #[arg(
192 long,
193 global = true,
194 default_value_t = 3,
195 help = "Number of retry attempts"
196 )]
197 pub retry: usize,
198
199 #[arg(long, global = true, help = "GitHub proxy to use")]
201 pub proxy: Option<Proxy>,
202
203 #[arg(long, global = true, help = "Network request timeout in seconds")]
205 pub timeout: Option<u64>,
206
207 #[arg(
209 long,
210 global = true,
211 help = "Strip debug symbols from executable",
212 default_missing_value = "true",
213 num_args = 0..=1,
214 )]
215 pub strip: Option<bool>,
216
217 #[arg(
219 long,
220 global = true,
221 help = "Compress executable with UPX",
222 default_missing_value = "true",
223 num_args = 0..=1,
224 )]
225 pub upx: Option<bool>,
226
227 #[arg(
229 long,
230 short,
231 global = true,
232 help = "Suppress all output messages",
233 default_missing_value = "true",
234 num_args = 0..=1,
235 default_value_t = false
236 )]
237 pub quiet: bool,
238
239 #[arg(
247 long,
248 help = "Use fuzzy target matching (match arch+os, ignoring abi)",
249 default_missing_value = "true",
250 num_args = 0..=1,
251 default_value_t = false
252 )]
253 pub fuzzy: bool,
254
255 #[arg(
269 long,
270 help = "Regex to match asset filenames directly (bypasses guess_target)"
271 )]
272 pub regex: Option<String>,
273}
274
275impl Default for Args {
276 fn default() -> Self {
277 Self {
278 cmd: None,
279 url: "".to_string(),
280 dir: None,
281 no_path: false,
282 name: vec![],
283 alias: None,
284 target: None,
285 retry: 3,
286 proxy: None,
287 timeout: None,
288 strip: None,
289 upx: None,
290 quiet: false,
291 fuzzy: false,
292 regex: None,
293 }
294 }
295}
296
297impl From<Args> for InstallConfig {
298 fn from(value: Args) -> Self {
299 let persistent_config = PersistentConfig::load();
300
301 let proxy = value
302 .proxy
303 .or(persistent_config.proxy)
304 .unwrap_or(Proxy::Github);
305
306 let timeout = value.timeout.or(persistent_config.timeout).unwrap_or(600);
307
308 let dir = value.dir.clone().or(persistent_config.dir);
309
310 let target = value.target.or(persistent_config.target);
311 let strip = value.strip.or(persistent_config.strip).unwrap_or(false);
312 let upx = value.upx.or(persistent_config.upx).unwrap_or(false);
313
314 InstallConfig {
315 dir,
316 name: value.name.clone(),
317 alias: value.alias.clone(),
318 target,
319 retry: value.retry,
320 proxy,
321 timeout,
322 strip,
323 upx,
324 quiet: value.quiet,
325 no_path: value.no_path,
326 fuzzy: value.fuzzy,
327 regex: value.regex,
328 }
329 }
330}
331
332pub async fn run_main(args: Args) -> Result<()> {
333 if let Some(Command::Completions { shell }) = args.cmd {
335 return handle_completions_command(shell);
336 }
337
338 if let Some(Command::Upgrade) = args.cmd {
339 return handle_upgrade(args.into()).await;
340 }
341
342 if let Some(Command::Config { subcmd }) = args.cmd {
344 let quiet = args.quiet;
345 return handle_config_command(subcmd, quiet);
346 }
347
348 let url = args.url.clone();
350
351 if url.is_empty() {
352 let s = Args::command().render_help();
353 println!("{s}");
354 return Ok(());
355 }
356 let config = args.into();
357 ei(&url, &config).await?;
358 Ok(())
359}
360
361pub async fn ei(url: &str, config: &InstallConfig) -> Result<()> {
362 let output = install::install(url, config).await?;
363 if !config.no_path {
364 add_output_to_path(&output, config);
365 }
366 if output.is_empty() && !config.quiet {
367 println!("No file installed from {url}");
368 }
369 Ok(())
370}
371
372fn handle_completions_command(shell: clap_complete::Shell) -> Result<()> {
373 use clap_complete::generate;
374 use std::io;
375
376 let mut cmd = Args::command();
377 let bin_name = cmd.get_name().to_string();
378
379 generate(shell, &mut cmd, bin_name, &mut io::stdout());
380 Ok(())
381}
382
383async fn handle_upgrade(user_config: InstallConfig) -> Result<()> {
384 let exe = std::env::current_exe()?;
385 let dir = exe
386 .parent()
387 .ok_or_else(|| err!("ei dir not found"))?;
388
389 let config = InstallConfig {
392 dir: Some(dir.to_string_lossy().to_string()),
393 alias: Some("ei".to_string()),
394 ..user_config
395 };
396
397 #[cfg(windows)]
398 {
399 let old_exe = exe.with_extension("exe.old");
403 let _ = std::fs::remove_file(&old_exe);
404 std::fs::rename(&exe, &old_exe).context("Failed to rename running ei.exe for upgrade")?;
405
406 match ei("easy-install/easy-install", &config).await {
407 Ok(()) => {
408 let _ = std::fs::remove_file(&old_exe);
409 Ok(())
410 }
411 Err(e) => {
412 let _ = std::fs::remove_file(&exe);
413 std::fs::rename(&old_exe, &exe)?;
414 Err(e)
415 }
416 }
417 }
418
419 #[cfg(not(windows))]
420 {
421 ei("easy-install/easy-install", &config).await
422 }
423}
424
425fn handle_config_command(subcmd: Option<ConfigSubcommand>, quiet: bool) -> Result<()> {
426 let mut config = PersistentConfig::load();
427
428 let Some(subcmd) = subcmd else {
429 if !quiet {
430 config.display();
431 }
432 return Ok(());
433 };
434
435 match subcmd {
436 ConfigSubcommand::Proxy { value } => {
437 let current = config
438 .proxy
439 .map_or("not set (default: Github)".to_string(), |p| {
440 format!("{p:?}")
441 });
442 apply_config(
443 &mut config,
444 quiet,
445 value,
446 PersistentConfig::set_proxy,
447 |v| format!("{v:?}"),
448 "Proxy",
449 current,
450 )?
451 }
452 ConfigSubcommand::Dir { value } => {
453 let current = config.dir.as_deref().unwrap_or("not set").to_string();
454 apply_config(
455 &mut config,
456 quiet,
457 value,
458 |c, v| c.set_dir(expand_path(&v)),
459 |v: &String| v.to_string(),
460 "Directory",
461 current,
462 )?
463 }
464 ConfigSubcommand::Target { value } => {
465 let current = config
466 .target
467 .map_or("not set (auto-detect)".to_string(), |t| {
468 t.to_str().to_string()
469 });
470 apply_config(
471 &mut config,
472 quiet,
473 value,
474 PersistentConfig::set_target,
475 |v| v.to_str().to_string(),
476 "Target",
477 current,
478 )?
479 }
480 ConfigSubcommand::Timeout { value } => {
481 let current = config
482 .timeout
483 .map_or("not set (default: 600 seconds)".to_string(), |t| {
484 format!("{t} seconds")
485 });
486 apply_config(
487 &mut config,
488 quiet,
489 value,
490 PersistentConfig::set_timeout,
491 |v: &u64| v.to_string(),
492 "Timeout",
493 current,
494 )?
495 }
496 ConfigSubcommand::Retry { value } => {
497 let current = config
498 .retry
499 .map_or("not set (default: 3)".to_string(), |t| format!("{t}"));
500 apply_config(
501 &mut config,
502 quiet,
503 value,
504 PersistentConfig::set_retry,
505 |v: &u64| v.to_string(),
506 "Retry",
507 current,
508 )?
509 }
510 ConfigSubcommand::Upx { value } => {
511 let current = config
512 .upx
513 .map_or("not set (default: false)".to_string(), |t| format!("{t}"));
514 apply_config(
515 &mut config,
516 quiet,
517 value,
518 PersistentConfig::set_upx,
519 |v: &bool| v.to_string(),
520 "Upx",
521 current,
522 )?
523 }
524 ConfigSubcommand::Strip { value } => {
525 let current = config
526 .strip
527 .map_or("not set (default: false)".to_string(), |t| format!("{t}"));
528 apply_config(
529 &mut config,
530 quiet,
531 value,
532 PersistentConfig::set_strip,
533 |v: &bool| v.to_string(),
534 "Strip",
535 current,
536 )?
537 }
538 }
539
540 Ok(())
541}
542
543fn apply_config<T>(
547 config: &mut PersistentConfig,
548 quiet: bool,
549 value: Option<T>,
550 setter: impl FnOnce(&mut PersistentConfig, T),
551 fmt_val: impl Fn(&T) -> String,
552 label: &str,
553 current: String,
554) -> Result<()> {
555 if let Some(v) = value {
556 let msg = format!("{label} set to: {}", fmt_val(&v));
557 setter(config, v);
558 config.save_quiet(quiet)?;
559 if !quiet {
560 println!("{msg}");
561 }
562 } else if !quiet {
563 println!("Current {label}: {current}");
564 }
565 Ok(())
566}