@@ -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.
14311436pub ( 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> {
24122421fn 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 = "---\n BUNDLE_PATH: \" vendor/x\" \n BUNDLE_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