rpk 0.2.2

A lightweight, cross-platform cli package manager.
pub mod cli;
pub mod commands;
pub mod config;
pub mod context;
pub mod installer;
pub mod manager;
pub mod provider;
pub mod util;

use std::process;

use anyhow::Context as _;
use clap::{CommandFactory as _, Parser as _, ValueEnum};
use clap_complete::{generate, generate_to, Shell};
use cli::{Opt, SubCommand, ENV_BIN_DIR, ENV_CACHE_DIR, ENV_CONFIG_DIR, ENV_DATA_DIR};
use config::{Package, Source};
use context::{log_error, Context};
use tracing_subscriber::EnvFilter;
use util::{mkdir_p, Shorten as _, CRATE_NAME};
use yansi::Condition;

fn try_main() -> anyhow::Result<()> {
    let opt = Opt::parse();
    let verbosity = opt.verbosity();

    let Opt { bin_dir, data_dir, cache_dir, config_dir, command, .. } = opt;

    let xdg_dirs = xdg::BaseDirectories::with_prefix(CRATE_NAME)?;

    let config_dir = config_dir.unwrap_or_else(|| xdg_dirs.get_config_home());
    mkdir_p(&config_dir).context("failed to create config dir")?;

    let cache_dir = cache_dir.unwrap_or_else(|| xdg_dirs.get_cache_home());
    mkdir_p(&cache_dir).context("failed to create cache dir")?;

    let data_dir = data_dir.unwrap_or_else(|| xdg_dirs.get_data_home().join("packages"));
    mkdir_p(&data_dir).context("failed to create data dir")?;

    let bin_dir = bin_dir.unwrap_or_else(|| xdg_dirs.get_data_home().join("bin"));
    mkdir_p(&bin_dir).context("failed to create binary dir")?;

    let config_file = config_dir.join("packages.toml");
    let lock_file = config_dir.join("packages.lock");

    let version = util::CRATE_RELEASE.to_string();
    let ctx = Context {
        version,
        config_file,
        config_dir,
        cache_dir,
        data_dir,
        bin_dir,
        lock_file,
        verbosity,
    };

    macro_rules! with_flock {
        ($command:expr) => {
            let _guard = acquire_flock(&ctx)?;
            $command
        };
    }

    match command {
        SubCommand::Init { from } => {
            with_flock!(commands::init(&ctx, from)?);
        }
        SubCommand::List => {
            with_flock!(commands::list(&ctx)?);
        }
        SubCommand::Sync => {
            with_flock!(commands::sync(&ctx, false)?);
        }
        SubCommand::Update { package } => {
            with_flock!(commands::update(&ctx, package)?);
        }
        SubCommand::Restore { package } => {
            with_flock!(commands::restore(&ctx, package)?);
        }
        SubCommand::Search { query, top } => {
            with_flock!(commands::search(query, top, &ctx)?);
        }
        SubCommand::Add { name, binary, repo: (owner, repo), version, desc } => {
            let pkg = Package {
                name: name.unwrap_or_else(|| repo.clone()),
                bins: if binary.is_empty() {
                    vec![repo.clone()]
                } else {
                    binary
                },
                source: Source::Github { repo: format!("{}/{}", owner, repo) },
                version,
                desc,
                enabled: true.into(),
            };
            with_flock!(commands::add(&ctx, pkg)?);
        }
        SubCommand::Cleanup { cache } => {
            with_flock!(commands::cleanup(&ctx, cache)?);
        }

        SubCommand::Env => {
            macro_rules! print_env {
                ($name:expr, $path:expr) => {
                    println!(r#"export {}="{}""#, $name, $path.shorten()?);
                };
            }
            print_env!(ENV_CONFIG_DIR, ctx.config_dir);
            print_env!(ENV_CACHE_DIR, ctx.cache_dir);
            print_env!(ENV_DATA_DIR, ctx.data_dir);
            print_env!(ENV_BIN_DIR, ctx.bin_dir);
            println!(r#"export PATH="${ENV_BIN_DIR}:$PATH""#);
        }
        SubCommand::Completions { shell, dir, list } => {
            let cmd = &mut Opt::command();
            if list {
                for shell in Shell::value_variants() {
                    println!("{}", shell);
                }
            } else {
                let shell = shell.context("shell not specified")?;
                match dir {
                    Some(dir) => {
                        let path = generate_to(shell, cmd, cmd.get_name().to_string(), dir)?;
                        ctx.log_status("Generated", path.shorten()?);
                    }
                    None => generate(
                        shell,
                        cmd,
                        cmd.get_name().to_string(),
                        &mut std::io::stdout(),
                    ),
                }
            }
        }
        SubCommand::Version => {
            println!("{} {}", util::CRATE_NAME, util::CRATE_VERBOSE_VERSION);
        }
    }

    Ok(())
}

fn main() {
    yansi::whenever(Condition::TTY_AND_COLOR);

    tracing_subscriber::fmt()
        .event_format(
            tracing_subscriber::fmt::format()
                .with_file(true)
                .with_line_number(true),
        )
        .with_env_filter(EnvFilter::from_default_env())
        .init();

    if let Err(e) = try_main() {
        log_error(&e);
        process::exit(1);
    }
}

fn acquire_flock(ctx: &Context) -> anyhow::Result<fmutex::Guard> {
    let path = &ctx.config_dir;
    match fmutex::try_lock(path).with_context(|| format!("failed to open `{}`", path.display()))? {
        Some(g) => Ok(g),
        None => {
            ctx.log_warning(
                "Blocking",
                format!("waiting for file lock on {}", path.shorten()?),
            );
            fmutex::lock(path)
                .with_context(|| format!("failed to acquire file lock `{}`", path.display()))
        }
    }
}