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

Implement optional groups #3531

Merged
merged 1 commit into from Apr 7, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/bundler/cli.rb
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."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably say "specified optional group" here, to make it clear that with only applies to optional groups.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that but actually it does not only apply to optional groups. Applying it to a normal group has no effect but also doesn't throw an error, however applying it to a normal group that was previously excluded with without, removes it from the excluded groups.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense. I read the rest of the diff after I made that comment. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did most reasoning in rubygems/bundler-features#68, which is why I spared it in the PRs description :)


def install
require 'bundler/cli/install'
Expand Down
30 changes: 28 additions & 2 deletions lib/bundler/cli/install.rb
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
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 @@ -161,7 +162,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 @@ -586,7 +587,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 @@ -620,5 +621,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
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
Expand Up @@ -21,6 +21,7 @@ def initialize
@git_sources = {}
@dependencies = []
@groups = []
@optional_groups = []
@platforms = []
@env = nil
@ruby_version = nil
Expand Down Expand Up @@ -153,11 +154,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 @@ -231,19 +241,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 @@ -288,6 +286,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
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
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
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
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