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 serde_yaml_ng;
use tokio::runtime::Runtime;
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::error::*;
use crate::lockfile::*;
use crate::sess::{Session, SessionArenas, SessionIo};
use crate::{debugln, 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,
#[cfg(debug_assertions)]
#[arg(
long,
global = true,
help_heading = "Global Options",
env = "BENDER_DEBUG"
)]
debug: bool,
#[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),
#[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 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);
#[cfg(debug_assertions)]
if cli.debug {
ENABLE_DEBUG.store(true, std::sync::atomic::Ordering::Relaxed);
}
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).map_err(|cause| {
Error::chain(format!("Failed to canonicalize path {:?}.", d), cause)
})?,
None => find_package_root(Path::new("."))
.map_err(|cause| Error::chain("Cannot find root directory of package.", cause))?,
};
debugln!("main: root dir {:?}", root_dir);
let manifest_path = root_dir.join("Bender.yml");
let manifest = read_manifest(&manifest_path)?;
debugln!("main: {:#?}", manifest);
let config = load_config(&root_dir, matches!(cli.command, Commands::Update(_)))?;
debugln!("main: {:#?}", 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,
);
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())?
}
_ => {
debugln!("main: 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 {
debugln!("main: maintaining link to {} at {:?}", pkg_name, path);
let pkg_path = io.get_package_path(sess.dependency_with_name(pkg_name)?);
if matches!(cli.command, Commands::Update(_)) || !pkg_path.clone().exists() {
let rt = Runtime::new()?;
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.clone(), path))
.unwrap_or(pkg_path);
if path.exists() {
let meta = path.symlink_metadata().map_err(|cause| {
Error::chain(
format!("Failed to read metadata of path {:?}.", path),
cause,
)
})?;
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) {
debugln!("main: removing existing link {:?}", path);
remove_symlink_dir(path).map_err(|cause| {
Error::chain(
format!("Failed to remove symlink at path {:?}.", path),
cause,
)
})?;
}
}
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|cause| {
Error::chain(format!("Failed to create directory {:?}.", parent), cause)
})?;
}
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).map_err(|cause| {
Error::chain(
format!(
"Failed to create symlink to {:?} at path {:?}.",
pkg_path, path
),
cause,
)
})?;
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),
Commands::Plugin(args) => {
let (plugin_name, plugin_args) = args
.split_first()
.ok_or_else(|| Error::new("No command specified.".to_string()))?;
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<()> {
Ok(std::os::unix::fs::symlink(p, q)?)
}
#[cfg(windows)]
pub fn symlink_dir(p: &Path, q: &Path) -> Result<()> {
Ok(std::os::windows::fs::symlink_dir(p, q)?)
}
#[cfg(unix)]
pub fn remove_symlink_dir(path: &Path) -> Result<()> {
Ok(std::fs::remove_file(path)?)
}
#[cfg(windows)]
pub fn remove_symlink_dir(path: &Path) -> Result<()> {
Ok(std::fs::remove_dir(path)?)
}
fn find_package_root(from: &Path) -> Result<PathBuf> {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
let mut path = canonicalize(from)
.map_err(|cause| Error::chain(format!("Failed to canonicalize path {:?}.", from), cause))?;
debugln!("find_package_root: canonicalized to {:?}", path);
#[cfg(unix)]
let limit_rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
#[cfg(unix)]
debugln!("find_package_root: limit rdev = {:?}", limit_rdev);
for _ in 0..100 {
debugln!("find_package_root: looking in {:?}", path);
if path.join("Bender.yml").exists() {
return Ok(path);
}
if !path.pop() {
return Err(Error::new(format!(
"No manifest (`Bender.yml` file) found. Stopped searching at filesystem root {:?}.",
path
)));
}
#[cfg(unix)]
{
let rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
debugln!("find_package_root: rdev = {:?}", rdev);
if rdev != limit_rdev {
return Err(Error::new(format!(
"No manifest (`Bender.yml` file) found. Stopped searching at filesystem boundary {:?}.",
path
)));
}
}
}
Err(Error::new(
"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;
debugln!("read_manifest: {:?}", path);
let file = File::open(path)
.map_err(|cause| Error::chain(format!("Cannot open manifest {:?}.", path), cause))?;
let partial: PartialManifest = serde_yaml_ng::from_reader(file)
.map_err(|cause| Error::chain(format!("Syntax error in manifest {:?}.", path), cause))?;
partial
.prefix_paths(path.parent().unwrap())
.map_err(|cause| Error::chain(format!("Error in manifest prefixing {:?}.", path), cause))?
.validate(&ValidationContext::default())
.map_err(|cause| Error::chain(format!("Error in manifest {:?}.", path), cause))
}
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)
.map_err(|cause| Error::chain(format!("Failed to canonicalize path {:?}.", from), cause))?;
debugln!("load_config: canonicalized to {:?}", path);
#[cfg(unix)]
let limit_rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
#[cfg(unix)]
debugln!("load_config: 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);
}
debugln!("load_config: 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();
debugln!("load_config: 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()),
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())
.map_err(|cause| Error::chain("Invalid configuration:", cause))?;
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;
debugln!("maybe_load_config: {:?}", path);
if !path.exists() {
return Ok(None);
}
let file = File::open(path)
.map_err(|cause| Error::chain(format!("Cannot open config {:?}.", path), cause))?;
let partial: PartialConfig = serde_yaml_ng::from_reader(file)
.map_err(|cause| Error::chain(format!("Syntax error in config {:?}.", path), cause))?;
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<()> {
debugln!("main: execute plugin `{}`", plugin);
let runtime = Runtime::new()?;
let io = SessionIo::new(sess);
let plugins = runtime.block_on(io.plugins(false))?;
let plugin = match plugins.get(plugin) {
Some(p) => p,
None => return Err(Error::new(format!("Unknown command `{}`.", plugin))),
};
debugln!("main: found plugin {:#?}", plugin);
let mut cmd = SysCommand::new(&plugin.path);
cmd.env(
"BENDER",
std::env::current_exe()
.map_err(|cause| Error::chain("Failed to determine current executable.", cause))?,
);
cmd.env(
"BENDER_CALL_DIR",
std::env::current_dir()
.map_err(|cause| Error::chain("Failed to determine current directory.", cause))?,
);
cmd.env("BENDER_MANIFEST_DIR", sess.root);
cmd.current_dir(sess.root);
cmd.args(args);
debugln!("main: executing plugin {:#?}", cmd);
let stat = cmd.status().map_err(|cause| {
Error::chain(
format!(
"Unable to spawn process for plugin `{}`. Command was {:#?}.",
plugin.name, cmd
),
cause,
)
})?;
std::process::exit(stat.code().unwrap_or(1));
}