use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use super::hardening::{self, HardeningStatus};
use crate::converter::format;
use crate::utils::config::{self, BendisConfig};
pub fn run(hard: bool) -> Result<()> {
if config::check_and_migrate_if_needed()? {
if !config::get_bendis_dir().exists() {
bail!(
"error: bendis_workspace directory not found\nrun `bendis init` to migrate or initialize the project"
);
}
}
println!("Updating dependencies...");
let cfg = BendisConfig::load()?;
let bendis_dir = config::get_bendis_dir();
let root_dir = config::get_root_dir();
if !bendis_dir.exists() {
bail!(
"error: bendis_workspace directory not found\nrun `bendis init` first to initialize the project"
);
}
let mut gitignore_warning = None;
if cfg.gitignore_check == 1 {
if let Err(e) = config::check_bendis_gitignore() {
gitignore_warning = Some(e.to_string());
}
}
println!(" Syncing hw/ and target/ directories to bendis_workspace/...");
config::copy_root_dirs_to_bendis_workspace()?;
restore_root_override_for_workspace_update(&bendis_dir, &root_dir)?;
println!(" Preparing cache files (it may take long)...");
run_bender_update_in_bendis(&bendis_dir, cfg.silent_mode == 1)?;
println!(" Converting URLs...");
let configs_changed = format::convert(&bendis_dir, &root_dir)?;
println!(" Updating bendis_workspace/.gitignore...");
config::ensure_bendis_workspace_gitignore_entries()?;
println!(" Updating root .gitignore...");
config::ensure_root_gitignore_excludes_hw_target()?;
println!(" Checking for changes...");
if configs_changed {
println!(" Detected changes, checking out dependencies...");
} else {
println!(" No changes detected, checking local dependency cache...");
}
run_bender_checkout_in_root(&root_dir, Path::new("bender"))?;
println!(" Cleaning up cache...");
cleanup_bendis_workspace_bender_dir(&bendis_dir, cfg.storage_saving_mode == 1)?;
verify_completion(&root_dir)?;
if let Some(warning_msg) = gitignore_warning {
eprintln!("\n{}", "warning:".yellow().bold());
eprintln!(" {}", warning_msg);
eprintln!(" Some files in bendis_workspace/ may be tracked by git unexpectedly.");
eprintln!(" Please review bendis_workspace/.gitignore and ensure it contains the required entries.");
eprintln!(
" To disable this check, set 'gitignore_check = 0' in: {}",
"bendis config".cyan()
);
}
if hard {
println!("Preparing AegisRTL workspace...");
let result = match hardening::run(&root_dir) {
Ok(result) => result,
Err(error) => {
eprintln!("Hardening failed; local RTL dependencies were not activated");
return Err(error);
}
};
for message in hardening_completion_messages(&result) {
println!("{message}");
}
}
println!("Done");
Ok(())
}
fn hardening_completion_messages(result: &hardening::HardeningResult) -> Vec<String> {
match result.status {
HardeningStatus::Completed => vec![
format!(
"Hardening completed. Changed RTL files: {}",
result.changed_files.len()
),
"Hardened RTL dependencies activated".to_string(),
],
HardeningStatus::Skipped => vec![
"Hardening skipped: aegisrtl/scripts/harden.sh was not found".to_string(),
"Local RTL dependencies activated without hardening".to_string(),
],
}
}
fn restore_root_override_for_workspace_update(bendis_dir: &Path, root_dir: &Path) -> Result<()> {
let source = bendis_dir.join(".bender.yml");
if !source.is_file() {
bail!("error: bendis_workspace/.bender.yml not found");
}
fs::copy(&source, root_dir.join(".bender.yml"))
.context("Failed to restore root .bender.yml from bendis_workspace")?;
Ok(())
}
fn run_bender_update_in_bendis(bendis_dir: &Path, silent: bool) -> Result<()> {
let mut cmd = Command::new("bender");
cmd.args(&["-d", "./bendis_workspace", "update"]);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.context("Failed to run bender. Is bender installed and in PATH?")?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
let mut stdout_reader = BufReader::new(stdout);
let mut stderr_reader = BufReader::new(stderr);
let mut log_buffer = Vec::new();
let mut line = String::new();
while stdout_reader.read_line(&mut line).unwrap_or(0) > 0 {
if !silent {
print!("{}", line);
}
log_buffer.push(line.clone());
line.clear();
}
while stderr_reader.read_line(&mut line).unwrap_or(0) > 0 {
if !silent {
eprint!("{}", line);
}
log_buffer.push(line.clone());
line.clear();
}
let status = child.wait().context("Failed to wait for bender process")?;
if !status.success() {
eprintln!(
"\n{}",
"Error: bender update failed in bendis_workspace/".red()
);
eprintln!("\nCommand output:");
for log_line in &log_buffer {
eprint!("{}", log_line);
}
let has_fetch_error = log_buffer.iter().any(|line| {
line.contains("Fetching Dependency")
&& line.contains("cannot satisfy requirement")
&& line.contains("may need fetch")
});
if has_fetch_error {
eprintln!("\n{}", "Possible solution:".yellow());
eprintln!(
" 1. Delete {} directory",
"bendis_workspace/.bender/".cyan()
);
eprintln!(" 2. Check your internet connection");
eprintln!(" 3. Run {} again", "bendis update".cyan());
}
bail!("error: bender update in bendis_workspace/ failed");
}
let lock_file = bendis_dir.join("Bender.lock");
if !lock_file.exists() {
bail!("error: failed to generate bendis_workspace/Bender.lock");
}
Ok(())
}
fn run_bender_checkout_in_root(root_dir: &Path, bender: &Path) -> Result<()> {
let local = Command::new(bender)
.args(["--local", "--git-submodules", "false", "checkout"])
.current_dir(root_dir)
.output()
.context("Failed to run bender. Is bender installed and in PATH?")?;
if local.status.success() && checkouts_have_complete_submodules(root_dir)? {
return Ok(());
}
println!(" Local checkout could not be completed; retrying with remote access");
let remote = Command::new(bender)
.arg("checkout")
.current_dir(root_dir)
.output()
.context("Failed to retry bender checkout with remote access")?;
if !remote.status.success() {
eprint!("{}", String::from_utf8_lossy(&remote.stdout));
eprint!("{}", String::from_utf8_lossy(&remote.stderr));
bail!("bender checkout failed in root directory");
}
if !checkouts_have_complete_submodules(root_dir)? {
bail!("bender checkout did not produce complete dependency submodules");
}
Ok(())
}
fn checkouts_have_complete_submodules(root_dir: &Path) -> Result<bool> {
let checkouts = root_dir.join(".bender/git/checkouts");
if !checkouts.is_dir() {
return Ok(true);
}
for entry in fs::read_dir(&checkouts)
.with_context(|| format!("Failed to read {}", checkouts.display()))?
{
let path = entry?.path();
if !path.is_dir() || !path.join(".git").exists() {
continue;
}
let submodules = Command::new("git")
.args(["submodule", "status", "--recursive"])
.current_dir(&path)
.output()
.with_context(|| format!("Failed to inspect submodules in {}", path.display()))?;
if !submodules.status.success()
|| submodules
.stdout
.split(|byte| *byte == b'\n')
.filter(|line| !line.is_empty())
.any(|line| line.first() != Some(&b' '))
{
return Ok(false);
}
}
Ok(true)
}
fn cleanup_bendis_workspace_bender_dir(bendis_dir: &Path, full_cleanup: bool) -> Result<()> {
if full_cleanup {
let bender_dir = bendis_dir.join(".bender");
if bender_dir.exists() {
fs::remove_dir_all(&bender_dir)
.context("Failed to remove bendis_workspace/.bender/")?;
}
} else {
}
Ok(())
}
fn verify_completion(root_dir: &Path) -> Result<()> {
let mut missing = Vec::new();
if !root_dir.join("Bender.yml").exists() {
missing.push("Bender.yml");
}
if !root_dir.join(".bender.yml").exists() {
missing.push(".bender.yml");
}
if !root_dir.join("Bender.lock").exists() {
missing.push("Bender.lock");
}
if !root_dir.join(".bender").exists() {
missing.push(".bender/");
}
if !missing.is_empty() {
bail!(
"error: verification failed, missing: {}",
missing.join(", ")
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
hardening_completion_messages, restore_root_override_for_workspace_update,
run_bender_checkout_in_root,
};
use crate::commands::hardening::{HardeningResult, HardeningStatus};
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn workspace_update_restores_root_override_before_running_bender() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root: PathBuf = std::env::temp_dir().join(format!("bendis-restore-{nonce}"));
let workspace = root.join("bendis_workspace");
fs::create_dir_all(&workspace).unwrap();
fs::write(
root.join(".bender.yml"),
"overrides:\n core: { path: '../aegisrtl/core' }\n",
)
.unwrap();
fs::write(
workspace.join(".bender.yml"),
"overrides:\n core: { git: 'ssh://ihep/core.git', rev: abc }\n",
)
.unwrap();
restore_root_override_for_workspace_update(&workspace, &root).unwrap();
assert_eq!(
fs::read_to_string(root.join(".bender.yml")).unwrap(),
fs::read_to_string(workspace.join(".bender.yml")).unwrap()
);
fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn root_checkout_uses_local_cache_without_removing_it() {
use std::os::unix::fs::PermissionsExt;
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!("bendis-root-checkout-{nonce}"));
let bin = root.join("fake-bender");
let log = root.join("calls.log");
fs::create_dir_all(root.join(".bender/git/db/core")).unwrap();
fs::write(root.join(".bender/git/db/core/marker"), "keep\n").unwrap();
fs::write(
&bin,
format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\n", log.display()),
)
.unwrap();
let mut permissions = fs::metadata(&bin).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&bin, permissions).unwrap();
run_bender_checkout_in_root(&root, &bin).unwrap();
assert_eq!(
fs::read_to_string(&log).unwrap(),
"--local --git-submodules false checkout\n"
);
assert!(root.join(".bender/git/db/core/marker").is_file());
fs::remove_dir_all(root).unwrap();
}
#[cfg(unix)]
#[test]
fn root_checkout_retries_once_with_remote_access() {
use std::os::unix::fs::PermissionsExt;
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!("bendis-root-retry-{nonce}"));
let bin = root.join("fake-bender");
let log = root.join("calls.log");
fs::create_dir_all(&root).unwrap();
fs::write(
&bin,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\n[ \"$1\" != \"--local\" ]\n",
log.display()
),
)
.unwrap();
let mut permissions = fs::metadata(&bin).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&bin, permissions).unwrap();
run_bender_checkout_in_root(&root, &bin).unwrap();
assert_eq!(
fs::read_to_string(&log).unwrap(),
"--local --git-submodules false checkout\ncheckout\n"
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn skipped_hardening_reports_unmodified_local_rtl_activation() {
let result = HardeningResult {
status: HardeningStatus::Skipped,
changed_files: Vec::new(),
};
assert_eq!(
hardening_completion_messages(&result),
vec![
"Hardening skipped: aegisrtl/scripts/harden.sh was not found".to_string(),
"Local RTL dependencies activated without hardening".to_string(),
]
);
}
}