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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ jobs:
cargo test -p rb-progress --features rb-task
cargo build -p rb-progress --features rb-task --example progress_ui_probe --example tasks
python3 crates/rb-progress/tests/pty_ui_test.py target/debug/examples/progress_ui_probe target/debug/examples/parallel
python3 crates/rb-cli/tests/sync_ui_test.py target/debug/rb

- name: Build release
run: cargo build --release
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/rb-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ path = "src/bin/rb.rs"
clap = { version = "4.0", features = ["derive", "color", "help", "usage", "env"] }
clap_complete = "4.0"
rb-core = { path = "../rb-core" }
rb-progress = { path = "../rb-progress" }
home = "0.5"
colored = "2.0"
log = "0.4"
Expand Down
29 changes: 4 additions & 25 deletions crates/rb-cli/src/commands/exec.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use colored::*;
use super::sync_report::{Completion, report_sync, sync_reporter};
use log::{debug, info};
use rb_core::butler::{ButlerError, ButlerRuntime, Command};

Expand All @@ -24,30 +24,9 @@ pub fn exec_command(butler: ButlerRuntime, program_args: Vec<String>) -> Result<
if let Some(bundler_runtime) = butler.bundler_runtime() {
match bundler_runtime.check_sync(&butler) {
Ok(false) => {
println!(
"{} {}",
"🎩 Butler Notice:".bright_blue().bold(),
"Bundler environment requires synchronization. Preparing now...".dimmed()
);

match bundler_runtime.synchronize(&butler, |line| {
println!("{}", line.dimmed());
}) {
Ok(_) => {
println!(
"{} {}",
"✨".bright_green(),
"Environment meticulously prepared. Proceeding with execution..."
.green()
);
}
Err(e) => {
return Err(ButlerError::General(format!(
"Failed to prepare bundler environment: {}",
e
)));
}
}
report_sync(sync_reporter(), Completion::Compact, |handler| {
bundler_runtime.synchronize_with_events(&butler, handler)
})?;
}
Ok(true) => {
debug!("Bundler environment already synchronized");
Expand Down
1 change: 1 addition & 0 deletions crates/rb-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod new;
pub mod run;
pub mod shell_integration;
pub mod sync;
mod sync_report;
pub mod version;

pub use exec::exec_command;
Expand Down
98 changes: 4 additions & 94 deletions crates/rb-cli/src/commands/sync.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::sync_report::{Completion, report_sync, sync_reporter};
use log::debug;
use rb_core::bundler::SyncResult;
use rb_core::butler::{ButlerError, ButlerRuntime};

pub fn sync_command(butler_runtime: ButlerRuntime) -> Result<(), ButlerError> {
Expand All @@ -14,99 +14,9 @@ pub fn sync_command(butler_runtime: ButlerRuntime) -> Result<(), ButlerError> {
}
};

println!("🔄 Synchronizing Bundler Environment");
println!();
println!("📂 Project: {}", bundler_runtime.root.display());
println!("📄 Gemfile: {}", bundler_runtime.gemfile_path().display());
println!("📦 Vendor: {}", bundler_runtime.vendor_dir().display());
println!();

match bundler_runtime.synchronize(&butler_runtime, |line| {
println!("{}", line);
}) {
Ok(SyncResult::AlreadySynced) => {
println!("✅ Environment Already Synchronized");
println!();
println!(
"Your bundler environment is meticulously prepared and ready for distinguished service."
);
println!("All dependencies are satisfied and properly installed.");
}
Ok(SyncResult::Synchronized) => {
println!();
println!("✅ Environment Successfully Synchronized");
println!();
println!(
"Your bundler environment has been meticulously prepared with all required dependencies."
);
println!("The installation is complete and ready for distinguished service.");
}
Err(e) => {
println!();
println!("❌ Synchronization Failed");
println!();

let error_msg = e.to_string();

if error_msg.contains("extconf.rb failed")
|| error_msg.contains("native extension")
|| error_msg.contains("development tools")
|| error_msg.contains("compiler failed")
|| error_msg.contains("Makefile")
{
println!("🔧 Native Extension Compilation Failed");
println!();
println!("Some gems in your Gemfile require native extensions to be compiled.");
println!("This requires development tools to be installed on your system.");
println!();
println!("📋 Required Development Tools:");
println!(" • Build essentials (gcc, make, etc.)");
println!(" • Ruby development headers");
println!(" • Platform-specific libraries");
println!();
println!("🚀 Installation Commands:");
println!(" Ubuntu/Debian: sudo apt-get install build-essential ruby-dev");
println!(
" CentOS/RHEL: sudo yum groupinstall 'Development Tools' && sudo yum install ruby-devel"
);
println!(" Alpine Linux: sudo apk add build-base ruby-dev");
println!(" macOS: xcode-select --install");
println!();
println!("💡 Alternative Solutions:");
println!(" • Use pre-compiled gem versions if available");
println!(" • Consider using --platform ruby to force source compilation");
println!(" • Use Docker with a development-ready base image");
} else if error_msg.contains("not found") && error_msg.contains("bundler") {
println!("📦 Bundler Not Found");
println!();
println!("The bundler executable is not available in your Ruby environment.");
println!();
println!("🚀 Installation:");
println!(" gem install bundler");
} else if error_msg.contains("permission") || error_msg.contains("Permission") {
println!("🔒 Permission Denied");
println!();
println!("Unable to write to the gem installation directory.");
println!();
println!("💡 Solutions:");
println!(" • Ensure write permissions to the vendor directory");
println!(" • Check file system permissions");
println!(" • Consider using a user-specific gem directory");
} else {
println!("⚠️ Bundle Installation Error");
println!();
println!("Details: {}", error_msg);
}

println!();
println!("🔍 For detailed error information, run:");
println!(" rb exec bundle install --verbose");

return Err(ButlerError::General(e.to_string()));
}
}

Ok(())
report_sync(sync_reporter(), Completion::Tree, |handler| {
bundler_runtime.synchronize_with_events(&butler_runtime, handler)
})
}

#[cfg(test)]
Expand Down
144 changes: 144 additions & 0 deletions crates/rb-cli/src/commands/sync_report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use rb_core::bundler::{SyncEvent, SyncResult};
use rb_core::butler::ButlerError;
use rb_progress::Reporter;
use std::io::IsTerminal;

pub(super) fn sync_reporter() -> Reporter {
if std::io::stderr().is_terminal() && std::io::stdout().is_terminal() {
Reporter::with_writer("Preparing your bundle", std::io::stderr())
} else {
Reporter::with_plain_writer("Preparing your bundle", std::io::stderr())
}
}

pub(super) enum Completion {
Tree,
Compact,
}

pub(super) fn report_sync(
reporter: Reporter,
completion: Completion,
run: impl FnOnce(&mut dyn FnMut(SyncEvent<'_>)) -> std::io::Result<SyncResult>,
) -> Result<(), ButlerError> {
let progress = reporter.handle();
progress.worker_started(0, "Bundler");
let mut active_phase = None;
let mut active_detail = None;
let result = run(&mut |event| {
let phase = match event {
SyncEvent::Checking => "checking dependencies",
SyncEvent::CheckingLockfile => "checking lockfile",
SyncEvent::LockfileChecked { changed } => {
active_detail = Some(
if changed {
"updated lockfile"
} else {
"unchanged"
}
.into(),
);
return;
}
SyncEvent::Installing => "installing dependencies",
SyncEvent::Output { line, .. } => {
progress.worker_output(0, line);
return;
}
};
if let Some(previous) = active_phase.replace(phase) {
progress.event(previous, 1, 1, active_detail.take());
}
progress.event(phase, 0, 1, None);
progress.worker_event(Some(0), phase, 0, 0, Some("Bundler".into()));
});
match result {
Ok(result) => {
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",
};
match completion {
Completion::Tree => reporter.finish_with_summary(summary),
Completion::Compact => reporter.finish_compact(summary),
}
Ok(())
}
Err(error) => {
progress.worker_failed(0, error.to_string());
reporter.finish();
Err(ButlerError::General(error.to_string()))
}
}
}

#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Default)]
struct Buffer(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);

impl std::io::Write for Buffer {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

#[test]
fn reporter_maps_bundler_phases_and_success() {
for result in [SyncResult::AlreadySynced, SyncResult::Synchronized] {
let buffer = Buffer::default();
let reporter = Reporter::with_writer("sync", buffer.clone());
report_sync(reporter, Completion::Tree, |handler| {
handler(SyncEvent::Checking);
handler(SyncEvent::CheckingLockfile);
handler(SyncEvent::Installing);
handler(SyncEvent::Output {
line: "compiler output",
stderr: true,
});
Ok(result.clone())
})
.unwrap();
let text = String::from_utf8(buffer.0.lock().unwrap().clone()).unwrap();
for phase in [
"checking dependencies",
"checking lockfile",
"installing dependencies",
"compiler output",
] {
assert!(text.contains(phase), "{text}");
}
assert!(text.contains("worker 0"));
assert!(text.contains("│ compiler output"));
assert!(!text.contains("| compiler output"));
assert!(text.contains(match result {
SyncResult::AlreadySynced => "Everything is already in order",
SyncResult::Synchronized => "Your bundle is ready",
}));
}
}

#[test]
fn reporter_retains_bundler_failure() {
let buffer = Buffer::default();
let reporter = Reporter::with_writer("sync", buffer.clone());
let result = report_sync(reporter, Completion::Tree, |handler| {
handler(SyncEvent::Installing);
Err(std::io::Error::other("compiler failed"))
});
assert!(result.unwrap_err().to_string().contains("compiler failed"));
let text = String::from_utf8(buffer.0.lock().unwrap().clone()).unwrap();
assert!(text.contains("compiler failed"));
assert!(text.contains("└─ × failed"));
assert!(!text.contains("Your bundle is ready"));
}
}
Loading
Loading