use std;
use std::ffi::OsString;
use std::fs::{canonicalize, metadata};
use std::path::{Path, PathBuf};
use std::process::Command as SysCommand;
use clap::parser::ValuesRef;
use clap::{Arg, ArgAction, Command};
use serde_yaml;
use crate::cmd;
use crate::config::{
Config, Locked, LockedPackage, LockedSource, Manifest, Merge, PartialConfig, PrefixPaths,
Validate,
};
use crate::error::*;
use crate::resolver::DependencyResolver;
use crate::sess::{Session, SessionArenas, SessionIo};
use tokio::runtime::Runtime;
pub fn main() -> Result<()> {
let app = Command::new(env!("CARGO_PKG_NAME"))
.subcommand_required(true)
.arg_required_else_help(true)
.allow_external_subcommands(true)
.version(env!("CARGO_PKG_VERSION"))
.author(env!("CARGO_PKG_AUTHORS"))
.about("A dependency management tool for hardware projects.")
.arg(
Arg::new("dir")
.short('d')
.long("dir")
.num_args(1)
.global(true)
.help("Sets a custom root working directory"),
)
.arg(
Arg::new("local")
.long("local")
.global(true)
.num_args(0)
.action(ArgAction::SetTrue)
.help("Disables fetching of remotes (e.g. for air-gapped computers)"),
)
.subcommand(
Command::new("update").about("Update the dependencies").arg(
Arg::new("fetch")
.short('f')
.long("fetch")
.num_args(0)
.action(ArgAction::SetTrue)
.help("forces fetch of git dependencies"),
),
)
.subcommand(cmd::path::new())
.subcommand(cmd::parents::new())
.subcommand(cmd::clone::new())
.subcommand(cmd::packages::new())
.subcommand(cmd::sources::new())
.subcommand(cmd::config::new())
.subcommand(cmd::script::new())
.subcommand(cmd::checkout::new())
.subcommand(cmd::vendor::new())
.subcommand(cmd::fusesoc::new());
let app = if cfg!(debug_assertions) {
app.arg(
Arg::new("debug")
.long("debug")
.global(true)
.num_args(0)
.action(ArgAction::SetTrue)
.help("Print additional debug information"),
)
} else {
app
};
let matches = app.get_matches();
if matches.contains_id("debug") && matches.get_flag("debug") {
ENABLE_DEBUG.store(true, std::sync::atomic::Ordering::Relaxed);
}
let mut force_fetch = false;
if let Some(("update", intern_matches)) = matches.subcommand() {
force_fetch = intern_matches.get_flag("fetch");
if matches.get_flag("local") && intern_matches.get_flag("fetch") {
warnln!(
"As --local argument is set for bender command, no fetching will be performed."
);
}
}
let root_dir: PathBuf = match matches.get_one::<String>("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)?;
debugln!("main: {:#?}", config);
let sess_arenas = SessionArenas::new();
let sess = Session::new(
&root_dir,
&manifest,
&config,
&sess_arenas,
matches.get_flag("local"),
force_fetch,
);
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 = match matches.subcommand() {
Some((command, matches)) => {
#[allow(clippy::unnecessary_unwrap)]
if command == "fusesoc" && matches.get_flag("single") {
return cmd::fusesoc::run_single(&sess, matches);
} else if command == "update" || locked_existing.is_none() {
if manifest.frozen {
return Err(Error::new(format!(
"Refusing to update dependencies because the package is frozen.
Remove the `frozen: true` from {:?} to proceed; there be dragons.",
manifest_path
)));
}
debugln!("main: lockfile {:?} outdated", lock_path);
let res = DependencyResolver::new(&sess);
let locked_new = res.resolve()?;
write_lockfile(&locked_new, &root_dir.join("Bender.lock"), &root_dir)?;
locked_new
} else {
debugln!("main: lockfile {:?} up-to-date", lock_path);
locked_existing.unwrap()
}
}
None => {
return Err(Error::new("Please specify a command.".to_string()));
}
};
sess.load_locked(&locked)?;
{
let rt = Runtime::new()?;
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 = rt.block_on(io.checkout(sess.dependency_with_name(pkg_name)?))?;
let pkg_path = path
.parent()
.and_then(|path| pathdiff::diff_paths(pkg_path, path))
.unwrap_or_else(|| pkg_path.into());
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() {
warnln!(
"Skipping link to package {} at {:?} since there is something there",
pkg_name,
path
);
continue;
}
if path.read_link().map(|d| d != pkg_path).unwrap_or(true) {
debugln!("main: removing existing link {:?}", path);
std::fs::remove_file(path).map_err(|cause| {
Error::chain(
format!("Failed to remove symlink at path {:?}.", path),
cause,
)
})?;
}
}
if !path.exists() {
stageln!("Linking", "{} ({:?})", pkg_name, path);
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,
};
std::os::unix::fs::symlink(&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();
}
}
}
}
match matches.subcommand() {
Some(("path", matches)) => cmd::path::run(&sess, matches),
Some(("parents", matches)) => cmd::parents::run(&sess, matches),
Some(("clone", matches)) => cmd::clone::run(&sess, &root_dir, matches),
Some(("packages", matches)) => cmd::packages::run(&sess, matches),
Some(("sources", matches)) => cmd::sources::run(&sess, matches),
Some(("config", matches)) => cmd::config::run(&sess, matches),
Some(("script", matches)) => cmd::script::run(&sess, matches),
Some(("checkout", matches)) => cmd::checkout::run(&sess, matches),
Some(("update", _)) => Ok(()),
Some(("vendor", matches)) => cmd::vendor::run(&sess, matches),
Some(("fusesoc", matches)) => cmd::fusesoc::run(&sess, matches),
Some((plugin, matches)) => execute_plugin(&sess, plugin, matches.get_many::<OsString>("")),
_ => Ok(()),
}
}
fn find_package_root(from: &Path) -> Result<PathBuf> {
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);
let limit_rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
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);
}
let tested_path = path.clone();
if !path.pop() {
return Err(Error::new(format!(
"Stopped at filesystem root {:?}.",
path
)));
}
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!(
"Stopped at filesystem boundary {:?}.",
tested_path
)));
}
}
Err(Error::new("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::from_reader(file)
.map_err(|cause| Error::chain(format!("Syntax error in manifest {:?}.", path), cause))?;
let manifest = partial
.validate()
.map_err(|cause| Error::chain(format!("Error in manifest {:?}.", path), cause))?;
Ok(manifest.prefix_paths(path.parent().unwrap()))
}
fn load_config(from: &Path) -> Result<Config> {
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);
let limit_rdev: Option<_> = metadata(&path).map(|m| m.dev()).ok();
debugln!("load_config: limit rdev = {:?}", limit_rdev);
for _ in 0..100 {
if let Some(cfg) = maybe_load_config(&path.join("Bender.local"))? {
out = out.merge(cfg);
}
debugln!("load_config: looking in {:?}", path);
if let Some(cfg) = maybe_load_config(&path.join(".bender.yml"))? {
out = out.merge(cfg);
}
if !path.pop() {
break;
}
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)? {
out = out.merge(cfg);
}
}
if let Some(cfg) = maybe_load_config(Path::new("/etc/bender.yml"))? {
out = out.merge(cfg);
}
let default_cfg = PartialConfig {
database: Some(from.join(".bender")),
git: Some("git".into()),
overrides: None,
plugins: None,
};
out = out.merge(default_cfg);
out.validate()
.map_err(|cause| Error::chain("Invalid configuration:", cause))
}
fn maybe_load_config(path: &Path) -> 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::from_reader(file)
.map_err(|cause| Error::chain(format!("Syntax error in config {:?}.", path), cause))?;
Ok(Some(partial.prefix_paths(path.parent().unwrap())))
}
fn read_lockfile(path: &Path, root_dir: &Path) -> Result<Locked> {
debugln!("read_lockfile: {:?}", path);
use std::fs::File;
let file = File::open(path)
.map_err(|cause| Error::chain(format!("Cannot open lockfile {:?}.", path), cause))?;
let locked_loaded: Result<Locked> = serde_yaml::from_reader(file)
.map_err(|cause| Error::chain(format!("Syntax error in lockfile {:?}.", path), cause));
Ok(Locked {
packages: locked_loaded?
.packages
.iter()
.map(|pack| {
if let LockedSource::Path(path) = &pack.1.source {
(
pack.0.clone(),
LockedPackage {
revision: pack.1.revision.clone(),
version: pack.1.version.clone(),
source: LockedSource::Path(if path.is_relative() {
path.clone().prefix_paths(root_dir)
} else {
path.clone()
}),
dependencies: pack.1.dependencies.clone(),
},
)
} else {
(pack.0.clone(), pack.1.clone())
}
})
.collect(),
})
}
fn write_lockfile(locked: &Locked, path: &Path, root_dir: &Path) -> Result<()> {
debugln!("write_lockfile: {:?}", path);
let adapted_locked = Locked {
packages: locked
.packages
.iter()
.map(|pack| {
if let LockedSource::Path(path) = &pack.1.source {
(
pack.0.clone(),
LockedPackage {
revision: pack.1.revision.clone(),
version: pack.1.version.clone(),
source: LockedSource::Path(
path.strip_prefix(root_dir).unwrap_or(path).to_path_buf(),
),
dependencies: pack.1.dependencies.clone(),
},
)
} else {
(pack.0.clone(), pack.1.clone())
}
})
.collect(),
};
use std::fs::File;
let file = File::create(path)
.map_err(|cause| Error::chain(format!("Cannot create lockfile {:?}.", path), cause))?;
serde_yaml::to_writer(file, &adapted_locked)
.map_err(|cause| Error::chain(format!("Cannot write lockfile {:?}.", path), cause))?;
Ok(())
}
fn execute_plugin(
sess: &Session,
plugin: &str,
matches: Option<ValuesRef<OsString>>,
) -> Result<()> {
debugln!("main: execute plugin `{}`", plugin);
let runtime = Runtime::new()?;
let io = SessionIo::new(sess);
let plugins = runtime.block_on(io.plugins())?;
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);
if let Some(args) = matches {
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));
}