Skip to content

Commit 11b9fe3

Browse files
committed
Lock bundle after check to cleanup removals also.
1 parent b1e4b46 commit 11b9fe3

File tree

3 files changed

+162
-1
lines changed

3 files changed

+162
-1
lines changed

crates/rb-core/src/bundler/mod.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ impl BundlerRuntime {
187187
}
188188

189189
/// Check if bundler environment is synchronized (dependencies satisfied)
190+
/// Also updates Gemfile.lock if check passes to handle removed gems
190191
pub fn check_sync(
191192
&self,
192193
butler_runtime: &crate::butler::ButlerRuntime,
@@ -209,6 +210,13 @@ impl BundlerRuntime {
209210
is_synced,
210211
output.status.code().unwrap_or(-1)
211212
);
213+
214+
// If check passes, update lockfile to handle removed gems
215+
if is_synced {
216+
debug!("Bundle check passed, updating lockfile to match Gemfile");
217+
self.update_lockfile_quietly(butler_runtime)?;
218+
}
219+
212220
Ok(is_synced)
213221
}
214222
Err(e) => {
@@ -344,21 +352,103 @@ impl BundlerRuntime {
344352
}
345353
}
346354

355+
/// Update Gemfile.lock to match Gemfile quietly (no output)
356+
/// Used by check_sync to ensure lockfile is up to date
357+
fn update_lockfile_quietly(
358+
&self,
359+
butler_runtime: &crate::butler::ButlerRuntime,
360+
) -> std::io::Result<()> {
361+
debug!("Quietly updating Gemfile.lock to match Gemfile");
362+
363+
// Run bundle lock --local to regenerate lockfile based on Gemfile
364+
// Uses --local to avoid network access since bundle check already passed
365+
let output = Command::new("bundle")
366+
.arg("lock")
367+
.arg("--local")
368+
.current_dir(&self.root)
369+
.output_with_context(butler_runtime)?;
370+
371+
if output.status.success() {
372+
debug!("Gemfile.lock updated successfully");
373+
Ok(())
374+
} else {
375+
// Silently ignore errors - lockfile update is best-effort
376+
// The bundle check already passed, so environment is functional
377+
debug!(
378+
"Bundle lock failed but continuing (exit code: {})",
379+
output.status.code().unwrap_or(-1)
380+
);
381+
Ok(())
382+
}
383+
}
384+
385+
/// Update Gemfile.lock to match Gemfile (handles removed gems)
386+
/// Used by sync command with output streaming
387+
fn update_lockfile<F>(
388+
&self,
389+
butler_runtime: &crate::butler::ButlerRuntime,
390+
output_handler: &mut F,
391+
) -> std::io::Result<()>
392+
where
393+
F: FnMut(&str),
394+
{
395+
debug!("Updating Gemfile.lock to match Gemfile");
396+
397+
// Run bundle lock --local to regenerate lockfile based on Gemfile
398+
// Uses --local to avoid network access since bundle check already passed
399+
let output = Command::new("bundle")
400+
.arg("lock")
401+
.arg("--local")
402+
.current_dir(&self.root)
403+
.output_with_context(butler_runtime)?;
404+
405+
// Stream output to handler
406+
if !output.stdout.is_empty() {
407+
let stdout_str = String::from_utf8_lossy(&output.stdout);
408+
for line in stdout_str.lines() {
409+
output_handler(line);
410+
}
411+
}
412+
413+
if !output.stderr.is_empty() {
414+
let stderr_str = String::from_utf8_lossy(&output.stderr);
415+
for line in stderr_str.lines() {
416+
eprintln!("{}", line);
417+
}
418+
}
419+
420+
if output.status.success() {
421+
debug!("Gemfile.lock updated successfully");
422+
Ok(())
423+
} else {
424+
Err(std::io::Error::other(format!(
425+
"Bundle lock failed (exit code: {})",
426+
output.status.code().unwrap_or(-1)
427+
)))
428+
}
429+
}
430+
347431
/// Synchronize the bundler environment (configure path, check, and install if needed)
348432
pub fn synchronize<F>(
349433
&self,
350434
butler_runtime: &crate::butler::ButlerRuntime,
351-
output_handler: F,
435+
mut output_handler: F,
352436
) -> std::io::Result<SyncResult>
353437
where
354438
F: FnMut(&str),
355439
{
356440
debug!("Starting bundler synchronization");
357441

358442
// Step 1: Check if already synchronized
443+
// Note: check_sync already updates lockfile quietly, but for sync command
444+
// we want to show output, so we call update_lockfile explicitly
359445
match self.check_sync(butler_runtime)? {
360446
true => {
361447
debug!("Bundler environment already synchronized");
448+
449+
// For sync command, show the lockfile update output
450+
self.update_lockfile(butler_runtime, &mut output_handler)?;
451+
362452
Ok(SyncResult::AlreadySynced)
363453
}
364454
false => {

spec/commands/exec/bundler_spec.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,43 @@ Describe "Ruby Butler Exec Command - Bundler Environment"
230230
End
231231
End
232232

233+
Context "lockfile update on exec"
234+
BeforeEach 'setup_test_project'
235+
AfterEach 'cleanup_test_project'
236+
237+
It "updates Gemfile.lock when gem is removed before exec"
238+
# Create Gemfile with TWO gems
239+
cat > Gemfile << 'EOF'
240+
source 'https://rubygems.org'
241+
gem 'rake'
242+
gem 'minitest'
243+
EOF
244+
245+
# Initial sync to install both gems
246+
rb -R $RUBIES_DIR sync >/dev/null 2>&1
247+
248+
# Verify both gems are in Gemfile.lock
249+
grep -q "rake" Gemfile.lock || fail "rake should be in initial Gemfile.lock"
250+
grep -q "minitest" Gemfile.lock || fail "minitest should be in initial Gemfile.lock"
251+
252+
# Remove minitest from Gemfile
253+
cat > Gemfile << 'EOF'
254+
source 'https://rubygems.org'
255+
gem 'rake'
256+
EOF
257+
258+
# Execute a ruby command - this should trigger lockfile update via check_sync
259+
When run rb -R $RUBIES_DIR exec ruby -e "puts 'test'"
260+
The status should equal 0
261+
The output should include "test"
262+
263+
# Verify lockfile was updated: rake remains, minitest removed
264+
The path Gemfile.lock should be exist
265+
The contents of file Gemfile.lock should include "rake"
266+
The contents of file Gemfile.lock should not include "minitest"
267+
End
268+
End
269+
233270
Context "bundler error handling"
234271
BeforeEach 'setup_test_project'
235272
# Empty directory without Gemfile for error testing

spec/commands/sync_spec.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,38 @@ Describe 'rb sync command'
6767
The output should include "Synchronizing"
6868
End
6969
End
70+
71+
Context 'when gem is removed from Gemfile'
72+
It 'updates Gemfile.lock to reflect removed gem'
73+
# Create Gemfile with TWO gems
74+
cat > Gemfile << 'EOF'
75+
source 'https://rubygems.org'
76+
gem 'rake'
77+
gem 'minitest'
78+
EOF
79+
80+
# Initial sync - install both gems
81+
rb -R $RUBIES_DIR sync >/dev/null 2>&1
82+
83+
# Verify both gems are in Gemfile.lock
84+
grep -q "rake" Gemfile.lock || fail "rake should be in initial Gemfile.lock"
85+
grep -q "minitest" Gemfile.lock || fail "minitest should be in initial Gemfile.lock"
86+
87+
# Remove minitest from Gemfile
88+
cat > Gemfile << 'EOF'
89+
source 'https://rubygems.org'
90+
gem 'rake'
91+
EOF
92+
93+
# Run sync again
94+
When run rb -R $RUBIES_DIR sync
95+
The status should be success
96+
The output should include "Synchronizing"
97+
98+
# Verify rake is still in lockfile but minitest is removed
99+
The path Gemfile.lock should be exist
100+
The contents of file Gemfile.lock should include "rake"
101+
The contents of file Gemfile.lock should not include "minitest"
102+
End
103+
End
70104
End

0 commit comments

Comments
 (0)