Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion crates/rb-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ clap = { version = "4.0", features = ["derive", "color", "help", "usage", "env"]
clap_complete = "4.0"
rb-core = { path = "../rb-core" }
rb-gemfile = { path = "../rb-gemfile" }
rb-progress = { path = "../rb-progress" }
rb-progress = { path = "../rb-progress", features = ["rb-task"] }
rb-task = { path = "../rb-task" }
rb-packages = { path = "../rb-packages" }
anyhow = "1"
home = "0.5"
colored = "2.0"
log = "0.4"
Expand Down
11 changes: 8 additions & 3 deletions crates/rb-cli/src/bin/rb.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::Parser;
use clap::{CommandFactory, FromArgMatches};
use rb_cli::config::TrackedConfig;
use rb_cli::dispatch::dispatch_command;
use rb_cli::error_display::{
Expand Down Expand Up @@ -33,7 +33,9 @@ fn handle_command_error(error: ButlerError, context: &CommandContext) -> ! {
}

fn main() {
let cli = Cli::parse();
let matches = Cli::command().get_matches();
let package_backend_source = matches.value_source("package_backend");
let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());

// Skip logging for bash completion (must be silent)
if !matches!(cli.command, Some(Commands::BashComplete { .. })) {
Expand All @@ -55,7 +57,10 @@ fn main() {
std::process::exit(0);
};

let tracked_config = TrackedConfig::from_merged(&cli_parsed.config, &file_config);
let mut tracked_config = TrackedConfig::from_merged(&cli_parsed.config, &file_config);
if package_backend_source == Some(clap::parser::ValueSource::EnvVariable) {
tracked_config.package_backend.source = rb_cli::config::ConfigSource::EnvVar;
}

if !tracked_config.work_dir.source.is_default() {
let target_dir = tracked_config.work_dir.get();
Expand Down
36 changes: 21 additions & 15 deletions crates/rb-cli/src/commands/exec.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
use super::sync_report::{Completion, report_sync, sync_reporter};
use super::sync_report::Completion;
use log::{debug, info};
use rb_core::butler::{ButlerError, ButlerRuntime, Command};

pub fn exec_command(butler: ButlerRuntime, program_args: Vec<String>) -> Result<(), ButlerError> {
let backend = crate::packages::select(crate::config::PackageBackendKind::Bundler, None, false);
exec_with_backend(butler, program_args, backend.as_ref())
}

pub(crate) fn exec_with_backend(
butler: ButlerRuntime,
program_args: Vec<String>,
backend: &dyn crate::packages::PackageBackend,
) -> Result<(), ButlerError> {
if program_args.is_empty() {
return Err(ButlerError::General(
"No program specified for execution.\nProper usage: rb exec <program> [arguments...]\nFor example: rb exec gem list\n rb exec bundle install".to_string()
Expand All @@ -21,21 +30,18 @@ pub fn exec_command(butler: ButlerRuntime, program_args: Vec<String>) -> Result<
program
);

if let Some(bundler_runtime) = butler.bundler_runtime() {
match bundler_runtime.check_sync(&butler) {
Ok(false) => {
report_sync(sync_reporter(), Completion::Compact, |handler| {
bundler_runtime.synchronize_with_events(&butler, handler)
})?;
}
Ok(true) => {
debug!("Bundler environment already synchronized");
}
Err(e) => {
debug!("Unable to verify bundler synchronization status: {}", e);
}
let butler = match backend
.check(&butler)
.map_err(|error| ButlerError::General(format!("{error:#}")))?
{
crate::packages::PackageStatus::Ready(runtime) => *runtime,
crate::packages::PackageStatus::NeedsPreparation => {
super::sync_report::report_packages(Completion::Compact, |events| {
backend.prepare(&butler, events)
})?
.runtime
}
}
};

debug!("Program: {}", program);
debug!("Arguments: {:?}", args);
Expand Down
4 changes: 4 additions & 0 deletions crates/rb-cli/src/commands/info/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ pub fn config_command(config: &TrackedConfig) -> Result<(), ButlerError> {
);
println!();

println!("Package Backend: {}", config.package_backend.get());
println!(" Source: {}", config.package_backend.source);
println!();

println!("{}", "Configuration sources (in priority order):".dimmed());
println!(" {} CLI arguments", "1.".dimmed());
println!(" {} Configuration file", "2.".dimmed());
Expand Down
23 changes: 20 additions & 3 deletions crates/rb-cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use rb_core::butler::{ButlerError, ButlerRuntime};
use rb_core::project::{ProjectRuntime, RbprojectDetector};
use std::path::PathBuf;

use super::exec::exec_command;
use super::exec::exec_with_backend;

fn list_available_scripts(
butler_runtime: ButlerRuntime,
Expand Down Expand Up @@ -151,6 +151,23 @@ pub fn run_command(
script_name: Option<String>,
args: Vec<String>,
project_file: Option<PathBuf>,
) -> Result<(), ButlerError> {
let backend = crate::packages::select(crate::config::PackageBackendKind::Bundler, None, false);
run_with_backend(
butler_runtime,
script_name,
args,
project_file,
backend.as_ref(),
)
}

pub(crate) fn run_with_backend(
butler_runtime: ButlerRuntime,
script_name: Option<String>,
args: Vec<String>,
project_file: Option<PathBuf>,
backend: &dyn crate::packages::PackageBackend,
) -> Result<(), ButlerError> {
if script_name.is_none() {
return list_available_scripts(butler_runtime, project_file);
Expand Down Expand Up @@ -231,8 +248,8 @@ pub fn run_command(

info!("Delegating to exec command with args: {:?}", full_args);

// Delegate to exec_command for consistent behavior (auto bundle exec, env composition)
exec_command(butler_runtime, full_args)
// Delegate to exec_with_backend for consistent behavior (auto bundle exec, env composition)
exec_with_backend(butler_runtime, full_args, backend)
}

/// Parse a command string into program and arguments
Expand Down
27 changes: 12 additions & 15 deletions crates/rb-cli/src/commands/sync.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
use super::sync_report::{Completion, report_sync, sync_reporter};
use log::debug;
use super::sync_report::Completion;
use rb_core::butler::{ButlerError, ButlerRuntime};

pub fn sync_command(butler_runtime: ButlerRuntime) -> Result<(), ButlerError> {
debug!("Starting sync command");

let bundler_runtime = match butler_runtime.bundler_runtime() {
Some(bundler) => bundler,
None => {
return Err(ButlerError::General(
"Bundler environment not detected.\n\nNo Gemfile found in the current directory or its ancestors.\nThe sync command requires a bundler-managed project to operate.\n\nTo create a new bundler project, create a Gemfile with: echo 'source \"https://rubygems.org\"' > Gemfile".to_string()
));
}
};
let backend = crate::packages::select(crate::config::PackageBackendKind::Bundler, None, false);
sync_with_backend(butler_runtime, backend.as_ref())
}

report_sync(sync_reporter(), Completion::Tree, |handler| {
bundler_runtime.synchronize_with_events(&butler_runtime, handler)
})
pub(crate) fn sync_with_backend(
runtime: ButlerRuntime,
backend: &dyn crate::packages::PackageBackend,
) -> Result<(), ButlerError> {
super::sync_report::report_packages(Completion::Tree, |events| {
backend.prepare(&runtime, events)
})?;
Ok(())
}

#[cfg(test)]
Expand Down
105 changes: 92 additions & 13 deletions crates/rb-cli/src/commands/sync_report.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use rb_core::bundler::{SyncEvent, SyncResult};
use rb_core::bundler::SyncEvent;
#[cfg(test)]
use rb_core::bundler::SyncResult;
use rb_core::butler::ButlerError;
use rb_progress::Reporter;
use std::io::IsTerminal;
Expand All @@ -16,16 +18,26 @@ pub(super) enum Completion {
Compact,
}

pub(super) fn report_sync(
pub(crate) fn report_preparation(
reporter: Reporter,
completion: Completion,
run: impl FnOnce(&mut dyn FnMut(SyncEvent<'_>)) -> std::io::Result<SyncResult>,
) -> Result<(), ButlerError> {
run: impl FnOnce(
crate::packages::PackageEvents<'_>,
) -> anyhow::Result<crate::packages::PreparedEnvironment>,
) -> Result<crate::packages::PreparedEnvironment, ButlerError> {
let progress = reporter.handle();
progress.worker_started(0, "Bundler");
let failed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let tasks = rb_task::TaskEvents::default();
tasks.subscribe(std::sync::Arc::new(FailureTracker(failed.clone())));
tasks.subscribe(reporter.task_sink());
let mut active_phase = None;
let mut active_detail = None;
let result = run(&mut |event| {
let mut started = false;
let mut handler = |event: SyncEvent<'_>| {
if !started {
progress.worker_started(0, "Bundler");
started = true;
}
let phase = match event {
SyncEvent::Checking => "checking dependencies",
SyncEvent::CheckingLockfile => "checking lockfile",
Expand All @@ -51,27 +63,74 @@ pub(super) fn report_sync(
}
progress.event(phase, 0, 1, None);
progress.worker_event(Some(0), phase, 0, 0, Some("Bundler".into()));
};
let result = run(crate::packages::PackageEvents {
tasks,
sync: &mut handler,
});
match result {
Ok(result) => {
progress.worker_finished(0);
if started {
progress.worker_finished(0);
}
if let Some(phase) = active_phase {
progress.event(phase, 1, 1, active_detail);
}
let summary = match result {
SyncResult::AlreadySynced => "Everything is already in order",
SyncResult::Synchronized => "Your bundle is ready",
let summary = if result.changed {
"Your bundle is ready"
} else {
"Everything is already in order"
};
match completion {
Completion::Tree => reporter.finish_with_summary(summary),
Completion::Compact => reporter.finish_compact(summary),
}
Ok(())
Ok(result)
}
Err(error) => {
progress.worker_failed(0, error.to_string());
if !failed.load(std::sync::atomic::Ordering::Relaxed) {
if !started {
progress.worker_started(0, "preparing dependencies");
}
progress.worker_failed(0, format!("{error:#}"));
}
reporter.finish();
Err(ButlerError::General(error.to_string()))
Err(ButlerError::General(format!("{error:#}")))
}
}
}

pub(crate) fn report_packages(
completion: Completion,
run: impl FnOnce(
crate::packages::PackageEvents<'_>,
) -> anyhow::Result<crate::packages::PreparedEnvironment>,
) -> Result<crate::packages::PreparedEnvironment, ButlerError> {
report_preparation(sync_reporter(), completion, run)
}

#[cfg(test)]
fn report_sync(
reporter: Reporter,
completion: Completion,
run: impl FnOnce(&mut dyn FnMut(SyncEvent<'_>)) -> std::io::Result<SyncResult>,
) -> Result<(), ButlerError> {
report_preparation(reporter, completion, |events| {
let result = run(events.sync)?;
Ok(crate::packages::PreparedEnvironment {
runtime: rb_core::butler::ButlerRuntime::empty(".".into(), ".".into()),
changed: result == SyncResult::Synchronized,
})
})
.map(|_| ())
}

struct FailureTracker(std::sync::Arc<std::sync::atomic::AtomicBool>);

impl rb_task::TaskEventSink for FailureTracker {
fn event(&self, event: rb_task::TaskEvent) {
if matches!(event, rb_task::TaskEvent::Failed { .. }) {
self.0.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
}
Expand Down Expand Up @@ -127,6 +186,26 @@ mod tests {
}
}

#[test]
fn failure_before_task_events_is_reported_as_failure() {
let buffer = Buffer::default();
let reporter = Reporter::with_plain_writer("sync", buffer.clone());
let result = report_preparation(reporter, Completion::Tree, |_| {
Err(anyhow::anyhow!("No project found"))
});
assert!(
result
.err()
.unwrap()
.to_string()
.contains("No project found")
);
let text = String::from_utf8(buffer.0.lock().unwrap().clone()).unwrap();
assert!(text.contains("failed: No project found"), "{text}");
assert!(text.contains("[overall] failed"), "{text}");
assert!(!text.contains("[overall] complete"), "{text}");
}

#[test]
fn reporter_retains_bundler_failure() {
let buffer = Buffer::default();
Expand Down
Loading
Loading