diff --git a/README.md b/README.md index 464f9165..37a0cd47 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,90 @@ When you prefix a command with `appraisal`, the command is run with the appropriate Gemfile for that appraisal, ensuring the correct dependencies are used. +Sharing Modular Gemfiles between Appraisals +------- + +_New for version 3.0_ + +It is common for Appraisals to duplicate sets of gems, and sometimes it +makes sense to DRY this up into a shared, modular, gemfile. +In a scenario where you do not load your main Gemfile in your Appraisals, +but you want to declare your various gem sets for e.g. +`%w(coverage test documentation audit)` once each, you can re-use the same +modular gemfiles for local development by referencing them from the main +Gemfile. + +To do this, use the `eval_gemfile` declaration within the necessary +`appraise` block in your `Appraisals` file, which will behave the same as +`eval_gemfile` does in a normal Gemfile. + +### Example Usage + +You could put your modular gemfiles in the `gemfiles` directory, or nest +them in `gemfiles/modular/*`, which will be used for this example. + +**Gemfile** +```ruby +eval_gemfile "gemfiles/modular/audit.gemfile" +``` + +**gemfiles/modular/audit.gemfile** +```ruby +# Many gems are dropping support for Ruby < 3.1, +# so we only want to run our security audit in CI on Ruby 3.1+ +gem "bundler-audit", "~> 0.9.2" +# And other security audit gems... +``` + +**Appraisals** +```ruby +appraise 'ruby-2-7' do + gem "dummy" +end + +appraise 'ruby-3-0' do + gem "dummy" +end + +appraise 'ruby-3-1' do + gem "dummy" + eval_gemfile "modular/audit.gemfile" +end + +appraise 'ruby-3-2' do + gem "dummy" + eval_gemfile "modular/audit.gemfile" +end + +appraise 'ruby-3-3' do + gem "dummy" + eval_gemfile "modular/audit.gemfile" +end + +appraise 'ruby-3-4' do + gem "dummy" + eval_gemfile "modular/audit.gemfile" +end +``` + +**Appraisal.root.gemfile** +```ruby +source "https://rubygems.org" + +# Appraisal Root Gemfile is for running appraisal to generate the Appraisal Gemfiles +# We do not load the standard Gemfile, as it is tailored for local development, +# while appraisals are tailored for CI. + +gemspec + +gem "appraisal" +``` + +Now when you need to update your appraisals: +```shell +BUNDLE_GEMFILE=Appraisal.root.gemfile bundle exec appraisal update +``` + Removing Gems using Appraisal ------- diff --git a/lib/appraisal/appraisal.rb b/lib/appraisal/appraisal.rb index 86f2c3eb..6c4118a9 100644 --- a/lib/appraisal/appraisal.rb +++ b/lib/appraisal/appraisal.rb @@ -19,6 +19,10 @@ def initialize(name, source_gemfile) @gemfile = source_gemfile.dup end + def eval_gemfile(*args) + gemfile.eval_gemfile(*args) + end + def gem(*args) gemfile.gem(*args) end diff --git a/lib/appraisal/bundler_dsl.rb b/lib/appraisal/bundler_dsl.rb index e102f4cd..f350235a 100644 --- a/lib/appraisal/bundler_dsl.rb +++ b/lib/appraisal/bundler_dsl.rb @@ -7,7 +7,7 @@ class BundlerDSL attr_reader :dependencies PARTS = %w[source ruby_version gits paths dependencies groups - platforms source_blocks install_if gemspec] + platforms source_blocks install_if gemspec eval_gemfile] def initialize @sources = [] @@ -21,12 +21,17 @@ def initialize @source_blocks = {} @git_sources = {} @install_if = {} + @eval_gemfile = [] end def run(&block) instance_exec(&block) end + def eval_gemfile(path, contents = nil) + @eval_gemfile << [path, contents] + end + def gem(name, *requirements) @dependencies.add(name, substitute_git_source(requirements)) end @@ -103,6 +108,12 @@ def git_source(source, &block) private + def eval_gemfile_entry + @eval_gemfile.map { |(p, c)| "eval_gemfile(#{p.inspect}#{", #{c.inspect}" if c})" } * "\n\n" + end + + alias_method :eval_gemfile_entry_for_dup, :eval_gemfile_entry + def source_entry @sources.uniq.map { |source| "source #{source.inspect}" }.join("\n") end diff --git a/spec/acceptance/eval_gemfile_spec.rb b/spec/acceptance/eval_gemfile_spec.rb new file mode 100644 index 00000000..152e8a00 --- /dev/null +++ b/spec/acceptance/eval_gemfile_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "eval_gemfile" do + before do + build_appraisal_file + build_modular_gemfile + build_rakefile + build_gemspec + end + + it "supports eval_gemfile syntax" do + write_file "Gemfile", <<-GEMFILE + source "https://rubygems.org" + + gem 'appraisal', :path => #{PROJECT_ROOT.inspect} + + gemspec + GEMFILE + + run "bundle install --local" + run "appraisal install" + output = run "appraisal rake version" + + expect(output).to include "Loaded 1.1.0" + end + + def build_modular_gemfile + Dir.mkdir("tmp/stage/gemfiles") rescue nil + + write_file File.join("gemfiles", "im_with_dummy"), <<-GEMFILE + # No source needed because this is a modular gemfile intended to be loaded into another gemfile, + # which will define source. + gem 'dummy' + GEMFILE + end + + def build_appraisal_file + super <<-APPRAISALS + appraise 'stock' do + gem 'rake' + eval_gemfile "im_with_dummy" + end + APPRAISALS + end + + def build_rakefile + write_file "Rakefile", <<-RAKEFILE + require 'rubygems' + require 'bundler/setup' + require 'appraisal' + + task :version do + require 'dummy' + puts "Loaded \#{$dummy_version}" + end + RAKEFILE + end + + def build_gemspec(path = ".") + Dir.mkdir("tmp/stage/#{path}") rescue nil + + write_file File.join(path, "gemspec_project.gemspec"), <<-GEMSPEC + Gem::Specification.new do |s| + s.name = 'gemspec_project' + s.version = '0.1' + s.summary = 'Awesome Gem!' + s.authors = "Appraisal" + end + GEMSPEC + end +end