Skip to content
This repository has been archived by the owner on Apr 14, 2021. It is now read-only.

Commit

Permalink
Merge pull request #3531 from jhass/optional_groups
Browse files Browse the repository at this point in the history
 Implement optional groups
  • Loading branch information
indirect committed Apr 7, 2015
2 parents 471e139 + 8f987f2 commit 13f44d1
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 23 deletions.
2 changes: 2 additions & 0 deletions lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ def check
Bundler.rubygems.security_policy_keys.join('|')
method_option "without", :type => :array, :banner =>
"Exclude gems that are part of the specified named group."
method_option "with", :type => :array, :banner =>
"Include gems that are part of the specified named group."

def install
require 'bundler/cli/install'
Expand Down
30 changes: 28 additions & 2 deletions lib/bundler/cli/install.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,35 @@ def run

warn_if_root

if options[:without]
options[:without] = options[:without].map{|g| g.tr(' ', ':') }
[:with, :without].each do |option|
if options[option]
options[option] = options[option].join(":").tr(" ", ":").split(":")
end
end

if options[:without] && options[:with]
conflicting_groups = options[:without] & options[:with]
unless conflicting_groups.empty?
Bundler.ui.error "You can't list a group in both, --with and --without." \
"The offending groups are: #{conflicting_groups.join(", ")}."
exit 1
end
end

Bundler.settings.with = [] if options[:with] && options[:with].empty?
Bundler.settings.without = [] if options[:without] && options[:without].empty?

with = options.fetch("with", [])
with |= Bundler.settings.with.map {|group| group.to_s }
with -= options[:without] if options[:without]

without = options.fetch("without", [])
without |= Bundler.settings.without.map {|group| group.to_s }
without -= options[:with] if options[:with]

options[:with] = with
options[:without] = without

ENV['RB_USER_INSTALL'] = '1' if Bundler::FREEBSD

# Just disable color in deployment mode
Expand Down Expand Up @@ -69,6 +94,7 @@ def run
Bundler.settings[:no_install] = true if options["no-install"]
Bundler.settings[:clean] = options["clean"] if options["clean"]
Bundler.settings.without = options[:without]
Bundler.settings.with = options[:with]
Bundler::Fetcher.disable_endpoint = options["full-index"]
Bundler.settings[:disable_shared_gems] = Bundler.settings[:path] ? '1' : nil

Expand Down
12 changes: 8 additions & 4 deletions lib/bundler/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ def self.build(gemfile, lockfile, unlock)
# @param unlock [Hash, Boolean, nil] Gems that have been requested
# to be updated or true if all gems should be updated
# @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version
def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil)
# @param optional_groups [Array(String)] A list of optional groups
def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [])
@unlocking = unlock == true || !unlock.empty?

@dependencies, @sources, @unlock = dependencies, sources, unlock
@dependencies, @sources, @unlock, @optional_groups = dependencies, sources, unlock, optional_groups
@remote = false
@specs = nil
@lockfile_contents = ""
Expand Down Expand Up @@ -162,7 +163,7 @@ def missing_specs

def requested_specs
@requested_specs ||= begin
groups = self.groups - Bundler.settings.without
groups = requested_groups
groups.map! { |g| g.to_sym }
specs_for(groups)
end
Expand Down Expand Up @@ -601,7 +602,7 @@ def expand_dependencies(dependencies, remote = false)
end

def requested_dependencies
groups = self.groups - Bundler.settings.without
groups = requested_groups
groups.map! { |g| g.to_sym }
dependencies.reject { |d| !d.should_include? || (d.groups & groups).empty? }
end
Expand Down Expand Up @@ -635,5 +636,8 @@ def pinned_spec_names(specs)
names
end

def requested_groups
self.groups - Bundler.settings.without - @optional_groups + Bundler.settings.with
end
end
end
3 changes: 3 additions & 0 deletions lib/bundler/deployment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def self.define_task(context, task_method = :task, opts = {})
set :bundle_dir, File.join(fetch(:shared_path), 'bundle')
set :bundle_flags, "--deployment --quiet"
set :bundle_without, [:development, :test]
set :bundle_with, [:mysql]
set :bundle_cmd, "bundle" # e.g. "/opt/ruby/bin/bundle"
set :bundle_roles, #{role_default} # e.g. [:app, :batch]
DESC
Expand All @@ -42,6 +43,7 @@ def self.define_task(context, task_method = :task, opts = {})
bundle_dir = context.fetch(:bundle_dir, File.join(context.fetch(:shared_path), 'bundle'))
bundle_gemfile = context.fetch(:bundle_gemfile, "Gemfile")
bundle_without = [*context.fetch(:bundle_without, [:development, :test])].compact
bundle_with = [*context.fetch(:bundle_with, [])].compact
app_path = context.fetch(:latest_release)
if app_path.to_s.empty?
raise error_type.new("Cannot detect current release path - make sure you have deployed at least once.")
Expand All @@ -50,6 +52,7 @@ def self.define_task(context, task_method = :task, opts = {})
args << "--path #{bundle_dir}" unless bundle_dir.to_s.empty?
args << bundle_flags.to_s
args << "--without #{bundle_without.join(" ")}" unless bundle_without.empty?
args << "--with #{bundle_with.join(" ")}" unless bundle_with.empty?

run "cd #{app_path} && #{bundle_cmd} install #{args.join(' ')}"
end
Expand Down
50 changes: 36 additions & 14 deletions lib/bundler/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def initialize
@git_sources = {}
@dependencies = []
@groups = []
@optional_groups = []
@platforms = []
@env = nil
@ruby_version = nil
Expand Down Expand Up @@ -154,11 +155,20 @@ def github(repo, options = {})
end

def to_definition(lockfile, unlock)
Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version)
Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups)
end

def group(*args, &blk)
opts = Hash === args.last ? args.pop.dup : {}
normalize_group_options(opts, args)

@groups.concat args

if opts["optional"]
optional_groups = args - @optional_groups
@optional_groups.concat optional_groups
end

yield
ensure
args.each { @groups.pop }
Expand Down Expand Up @@ -232,19 +242,7 @@ def normalize_options(name, version, opts)
normalize_hash(opts)

git_names = @git_sources.keys.map(&:to_s)

invalid_keys = opts.keys - (valid_keys + git_names)
if invalid_keys.any?
message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} "
message << if invalid_keys.size > 1
"as options for gem '#{name}', but they are invalid."
else
"as an option for gem '#{name}', but it is invalid."
end

message << " Valid options are: #{valid_keys.join(", ")}"
raise InvalidOption, message
end
validate_keys("gem '#{name}'", opts, valid_keys + git_names)

groups = @groups.dup
opts["group"] = opts.delete("groups") || opts["group"]
Expand Down Expand Up @@ -289,6 +287,30 @@ def normalize_options(name, version, opts)
opts["group"] = groups
end

def normalize_group_options(opts, groups)
normalize_hash(opts)

groups = groups.map {|group| ":#{group}" }.join(", ")
validate_keys("group #{groups}", opts, %w(optional))

opts["optional"] ||= false
end

def validate_keys(command, opts, valid_keys)
invalid_keys = opts.keys - valid_keys
if invalid_keys.any?
message = "You passed #{invalid_keys.map{|k| ':'+k }.join(", ")} "
message << if invalid_keys.size > 1
"as options for #{command}, but they are invalid."
else
"as an option for #{command}, but it is invalid."
end

message << " Valid options are: #{valid_keys.join(", ")}"
raise InvalidOption, message
end
end

def normalize_source(source)
case source
when :gemcutter, :rubygems, :rubyforge
Expand Down
20 changes: 18 additions & 2 deletions lib/bundler/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,19 @@ def pretty_values_for(exposed_key)
end

def without=(array)
self[:without] = (array.empty? ? nil : array.join(":")) if array
set_array(:without, array)
end

def with=(array)
set_array(:with, array)
end

def without
self[:without] ? self[:without].split(":").map { |w| w.to_sym } : []
get_array(:without)
end

def with
get_array(:with)
end

# @local_config["BUNDLE_PATH"] should be prioritized over ENV["BUNDLE_PATH"]
Expand Down Expand Up @@ -161,6 +169,14 @@ def to_bool(value)
!(value.nil? || value == '' || value =~ /^(false|f|no|n|0)$/i || value == false)
end

def get_array(key)
self[key] ? self[key].split(":").map { |w| w.to_sym } : []
end

def set_array(key, array)
self[key] = (array.empty? ? nil : array.join(":")) if array
end

def set_key(key, value, hash, file)
key = key_for(key)

Expand Down
9 changes: 9 additions & 0 deletions man/bundle-install.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ bundle-install(1) -- Install the dependencies specified in your Gemfile
[--standalone[=GROUP[ GROUP...]]]
[--trust-policy=POLICY]
[--without=GROUP[ GROUP...]]
[--with=GROUP[ GROUP...]]

## DESCRIPTION

Expand Down Expand Up @@ -125,8 +126,16 @@ update process below under [CONSERVATIVE UPDATING][].

* `--without=<list>`:
A space-separated list of groups referencing gems to skip during installation.
If a group is given that is in the remembered list of groups given
to --with, it is removed from that list.
This is a [remembered option][REMEMBERED OPTIONS].

* `--with=<list>`:
A space-separated list of groups referencing gems to install. If an
optional group is given it is installed. If a group is given that is
in the remembered list of groups given to --without, it is removed
from that list. This is a [remembered option][REMEMBERED OPTIONS].


## DEPLOYMENT MODE

Expand Down
6 changes: 5 additions & 1 deletion man/gemfile.5.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -430,11 +430,15 @@ applied to a group of gems by using block form.
gem "sqlite3"
end

group :development do
group :development, :optional => true do
gem "wirble"
gem "faker"
end

In the case of the group block form the :optional option can be given
to prevent a group from being installed unless listed in the `--with`
option given to the `bundle install` command.

In the case of the `git` block form, the `:ref`, `:branch`, `:tag`,
and `:submodules` options may be passed to the `git` method, and
all gems in the block will inherit those options.
Expand Down
66 changes: 66 additions & 0 deletions spec/install/gems/groups_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@
group :emo do
gem "activesupport", "2.3.5"
end
group :debugging, :optional => true do
gem "thin"
end
G
end

Expand Down Expand Up @@ -159,6 +162,69 @@
bundle :install
should_not_be_installed "activesupport 2.3.5"
end

it "does not install gems from the optional group" do
bundle :install
should_not_be_installed "thin 1.0"
end

it "does install gems from the optional group when requested" do
bundle :install, :with => "debugging"
should_be_installed "thin 1.0"
end

it "does install gems from the previously requested group" do
bundle :install, :with => "debugging"
should_be_installed "thin 1.0"
bundle :install
should_be_installed "thin 1.0"
end

it "does install gems from the optional groups requested with BUNDLE_WITH" do
ENV["BUNDLE_WITH"] = "debugging"
bundle :install
should_be_installed "thin 1.0"
ENV["BUNDLE_WITH"] = nil
end

it "clears with when passed an empty list" do
bundle :install, :with => "debugging"
bundle 'install --with ""'
should_not_be_installed "thin 1.0"
end

it "does remove groups from without when passed at with" do
bundle :install, :without => "emo"
bundle :install, :with => "emo"
should_be_installed "activesupport 2.3.5"
end

it "does remove groups from with when passed at without" do
bundle :install, :with => "debugging"
bundle :install, :without => "debugging"
should_not_be_installed "thin 1.0"
end

it "errors out when passing a group to with and without" do
bundle :install, :with => "emo debugging", :without => "emo"
expect(out).to include("The offending groups are: emo")
end

it "can add and remove a group at the same time" do
bundle :install, :with => "debugging", :without => "emo"
should_be_installed "thin 1.0"
should_not_be_installed "activesupport 2.3.5"
end

it "does have no effect when listing a not optional group in with" do
bundle :install, :with => "emo"
should_be_installed "activesupport 2.3.5"
end

it "does have no effect when listing an optional group in without" do
bundle :install, :without => "debugging"
should_not_be_installed "thin 1.0"
end
end

describe "with gems assigned to multiple groups" do
Expand Down

0 comments on commit 13f44d1

Please sign in to comment.