Skip to content

Commit fe20764

Browse files
committed
Drop a same-tier path under system gems
A Bundler config that records a path and, in the same file, sets disable_shared_gems to a false value sends Bundler to the system gem homes, so the recorded path (local or global) is not where it loads gems. Discovery still crawled it and apply could patch that unused copy; it now drops the path the same way path.system true does. bundler_sets_explicit_path also picks its deciding tier through BundlerPathTiers::deciding_of, the same resolver discovery uses, and the discovery comments now name both system-gem flags. Refs #1931 Assisted-by: Claude Code:claude-opus-5-5
1 parent ba9a49b commit fe20764

1 file changed

Lines changed: 71 additions & 14 deletions

File tree

‎crates/socket-patch-core/src/crawlers/ruby_crawler.rs‎

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,10 @@ impl RubyCrawler {
447447
let default_root = cwd.join("vendor").join("bundle");
448448
let default_root = normalize_lexically(&default_root).unwrap_or(default_root);
449449

450-
// A truthy `path.system` in the tier Bundler reads sends it to the
451-
// system gem home, which ignores every bundle path: the env
450+
// A truthy `path.system` or a falsy `disable_shared_gems` (#1931)
451+
// in the tier Bundler reads sends it to the system gem home
452+
// ([`BundlerPathSettings::uses_system_gems`]), which ignores every
453+
// bundle path: the env
452454
// `BUNDLE_PATH` it shadows (or that sits in the same tier) and the
453455
// default `vendor/bundle`. A leftover store there must not be
454456
// crawled, nor switch off the `gem env` homes where the loaded
@@ -533,8 +535,7 @@ impl RubyCrawler {
533535
roots.push(resolve_bundle_path(cwd, Path::new(&value), home));
534536
}
535537
}
536-
// With no explicit path and no truthy `path.system` in the
537-
// deciding tier, Bundler's `Path#base_path` is `<root>/.bundle`
538+
// With no explicit path and no system gems in the deciding tier, Bundler's `Path#base_path` is `<root>/.bundle`
538539
// whenever `use_system_gems?` is false: `default_install_uses_path`
539540
// on 2.x, `bundler_5_mode?` (`simulate_version 5`) on 4.x, and
540541
// the default from Bundler 5 on (#967). Probed without reading
@@ -1405,9 +1406,13 @@ impl BundlerPathTiers {
14051406
/// [`BundlerPathSettings::decides_path`] alone; `None` when no tier
14061407
/// does.
14071408
fn deciding(self) -> Option<(BundlerPathLevel, BundlerPathSettings)> {
1408-
self.settings()
1409-
.into_iter()
1410-
.find(|(_, tier)| tier.decides_path())
1409+
Self::deciding_of(self.settings())
1410+
}
1411+
1412+
fn deciding_of(
1413+
settings: Vec<(BundlerPathLevel, BundlerPathSettings)>,
1414+
) -> Option<(BundlerPathLevel, BundlerPathSettings)> {
1415+
settings.into_iter().find(|(_, tier)| tier.decides_path())
14111416
}
14121417
}
14131418

@@ -1430,14 +1435,17 @@ impl BundlerPathTiers {
14301435
/// read.
14311436
pub(crate) fn bundler_sets_explicit_path(tiers: BundlerPathTiers) -> bool {
14321437
let settings = tiers.settings();
1433-
if let Some((_, tier)) = settings.iter().find(|(_, tier)| tier.decides_path()) {
1434-
return !tier.uses_system_gems() && tier.path.as_deref().is_some_and(|p| !p.is_empty());
1435-
}
14361438
// `path = "vendor/bundle" if self[:deployment]`, after the tier loop.
1437-
settings
1439+
let deployment = settings
14381440
.iter()
14391441
.find_map(|(_, tier)| tier.deployment.as_deref())
1440-
.is_some_and(bundler_truthy)
1442+
.is_some_and(bundler_truthy);
1443+
match BundlerPathTiers::deciding_of(settings) {
1444+
Some((_, tier)) => {
1445+
!tier.uses_system_gems() && tier.path.as_deref().is_some_and(|p| !p.is_empty())
1446+
}
1447+
None => deployment,
1448+
}
14411449
}
14421450

14431451
/// Whether the installed gem dir `copy` (`<home>/gems/<name>-<version>`)
@@ -2393,7 +2401,8 @@ fn resolve_config_bundle_path(
23932401
/// `BUNDLE_PATH__SYSTEM:` must not match the path key — the prefix requires
23942402
/// the colon immediately after `BUNDLE_PATH`.
23952403
///
2396-
/// `BUNDLE_PATH__SYSTEM: "true"` (bundler's `path.system` setting) makes
2404+
/// `BUNDLE_PATH__SYSTEM: "true"` (bundler's `path.system` setting), or a
2405+
/// falsy `BUNDLE_DISABLE_SHARED_GEMS` (#1931), makes
23972406
/// bundler IGNORE any recorded path and use the system/default gem home, so
23982407
/// the whole config entry parses as unset — the caller then falls through
23992408
/// to the `gem env` homes, which is exactly where those gems live. The flag
@@ -2412,6 +2421,7 @@ fn parse_legacy_bundle_config_path(contents: &str) -> Option<String> {
24122421
fn parse_bundle_config_path_with(contents: &str, unquote: fn(&str) -> &str) -> Option<String> {
24132422
let mut path: Option<String> = None;
24142423
let mut path_system = false;
2424+
let mut shared_gems = false;
24152425
for line in contents.lines() {
24162426
if let Some(rest) = line.strip_prefix("BUNDLE_PATH:") {
24172427
let v = unquote(rest);
@@ -2420,9 +2430,12 @@ fn parse_bundle_config_path_with(contents: &str, unquote: fn(&str) -> &str) -> O
24202430
}
24212431
} else if let Some(rest) = line.strip_prefix("BUNDLE_PATH__SYSTEM:") {
24222432
path_system = bundler_truthy(unquote(rest));
2433+
} else if let Some(rest) = line.strip_prefix("BUNDLE_DISABLE_SHARED_GEMS:") {
2434+
shared_gems = !bundler_truthy(unquote(rest));
24232435
}
24242436
}
2425-
if path_system {
2437+
// The same system-home rule as [`BundlerPathSettings::uses_system_gems`].
2438+
if path_system || shared_gems {
24262439
None
24272440
} else {
24282441
path
@@ -5280,6 +5293,50 @@ mod tests {
52805293
}
52815294
}
52825295

5296+
/// #1931: a path recorded in the same tier as a falsy
5297+
/// `disable_shared_gems` is ignored too (`Path#path` takes the system
5298+
/// home whenever `system_path` is set), local and global.
5299+
#[tokio::test]
5300+
async fn falsy_disable_shared_gems_drops_same_tier_path() {
5301+
let setting = "---\nBUNDLE_PATH: \"vendor/x\"\nBUNDLE_DISABLE_SHARED_GEMS: \"false\"\n";
5302+
let stage = |root: PathBuf| async move {
5303+
let store = root.join("vendor/x/ruby/3.3.0/gems");
5304+
tokio::fs::create_dir_all(store.join("colorize-0.8.1"))
5305+
.await
5306+
.unwrap();
5307+
tokio::fs::write(root.join("Gemfile"), b"gem \"colorize\"\n")
5308+
.await
5309+
.unwrap();
5310+
};
5311+
5312+
let dir = tempfile::tempdir().unwrap();
5313+
stage(dir.path().to_path_buf()).await;
5314+
tokio::fs::create_dir_all(dir.path().join(".bundle"))
5315+
.await
5316+
.unwrap();
5317+
tokio::fs::write(dir.path().join(".bundle/config"), setting)
5318+
.await
5319+
.unwrap();
5320+
let discovery =
5321+
RubyCrawler::discover_bundle_stores_with_env(dir.path(), None, None, None, None).await;
5322+
assert!(discovery.stores.is_empty(), "local: {:?}", discovery.stores);
5323+
assert_eq!(discovery.skipped_config_path, None);
5324+
5325+
let dir = tempfile::tempdir().unwrap();
5326+
let root = dir.path().join("proj");
5327+
stage(root.clone()).await;
5328+
let global = dir.path().join("global-config");
5329+
tokio::fs::write(&global, setting).await.unwrap();
5330+
let discovery =
5331+
RubyCrawler::discover_bundle_stores_with_env(&root, None, None, None, Some(&global))
5332+
.await;
5333+
assert!(
5334+
discovery.stores.is_empty(),
5335+
"global: {:?}",
5336+
discovery.stores
5337+
);
5338+
}
5339+
52835340
/// #1931 controls: a truthy `disable_shared_gems` keeps Bundler off the
52845341
/// system gems, so the leftover store stays; a falsy flag in a tier a
52855342
/// higher tier shadows does not count.

0 commit comments

Comments
 (0)