Code Monkey home page Code Monkey logo

bundler-inject's Introduction

bundler-inject

Gem Version CI Code Climate Test Coverage

bundler-inject is a bundler plugin that allows a developer to extend a project with their own personal gems and/or override existing gems, without having to modify the Gemfile, thus avoiding accidental modification of git history.

Installation

Add these lines to your application's Gemfile:

plugin 'bundler-inject'
require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil

Additionally, commit a bundler.d/.gitkeep file, and add /bundler.d to your .gitignore file. This will be one of the locations developers can place their overrides.

Usage

Once the above lines are in the Gemfile, subsequent bundle commands will attempt to evaluate extra gemfiles from two locations if they are present.

  • global - ~/.bundler.d/*.rb
  • project - $PROJECT_DIR/bundler.d/*.rb

For example, a developer may prefer the pry gem over irb, but can't use it because the project Gemfile doesn't have pry in it. Instead of modifying the Gemfile and hoping they don't commit the changes, they can create a bundler.d/developer.rb file with the contents set to gem "pry". From then on the developer can use pry, even though the project didn't state it explicitly.

Since this example developer likely always wants to use pry, it would be preferable to specify this as a global choice in a ~/.bundler.d/developer.rb file instead.

override_gem

override_gem is an extra DSL command that allows overriding existing gems from the Gemfile. A useful example of this is if you are making a change to a dependent gem, and temporarily want to override the existing gem definition with, a git or path reference.

For example, there is a project with a Gemfile with gem "foo" in it. We want to fix a bug in foo, and we intend to make a pull request to upstream, but in the interim we want to test our foo change with the project. Instead of modifying the Gemfile and hoping we don't commit the changes, we can create a bundler.d/developer.rb with the contents set to override_gem "foo", :git => "https://github.com/me/foo.git". bundler-inject will output a warning to the screen to make us aware we are overriding, and then it will use the new definition.

override_gem will raise an exception if the specified gem does not exist in the original Gemfile.

ensure_gem

ensure_gem is an extra DSL command similar to override_gem, and primarily meant for the global override file.

One issue with the global file is that it specifies a new gem with gem, but that gem already exists in the project you will get a nasty warning. Conversely, if it specifies an override with override_gem, but the gem does not exist in the project you will get an exception. To deal with these issues, you can use ensure_gem in your global file.

ensure_gem works by checking if the gem is already in the dependency list, and comparing the options specified. If the dependency does not exist, it uses gem, otherwise if the options or version specified are significantly different, it will use override_gem, otherwise it will just do nothing, deferring to the original declaration.

Configuration

Disabling warnings

To disable warnings that are output to the console when override_gem or ensure_gem is in use, you can update a bundler setting:

$ bundle config bundler_inject.disable_warn_override_gem true

or use an environment variable:

$ export BUNDLE_BUNDLER_INJECT__DISABLE_WARN_OVERRIDE_GEM=true

There is a fallback for those that will check the RAILS_ENV environment variable, and will disable the warning when in "production".

Specifying gem source directories

Many developers checkout gems into a single directory for enhancement. Instead of specifying the full path of gems every time, specify a gem path to locate these directories. This can be defined with a bundler setting:

$ bundle config bundler_inject.gem_path ~/src:~/gem_src

or use an environment variable:

$ export BUNDLE_BUNDLER_INJECT__GEM_PATH=~/src:~/gem_src

An override will find a gem in either of these two directories or in the directory where the Gemfile override is located.

# located in ~/src/ansi
override_gem "ansi"
# located in $PWD/mime_override
override_gem "mime/type", path: "mime_override"

What is this sorcery?

While this is technically a bundler plugin, bundler-inject does not use the expected plugin hooks/sources/commands. To understand how this works and why these were not used, it's useful to understand how bundler passes over your Gemfile.

For bundle install/bundle update, bundler makes two passes over the Gemfile.

On the first pass, bundler executes your Gemfile in a Bundler::Plugin::DSL context. This class is a special subclass of Bundler::Dsl, where nearly all of the usual DSL methods like gem and gemspec are a no-op, and the plugin DSL method does it's thing. So, after the first pass, bundler sees only your plugin gems, and will then install them into .bundle/plugin.

On the second pass, bundler executes your Gemfile in a Bundler::Dsl context, where the usual DSL methods like gem and gemspec do their thing, and the plugin DSL method is instead a no-op. This is the pass most people think of.

On the very first install of the plugin, between the two passes, bundler will load your plugin to see what kind of features it has, whether hooks, sources, or commands, and will store this information in the .bundle/plugin/index file. It would seem that since the plugin code is loaded this would be an opportune time to do what we need, but unfortunately this only happens on initial install. On subsequent bundle update calls, bundler sees that the index file exists, and doesn't need to load the plugin, since all of the information is in the index.

Complicating the matter further, for bundle check/bundle exec, bundler makes only one pass over the Gemfile in a Bundler::Dsl context.

One immediate question on your mind may be, why not just define a fake hook or source, and then put the code in there. Unfortunately, the problem is that any of hooks, sources, or commands that will trigger the load of the plugin occur after the Gemfile has already been passed over, preventing us from adding to the DSL, as well as loading extra gemfiles.

As such, the earliest we can possibly trigger our plugin load in all cases is immediately after we declare the plugin in the Gemfile. This is why we need that magic line after the plugin line in the Gemfile.

Once we have the ability to trigger a load of our plugin, whether direct via the magic line, or automatically on first plugin installation, then the plugin can manipulate the Bundler::Dsl to add the override_gem method and trigger the eval of extra gemfiles.

Development

Development of bundler plugins can be a little strange, with a few gotchas.

  1. bundler installs your gem into the .bundle/plugin directory of the target project.
  2. plugin "bundler-inject", :path => "/path/to/bundler-inject" doesn't work as expected since bundler needs to "install" your gem into .bundle/plugin, and thus doesn't know how. To get around this, use :git => File.expand_path("/path/to/bundler-inject").
  3. If you are using :git (which also applies to :path with the workaround above), bundler will only consider committed code. Therefore, you must commit your code in a temporary commit if you want it to be picked up.
  4. bundler plugins are copied to .bundle/plugin only on first install, and then updated only if a change is detected. Unless you have a :ref or a changing version number, bundler will think your gem hasn't changed and will not update it, even if you commit something. To force bundler to pull in your changes, you will have to rm -rf .bundle/plugin.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/ManageIQ/bundler-inject.

License

This project is available as open source under the terms of the Apache License 2.0.

bundler-inject's People

Contributors

agrare avatar bdunne avatar chessbyte avatar durandom avatar fryguy avatar himdel avatar jhernand avatar jrafanie avatar kbrock avatar mend-bolt-for-github[bot] avatar nicklamuro avatar renovate[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bundler-inject's Issues

Test against multiple bundler versions in CI

There are two things that need to happen for this to be resolved:

  • Actually write some meaningful specs ( #4 )
  • Update the .travis.yml install different versions of bundler ( #4 )

The first is obvious, though, I think what we want to be testing as part of this is actually running bundle install against a test Gemfile with override_gem being used, and confirm things are working as expected (subsequent after subsequent bundle updates and executing some code as well. So very integration style tests that probably make use of a spec/support/dummy/ directory to handle that.

Granted, this code is pretty battle tested, but it would be nice knowing that it works for bundler versions we aren't using regularly... which leads to the next point...

Testing multiple versions on travis should be pretty easy, and we probably could change the .travis.yml to do something like this:

---
sudo: false
language: ruby
cache: bundler
rvm:
  - 2.4.5
before_install: gem install bundler -v $BUNDLER_VERSION
env:
  BUNDLER_VERSION=1.15.4
  BUNDLER_VERSION=1.16.1
  BUNDLER_VERSION=1.17.x
  BUNDLER_VERSION=2.0.0

Or something similar.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

This repository currently has no open or pending branches.

Detected dependencies

bundler
Gemfile
github-actions
.github/workflows/ci.yaml
  • actions/checkout v4
  • ruby/setup-ruby v1
  • paambaati/codeclimate-action v6

  • Check this box to trigger a request for Renovate to run again on this repository

Bundler fails on production with the plugin added

Speaking to @Fryguy he thinks this is a bundler issue, relating to that the plugin is not in the lock file.

Here is the output from my Heroku deploy

-----> Ruby app detected
-----> Installing bundler 1.17.3
-----> Removing BUNDLED WITH version in the Gemfile.lock
-----> Compiling Ruby/Rails
-----> Using Ruby version: ruby-2.6.5
-----> Installing dependencies using bundler 1.17.3
       Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin -j4 --deployment
       The dependency bundler-inject (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for  but the dependency is only for . To add those platforms to the bundle, run `bundle lock --add-platform `.
       Unable to find a spec satisfying bundler-inject (>= 0) in the set. Perhaps the
       lockfile is corrupted?
       Bundler Output: The dependency bundler-inject (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for  but the dependency is only for . To add those platforms to the bundle, run `bundle lock --add-platform `.
       Unable to find a spec satisfying bundler-inject (>= 0) in the set. Perhaps the
       lockfile is corrupted?
 !
 !     Failed to install gems via Bundler.
 !
 !     Push rejected, failed to compile Ruby app.
 !     Push failed

Moving plugin 'bundler-inject' to group :development do didn't fix this either as I get a NoMethodError: undefined method 'full_gem_path' for nil:NilClass as seen in this bundler issue rubygems/rubygems#3319 (comment)

override_gem doesn't override a transient dependency

In ManageIQ, if I do

override_gem "azure-armrest", :path => "~/dev/azure-armrest"

I get the following error

Installing bundler-inject 1.1.0

[!] There was an error parsing `Gemfile.local.rb`: Trying to override unknown gem "azure-armrest". Bundler cannot continue.

 #  from /Users/jfrey/dev/manageiq/bundler.d/Gemfile.local.rb:7
 #  -------------------------------------------
 #  #
 >  override_gem "azure-armrest", :path => "/Users/jfrey/dev/manageiq-cross_repo-tests/repos/ManageIQ/azure-armrest@a28277d722984416a782f940da907e7a5cb7f036"
 #  -------------------------------------------

Even though it's a transient dependency, I'd expect it to work.

Use `Bundler::Settings` for `warn_override_gem`

Using ENV["RAILS_ENVIRONMENT"] == "production" is too Rails™ specific for this project, and kind of a hidden feature.

return if ENV["RAILS_ENV"] == "production"

It would be much better to hook into the Bundler::Settings, which is already something available and should be generic enough to work with our plugin:

return if Bundler.settings["bundler-inject.disable_warn_override_gem_output"]

Then you can run:

$ bundle config bundler-inject.disable_warn_override_gem_output true
$ irb
irb(main):001:0> require 'bundler/setup'
=> true
irb(main):002:0> Bundler.settings["bundler-inject.disable_warn_override_gem_output"]
=> "true"
irb(main):003:0>

And make use of the disabling setting that way. Can be an environment variable as well (though, we might want to consider it without the - since that isn't a valid ENV var variable in most shells...):

$ bundle config --delete bundler-inject.disable_warn_override_gem_output
$ irb
irb(main):001:0> require 'bundler/setup'
=> true
irb(main):002:0> Bundler.settings["bundler-inject.disable_warn_override_gem_output"]
=> nil
irb(main):003:0> exit
$ env "BUNDLE_BUNDLER-INJECT__DISABLE_WARN_OVERRIDE_GEM_OUTPUT=true" irb
irb(main):001:0> require 'bundler/setup'
=> true
irb(main):002:0> Bundler.settings["bundler-inject.disable_warn_override_gem_output"]
=> "true"
irb(main):003:0>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.