From aad374a88a5d7bd8a5d5a458294a9a411b323016 Mon Sep 17 00:00:00 2001 From: The Bundler Bot Date: Mon, 18 Sep 2017 04:26:44 +0000 Subject: [PATCH] Auto merge of #6024 - gwerbin:custom-user-bundle-dir, r=colby-swandale Allow user to override ~/.bundle with environment variables ### What was the end-user problem that led to this PR? As in #4333, users wanted a way to make Bundler respect the XDG specification. ### What was your diagnosis of the problem? The directory `~/.bundle` was hard-coded and could not be configured. ### What is your fix for the problem, implemented in this PR? Allow users to configure `~/.bundle` and its relevant sub-directories by setting environment variables. The environment variables and their fallbacks are as follows: | variable | fallback if unset | |---|---| | `BUNDLE_USER_HOME` | `$HOME/.bundle` | | `BUNDLE_USER_CACHE` | `$BUNDLE_USER_HOME/cache` | | `BUNDLE_USER_CONFIG` | `$BUNDLE_USER_HOME/config` | | `BUNDLE_USER_PLUGIN` | `$BUNDLE_USER_HOME/plugin` | ### Why did you choose this fix out of the possible options? Unlike https://github.com/bundler/bundler/pull/5787, This solution is not specific to the XDG specification. Users have all kinds of setups, and this is a very general system for allowing them to configure their development machines however they need. It tries to keep all files created by Bundler in the same place (as per https://github.com/bundler/bundler/pull/5787#issuecomment-310154273), but allows the user to override that convention _if they really want to and they know what they are doing_. If they want to use XDG for everything, they can do it by explicitly setting the `BUNDLE_USER_*` variables to the equivalent `XDG_DATA_*`. If they just want to get `.bundle` out of their home directory, they can do it by setting `BUNDLE_USER_HOME` and don't have to mess with the more specific env variables if they don't want to. To me, this solution strikes the right balance between "fine-grained control for power users" and "simple, sane defaults for everyone else". Please let me know if my tests can be improved. My only Ruby experience so far has been writing Homebrew formulas and configuring Jekyll, so I'm sure I have a lot to learn. (cherry picked from commit e6cc7a24d5ca9da842aeefd23167be4d850c9aa5) --- lib/bundler.rb | 26 ++++++++++++---- lib/bundler/plugin.rb | 6 ++-- lib/bundler/settings.rb | 2 +- spec/bundler/bundler_spec.rb | 58 ++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 10 deletions(-) diff --git a/lib/bundler.rb b/lib/bundler.rb index 829dc5d9496..f44b7b6d786 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -195,8 +195,26 @@ def tmp_home_path(login, warning) raise e.exception("#{warning}\nBundler also failed to create a temporary home directory at `#{path}':\n#{e}") end - def user_bundle_path - Pathname.new(user_home).join(".bundle") + def user_bundle_path(dir = "home") + env_var, fallback = case dir + when "home" + ["BUNDLE_USER_HOME", Pathname.new(user_home).join(".bundle")] + when "cache" + ["BUNDLE_USER_CACHE", user_bundle_path.join("cache")] + when "config" + ["BUNDLE_USER_CONFIG", user_bundle_path.join("config")] + when "plugin" + ["BUNDLE_USER_PLUGIN", user_bundle_path.join("plugin")] + else + raise BundlerError, "Unknown user path requested: #{dir}" + end + # `fallback` will already be a Pathname, but Pathname.new() is + # idempotent so it's OK + Pathname.new(ENV.fetch(env_var, fallback)) + end + + def user_cache + user_bundle_path("cache") end def home @@ -211,10 +229,6 @@ def specs_path bundle_path.join("specifications") end - def user_cache - user_bundle_path.join("cache") - end - def root @root ||= begin SharedHelpers.root diff --git a/lib/bundler/plugin.rb b/lib/bundler/plugin.rb index 0aa13ab7849..53f9806b73d 100644 --- a/lib/bundler/plugin.rb +++ b/lib/bundler/plugin.rb @@ -81,8 +81,8 @@ def index # The directory root for all plugin related data # - # Points to root in app_config_path if ran in an app else points to the one - # in user_bundle_path + # If run in an app, points to local root, in app_config_path + # Otherwise, points to global root, in Bundler.user_bundle_path("plugin") def root @root ||= if SharedHelpers.in_bundle? local_root @@ -97,7 +97,7 @@ def local_root # The global directory root for all plugin related data def global_root - Bundler.user_bundle_path.join("plugin") + Bundler.user_bundle_path("plugin") end # The cache directory for plugin stuffs diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index 611c5a4ef9d..fe68d510ffa 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -395,7 +395,7 @@ def global_config_file Pathname.new(ENV["BUNDLE_CONFIG"]) else begin - Bundler.user_bundle_path.join("config") + Bundler.user_bundle_path("config") rescue PermissionError, GenericSystemCallError nil end diff --git a/spec/bundler/bundler_spec.rb b/spec/bundler/bundler_spec.rb index 40dc0948854..2b9051a9747 100644 --- a/spec/bundler/bundler_spec.rb +++ b/spec/bundler/bundler_spec.rb @@ -306,4 +306,62 @@ expect(Bundler.tmp_home_path("USER", "")).to eq(Pathname("/TMP/bundler/home/USER")) end end + + context "user cache dir" do + let(:home_path) { Pathname.new(ENV["HOME"]) } + + let(:xdg_data_home) { home_path.join(".local") } + let(:xdg_cache_home) { home_path.join(".cache") } + let(:xdg_config_home) { home_path.join(".config") } + + let(:bundle_user_home_default) { home_path.join(".bundle") } + let(:bundle_user_home_custom) { xdg_data_home.join("bundle") } + + let(:bundle_user_cache_default) { bundle_user_home_default.join("cache") } + let(:bundle_user_cache_custom) { xdg_cache_home.join("bundle") } + + let(:bundle_user_config_default) { bundle_user_home_default.join("config") } + let(:bundle_user_config_custom) { xdg_config_home.join("bundle") } + + let(:bundle_user_plugin_default) { bundle_user_home_default.join("plugin") } + let(:bundle_user_plugin_custom) { xdg_data_home.join("bundle").join("plugin") } + + describe "#user_bundle_path" do + before do + allow(Bundler.rubygems).to receive(:user_home).and_return(home_path) + end + + it "should use the default home path" do + expect(Bundler.user_bundle_path).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_default) + expect(Bundler.user_cache).to eq(bundle_user_cache_default) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_default) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_default) + end + + it "should use custom home path as root for other paths" do + ENV["BUNDLE_USER_HOME"] = bundle_user_home_custom.to_s + expect(Bundler.user_bundle_path).to eq(bundle_user_home_custom) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_custom) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_home_custom.join("cache")) + expect(Bundler.user_cache).to eq(bundle_user_home_custom.join("cache")) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_home_custom.join("config")) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_home_custom.join("plugin")) + end + + it "should use all custom paths, except home" do + ENV.delete("BUNDLE_USER_HOME") + ENV["BUNDLE_USER_CACHE"] = bundle_user_cache_custom.to_s + ENV["BUNDLE_USER_CONFIG"] = bundle_user_config_custom.to_s + ENV["BUNDLE_USER_PLUGIN"] = bundle_user_plugin_custom.to_s + expect(Bundler.user_bundle_path).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default) + expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_custom) + expect(Bundler.user_cache).to eq(bundle_user_cache_custom) + expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_custom) + expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_custom) + end + end + end end