use std;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command as SysCommand;
#[cfg(unix)]
use std::fs::{canonicalize, metadata};
#[cfg(windows)]
use dunce::canonicalize;
use clap::builder::styling::{AnsiColor, Effects, Styles};
use clap::{ArgAction, CommandFactory, Parser, Subcommand, value_parser};
use indexmap::IndexSet;
use miette::{Context as _, IntoDiagnostic as _};
use serde_yaml_ng;
use tokio::runtime::Runtime;
use crate::Result;
use crate::cmd;
use crate::cmd::fusesoc::FusesocArgs;
use crate::config::{
Config, Manifest, Merge, PartialConfig, PrefixPaths, Validate, ValidationContext,
};
use crate::diagnostic::{Diagnostics, Warnings};
use crate::lockfile::*;
use crate::sess::{Session, SessionArenas, SessionIo};
use crate::{bail, err};
use crate::{fmt_path, fmt_pkg, stageln};
#[derive(Parser, Debug)]
#[command(name = "bender")]
#[command(author, version, about, long_about = None)]
#[command(after_help = "Type 'bender <SUBCOMMAND> --help' for more information...")]
#[command(styles = cli_styles())]
struct Cli {
#[arg(short, long, global = true, help_heading = "Global Options", env = "BENDER_DIR", value_parser = value_parser!(String))]
dir: Option<String>,
#[arg(
long,
global = true,
help_heading = "Global Options",
env = "BENDER_LOCAL"
)]
local: bool,
#[arg(
long,
global = true,
help_heading = "Global Options",
env = "BENDER_GIT_THROTTLE"
)]
git_throttle: Option<usize>,
#[arg(long, global = true, action = ArgAction::Append, help_heading = "Global Options", env = "BENDER_SUPPRESS_WARNINGS")]
suppress: Vec<String>,
#[arg(
long,
global = true,
help_heading = "Global Options",
env = "BENDER_NO_PROGRESS"
)]
no_progress: bool,
#[arg(
short,
long,
long_help = "Increase logging verbosity (-v info, -vv debug, -vvv trace). Disables progress bars.\nSet BENDER_VERBOSE to a number (e.g. BENDER_VERBOSE=2) for -vv equivalent.",
action = ArgAction::Count,
global = true,
help_heading = "Global Options",
env = "BENDER_VERBOSE"
)]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Update(cmd::update::UpdateArgs),
Path(cmd::path::PathArgs),
Parents(cmd::parents::ParentsArgs),
Clone(cmd::clone::CloneArgs),
Clean(cmd::clean::CleanArgs),
Packages(cmd::packages::PackagesArgs),
Sources(cmd::sources::SourcesArgs),
Completion(cmd::completion::CompletionArgs),
Config,
Script(cmd::script::ScriptArgs),
Checkout(cmd::checkout::CheckoutArgs),
Vendor(cmd::vendor::VendorArgs),
Fusesoc(cmd::fusesoc::FusesocArgs),
Init,
Snapshot(cmd::snapshot::SnapshotArgs),
Audit(cmd::audit::AuditArgs),
#[cfg(feature = "slang")]
Pickle(cmd::pickle::PickleArgs),
#[command(external_subcommand)]
Plugin(Vec<String>),
}
fn cli_styles() -> Styles {
Styles::styled()
.header(AnsiColor::Green.on_default() | Effects::BOLD)
.usage(AnsiColor::Green.on_default() | Effects::BOLD)
.literal(AnsiColor::Cyan.on_default() | Effects::BOLD)
.placeholder(AnsiColor::Cyan.on_default())
.error(AnsiColor::Red.on_default() | Effects::BOLD)
.valid(AnsiColor::Cyan.on_default() | Effects::BOLD)
.invalid(AnsiColor::Yellow.on_default() | Effects::BOLD)
}
pub fn main() -> Result<()> {
let cli = Cli::parse();
let log_level = match cli.verbose {
0 => log::LevelFilter::Warn,
1 => log::LevelFilter::Info,
2 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
};
env_logger::Builder::new()
.filter_level(log_level)
.format_timestamp(None)
.format_target(false)
.init();
let mut suppressed_warnings: HashSet<String> =
cli.suppress.into_iter().map(|s| s.to_owned()).collect();
suppressed_warnings = suppressed_warnings
.into_iter()
.flat_map(|s| {
s.split(&[',', ' '][..])
.map(|t| t.to_string())
.collect::<Vec<_>>()
})
.collect();
Diagnostics::init(suppressed_warnings);
match &cli.command {
Commands::Completion(args) => {
let mut cmd = Cli::command();
return cmd::completion::run(args, &mut cmd);
}
Commands::Init => {
return cmd::init::run();
}
_ => {}
}
let force_fetch = match cli.command {
Commands::Update(ref args) => cmd::update::setup(args, cli.local)?,
_ => false,
};
let root_dir: PathBuf = match &cli.dir {
Some(d) => canonicalize(d)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to canonicalize path {:?}.", d))?,
None => {
find_package_root(Path::new(".")).wrap_err("Cannot find root directory of package.")?
}
};
log::debug!("root dir {:?}", root_dir);
let manifest_path = root_dir.join("Bender.yml");
let manifest = read_manifest(&manifest_path)?;
log::debug!("{:#?}", manifest);
let config = load_config(&root_dir, matches!(cli.command, Commands::Update(_)))?;
log::debug!("{:#?}", config);
let git_throttle = cli.git_throttle.or(config.git_throttle).unwrap_or(4);
let sess_arenas = SessionArenas::new();
let sess = Session::new(
&root_dir,
&manifest,
&config,
&sess_arenas,
cli.local,
force_fetch,
git_throttle,
cli.no_progress || cli.verbose > 0,
);
if let Commands::Clean(args) = cli.command {
return cmd::clean::run(&sess, args.all, &root_dir);
}
let lock_path = root_dir.join("Bender.lock");
let locked_existing = if lock_path.exists() {
Some(read_lockfile(&lock_path, &root_dir)?)
} else {
None
};
let (locked_list, update_list) = match &cli.command {
Commands::Fusesoc(args @ FusesocArgs { single: true, .. }) => {
return cmd::fusesoc::run_single(&sess, args);
}
Commands::Update(args) => cmd::update::run(args, &sess, locked_existing.as_ref())?,
_ if locked_existing.is_none() => {
cmd::update::run_plain(false, &sess, locked_existing.as_ref(), IndexSet::new())?
}
_ => {
log::debug!("lockfile {:?} up-to-date", lock_path);
(locked_existing.unwrap(), Vec::new())
}
};
sess.load_locked(&locked_list)?;
{
let io = SessionIo::new(&sess);
for (path, pkg_name) in &sess.manifest.workspace.package_links {
log::debug!("maintaining link to {} at {:?}", pkg_name, path);
let pkg_path = sess.get_package_path(sess.dependency_with_name(pkg_name)?);
if matches!(cli.command, Commands::Update(_)) || !pkg_path.exists() {
let rt = Runtime::new().into_diagnostic()?;
rt.block_on(io.checkout(sess.dependency_with_name(pkg_name)?, false, &[]))?;
}
let pkg_path = path
.parent()
.and_then(|path| pathdiff::diff_paths(&pkg_path, path))
.unwrap_or(pkg_path);
if path.exists() {
let meta = path
.symlink_metadata()
.into_diagnostic()
.wrap_err_with(|| format!("Failed to read metadata of path {:?}.", path))?;
if !meta.file_type().is_symlink() {
Warnings::SkippingPackageLink(pkg_name.clone(), path.clone()).emit();
continue;
}
if path.read_link().map(|d| d != pkg_path).unwrap_or(true) {
log::debug!("removing existing link {:?}", path);
remove_symlink_dir(path).wrap_err_with(|| {
format!("Failed to remove symlink at path {:?}.", path)
})?;
}
}
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to create directory {:?}.", parent))?;
}
let previous_dir = match path.parent() {
Some(parent) => {
let d = std::env::current_dir().unwrap();
std::env::set_current_dir(parent).unwrap();
Some(d)
}
None => None,
};
symlink_dir(&pkg_path, path).wrap_err_with(|| {
format!(
"Failed to create symlink to {:?} at path {:?}.",
pkg_path, path
)
})?;
if let Some(d) = previous_dir {
std::env::set_current_dir(d).unwrap();
}
stageln!(
"Linked",
"{} to {}",
fmt_pkg!(pkg_name),
fmt_path!(path.display())
);
}
}
}
match cli.command {
Commands::Path(args) => cmd::path::run(&sess, &args),
Commands::Parents(args) => cmd::parents::run(&sess, &args),
Commands::Clone(args) => cmd::clone::run(&sess, &root_dir, &args),
Commands::Packages(args) => cmd::packages::run(&sess, &args),
Commands::Sources(args) => cmd::sources::run(&sess, &args),
Commands::Config => cmd::config::run(&sess),
Commands::Script(args) => cmd::script::run(&sess, &args),
Commands::Checkout(args) => cmd::checkout::run(&sess, &args),
Commands::Update(args) => cmd::update::run_final(&sess, &args, &update_list),
Commands::Vendor(args) => cmd::vendor::run(&sess, &args),
Commands::Fusesoc(args) => cmd::fusesoc::run(&sess, &args),
Commands::Snapshot(args) => cmd::snapshot::run(&sess, &args),
Commands::Audit(args) => cmd::audit::run(&sess, &args),
#[cfg(feature = "slang")]
Commands::Pickle(args) => cmd::pickle::run(&sess, args),
Commands::Plugin(args) => {
let (plugin_name, plugin_args) = args
.split_first()
.ok_or_else(|| err!("No command specified."))?;
execute_plugin(&sess, plugin_name, plugin_args)
}
Commands::Completion(_) | Commands::Init | Commands::Clean(_) => {
unreachable!()
}
}
}
#[cfg(unix)]
pub fn symlink_dir(p: &Path, q: &Path) -> Result<()> {
std::os::unix::fs::symlink(p, q).into_diagnostic()
}
#[cfg(windows)]
pub fn symlink_dir(p: &Path, q: &Path) -> Result<()> {
std::os::windows::fs::symlink_dir(p, q).into_diagnostic()
}
#[cfg(unix)]
pub fn remove_symlink_dir(path: &Path) -> Result<()> {
std::fs::remove_file(path).into_diagnostic()
}
#[cfg(windows)]
pub fn remove_symlink_dir(path: &Path) -> Result<()> {
std::fs::remove_dir(path).into_diagnostic()
}
fn find_package_root(from: &Path) -> Result<PathBuf> {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
let mut path = canonicalize(from)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to canonicalize path {:?}.", from))?;
log::debug!("canonicalized to {:?}", path);
#[cfg(unix)]
let limit_rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
#[cfg(unix)]
log::debug!("limit rdev = {:?}", limit_rdev);
for _ in 0..100 {
log::debug!("looking in {:?}", path);
if path.join("Bender.yml").exists() {
return Ok(path);
}
if !path.pop() {
bail!(
"No manifest (`Bender.yml` file) found. Stopped searching at filesystem root {:?}.",
path
);
}
#[cfg(unix)]
{
let rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
log::debug!("rdev = {:?}", rdev);
if rdev != limit_rdev {
bail!(
"No manifest (`Bender.yml` file) found. Stopped searching at filesystem boundary {:?}.",
path
);
}
}
}
bail!("No manifest (`Bender.yml` file) found. Reached maximum number of search steps.")
}
pub fn read_manifest(path: &Path) -> Result<Manifest> {
use crate::config::PartialManifest;
use std::fs::File;
log::debug!("reading manifest {:?}", path);
let file = File::open(path)
.into_diagnostic()
.wrap_err_with(|| format!("Cannot open manifest {:?}.", path))?;
let partial: PartialManifest = serde_yaml_ng::from_reader(file)
.into_diagnostic()
.wrap_err_with(|| format!("Syntax error in manifest {:?}.", path))?;
partial
.prefix_paths(path.parent().unwrap())
.wrap_err_with(|| format!("Error in manifest prefixing {:?}.", path))?
.validate(&ValidationContext::default())
.wrap_err_with(|| format!("Error in manifest {:?}.", path))
}
fn load_config(from: &Path, warn_config_loaded: bool) -> Result<Config> {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
let mut out = PartialConfig::new();
let mut path = canonicalize(from)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to canonicalize path {:?}.", from))?;
log::debug!("canonicalized to {:?}", path);
#[cfg(unix)]
let limit_rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
#[cfg(unix)]
log::debug!("limit rdev = {:?}", limit_rdev);
for _ in 0..100 {
if let Some(cfg) = maybe_load_config(&path.join("Bender.local"), warn_config_loaded)? {
out = out.merge(cfg);
}
log::debug!("looking in {:?}", path);
if let Some(cfg) = maybe_load_config(&path.join(".bender.yml"), warn_config_loaded)? {
out = out.merge(cfg);
}
if !path.pop() {
break;
}
#[cfg(unix)]
{
let rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
log::debug!("rdev = {:?}", rdev);
if rdev != limit_rdev {
break;
}
}
}
if let Some(mut home) = dirs::home_dir() {
home.push(".config");
home.push("bender.yml");
if let Some(cfg) = maybe_load_config(&home, warn_config_loaded)? {
out = out.merge(cfg);
}
}
if let Some(cfg) = maybe_load_config(Path::new("/etc/bender.yml"), warn_config_loaded)? {
out = out.merge(cfg);
}
let default_cfg = PartialConfig {
database: Some(from.join(".bender").to_str().unwrap().to_string()),
db_dir: std::env::var("BENDER_DB_DIR")
.ok()
.filter(|s| !s.is_empty()),
git: Some("git".into()),
overrides: None,
plugins: None,
git_throttle: None,
git_lfs: None,
};
out = out.merge(default_cfg);
let mut out = out
.validate(&ValidationContext::default())
.wrap_err("Invalid configuration:")?;
out.overrides = out
.overrides
.into_iter()
.map(|(k, v)| (k.to_lowercase(), v))
.collect();
Ok(out)
}
fn maybe_load_config(path: &Path, warn_config_loaded: bool) -> Result<Option<PartialConfig>> {
use std::fs::File;
log::debug!("maybe loading config {:?}", path);
if !path.exists() {
return Ok(None);
}
let file = File::open(path)
.into_diagnostic()
.wrap_err_with(|| format!("Cannot open config {:?}.", path))?;
let partial: PartialConfig = serde_yaml_ng::from_reader(file)
.into_diagnostic()
.wrap_err_with(|| format!("Syntax error in config {:?}.", path))?;
if warn_config_loaded {
Warnings::UsingConfigForOverride {
path: path.to_path_buf(),
}
.emit();
}
Ok(Some(partial.prefix_paths(path.parent().unwrap())?))
}
fn execute_plugin(sess: &Session, plugin: &str, args: &[String]) -> Result<()> {
log::debug!("execute plugin `{}`", plugin);
let runtime = Runtime::new().into_diagnostic()?;
let io = SessionIo::new(sess);
let plugins = runtime.block_on(io.plugins(false))?;
let plugin = match plugins.get(plugin) {
Some(p) => p,
None => bail!("Unknown command `{}`.", plugin),
};
log::debug!("found plugin {:#?}", plugin);
let mut cmd = SysCommand::new(&plugin.path);
cmd.env(
"BENDER",
std::env::current_exe()
.into_diagnostic()
.wrap_err("Failed to determine current executable.")?,
);
cmd.env(
"BENDER_CALL_DIR",
std::env::current_dir()
.into_diagnostic()
.wrap_err("Failed to determine current directory.")?,
);
cmd.env("BENDER_MANIFEST_DIR", sess.root);
cmd.current_dir(sess.root);
cmd.args(args);
log::debug!("executing plugin {:#?}", cmd);
let stat = cmd.status().into_diagnostic().wrap_err_with(|| {
format!(
"Unable to spawn process for plugin `{}`. Command was {:#?}.",
plugin.name, cmd
)
})?;
std::process::exit(stat.code().unwrap_or(1));
}