class Bundler::Dsl

Constants

GITHUB_PULL_REQUEST_URL
GITLAB_MERGE_REQUEST_URL
VALID_KEYS
VALID_PLATFORMS

Attributes

dependencies[RW]
gemfile[R]
gemspecs[R]

Public Class Methods

evaluate(gemfile, lockfile, unlock) click to toggle source
# File bundler/dsl.rb, line 10
def self.evaluate(gemfile, lockfile, unlock)
  builder = new
  builder.eval_gemfile(gemfile)
  builder.to_definition(lockfile, unlock)
end
new() click to toggle source
# File bundler/dsl.rb, line 27
def initialize
  @source               = nil
  @sources              = SourceList.new
  @git_sources          = {}
  @dependencies         = []
  @groups               = []
  @install_conditionals = []
  @optional_groups      = []
  @platforms            = []
  @env                  = nil
  @ruby_version         = nil
  @gemspecs             = []
  @gemfile              = nil
  @gemfiles             = []
  add_git_sources
end

Public Instance Methods

check_primary_source_safety() click to toggle source
# File bundler/dsl.rb, line 281
def check_primary_source_safety
  check_path_source_safety
  check_rubygems_source_safety
end
env(name) { || ... } click to toggle source
# File bundler/dsl.rb, line 265
def env(name)
  old = @env
  @env = name
  yield
ensure
  @env = old
end
eval_gemfile(gemfile, contents = nil) click to toggle source
# File bundler/dsl.rb, line 44
def eval_gemfile(gemfile, contents = nil)
  expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent)
  original_gemfile = @gemfile
  @gemfile = expanded_gemfile_path
  @gemfiles << expanded_gemfile_path
  contents ||= Bundler.read_file(@gemfile.to_s)
  instance_eval(contents, @gemfile.to_s, 1)
rescue Exception => e # rubocop:disable Lint/RescueException
  message = "There was an error " \
    "#{e.is_a?(GemfileEvalError) ? "evaluating" : "parsing"} " \
    "`#{File.basename gemfile.to_s}`: #{e.message}"

  raise DSLError.new(message, gemfile, e.backtrace, contents)
ensure
  @gemfile = original_gemfile
end
gem(name, *args) click to toggle source
# File bundler/dsl.rb, line 95
def gem(name, *args)
  options = args.last.is_a?(Hash) ? args.pop.dup : {}
  options["gemfile"] = @gemfile
  version = args || [">= 0"]

  normalize_options(name, version, options)

  dep = Dependency.new(name, version, options)

  # if there's already a dependency with this name we try to prefer one
  if current = @dependencies.find {|d| d.name == dep.name }
    if current.requirement != dep.requirement
      current_requirement_open = current.requirements_list.include?(">= 0")

      gemspec_dep = [dep, current].find(&:gemspec_dev_dep?)
      if gemspec_dep
        gemfile_dep = [dep, current].find(&:runtime?)

        unless current_requirement_open
          Bundler.ui.warn "A gemspec development dependency (#{gemspec_dep.name}, #{gemspec_dep.requirement}) is being overridden by a Gemfile dependency (#{gemfile_dep.name}, #{gemfile_dep.requirement}).\n" \
                          "This behaviour may change in the future. Please remove either of them, or make sure they both have the same requirement\n"
        end
      else
        update_prompt = ""

        if File.basename(@gemfile) == Injector::INJECTED_GEMS
          if dep.requirements_list.include?(">= 0") && !current_requirement_open
            update_prompt = ". Gem already added"
          else
            update_prompt = ". If you want to update the gem version, run `bundle update #{current.name}`"

            update_prompt += ". You may also need to change the version requirement specified in the Gemfile if it's too restrictive." unless current_requirement_open
          end
        end

        raise GemfileError, "You cannot specify the same gem twice with different version requirements.\n" \
                       "You specified: #{current.name} (#{current.requirement}) and #{dep.name} (#{dep.requirement})" \
                       "#{update_prompt}"
      end
    end

    # Always prefer the dependency from the Gemfile
    if current.gemspec_dev_dep?
      @dependencies.delete(current)
    elsif dep.gemspec_dev_dep?
      return
    elsif current.source != dep.source
      raise GemfileError, "You cannot specify the same gem twice coming from different sources.\n" \
                      "You specified that #{dep.name} (#{dep.requirement}) should come from " \
                      "#{current.source || "an unspecified source"} and #{dep.source}\n"
    else
      Bundler.ui.warn "Your Gemfile lists the gem #{current.name} (#{current.requirement}) more than once.\n" \
                      "You should probably keep only one of them.\n" \
                      "Remove any duplicate entries and specify the gem only once.\n" \
                      "While it's not a problem now, it could cause errors if you change the version of one of them later."
    end
  end

  @dependencies << dep
end
gemspec(opts = nil) click to toggle source
# File bundler/dsl.rb, line 61
def gemspec(opts = nil)
  opts ||= {}
  path              = opts[:path] || "."
  glob              = opts[:glob]
  name              = opts[:name]
  development_group = opts[:development_group] || :development
  expanded_path     = gemfile_root.join(path)

  gemspecs = Gem::Util.glob_files_in_dir("{,*}.gemspec", expanded_path).map {|g| Bundler.load_gemspec(g) }.compact
  gemspecs.reject! {|s| s.name != name } if name
  specs_by_name_and_version = gemspecs.group_by {|s| [s.name, s.version] }

  case specs_by_name_and_version.size
  when 1
    specs = specs_by_name_and_version.values.first
    spec = specs.find {|s| s.match_platform(Bundler.local_platform) } || specs.first

    @gemspecs << spec

    gem spec.name, name: spec.name, path: path, glob: glob

    group(development_group) do
      spec.development_dependencies.each do |dep|
        gem dep.name, *(dep.requirement.as_list + [type: :development])
      end
    end
  when 0
    raise InvalidOption, "There are no gemspecs at #{expanded_path}"
  else
    raise InvalidOption, "There are multiple gemspecs at #{expanded_path}. " \
      "Please use the :name option to specify which one should be used"
  end
end
git(uri, options = {}, &blk) click to toggle source
# File bundler/dsl.rb, line 206
def git(uri, options = {}, &blk)
  unless block_given?
    msg = "You can no longer specify a git source by itself. Instead, \n" \
          "either use the :git option on a gem, or specify the gems that \n" \
          "bundler should find in the git source by passing a block to \n" \
          "the git method, like: \n\n" \
          "  git 'git://github.com/rails/rails.git' do\n" \
          "    gem 'rails'\n" \
          "  end"
    raise DeprecatedError, msg
  end

  with_source(@sources.add_git_source(normalize_hash(options).merge("uri" => uri)), &blk)
end
git_source(name, &block) click to toggle source
# File bundler/dsl.rb, line 180
def git_source(name, &block)
  unless block_given?
    raise InvalidOption, "You need to pass a block to #git_source"
  end

  if valid_keys.include?(name.to_s)
    raise InvalidOption, "You cannot use #{name} as a git source. It " \
      "is a reserved key. Reserved keys are: #{valid_keys.join(", ")}"
  end

  @git_sources[name.to_s] = block
end
github(repo, options = {}) { || ... } click to toggle source
# File bundler/dsl.rb, line 221
def github(repo, options = {})
  raise ArgumentError, "GitHub sources require a block" unless block_given?
  github_uri  = @git_sources["github"].call(repo)
  git_options = normalize_hash(options).merge("uri" => github_uri)
  git_source  = @sources.add_git_source(git_options)
  with_source(git_source) { yield }
end
group(*args) { || ... } click to toggle source
# File bundler/dsl.rb, line 234
def group(*args, &blk)
  options = args.last.is_a?(Hash) ? args.pop.dup : {}
  normalize_group_options(options, args)

  @groups.concat args

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

  yield
ensure
  args.each { @groups.pop }
end
install_if(*args) { || ... } click to toggle source
# File bundler/dsl.rb, line 250
def install_if(*args)
  @install_conditionals.concat args
  yield
ensure
  args.each { @install_conditionals.pop }
end
method_missing(name, *args) click to toggle source
# File bundler/dsl.rb, line 277
def method_missing(name, *args)
  raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile"
end
path(path, options = {}, &blk) click to toggle source
# File bundler/dsl.rb, line 193
def path(path, options = {}, &blk)
  source_options = normalize_hash(options).merge(
    "path" => Pathname.new(path),
    "root_path" => gemfile_root,
    "gemspec" => gemspecs.find {|g| g.name == options["name"] }
  )

  source_options["global"] = true unless block_given?

  source = @sources.add_path_source(source_options)
  with_source(source, &blk)
end
platform(*platforms)
Alias for: platforms
platforms(*platforms) { || ... } click to toggle source
# File bundler/dsl.rb, line 257
def platforms(*platforms)
  @platforms.concat platforms
  yield
ensure
  platforms.each { @platforms.pop }
end
Also aliased as: platform
plugin(*args) click to toggle source
# File bundler/dsl.rb, line 273
def plugin(*args)
  # Pass on
end
source(source, *args, &blk) click to toggle source
# File bundler/dsl.rb, line 156
def source(source, *args, &blk)
  options = args.last.is_a?(Hash) ? args.pop.dup : {}
  options = normalize_hash(options)
  source = normalize_source(source)

  if options.key?("type")
    options["type"] = options["type"].to_s
    unless Plugin.source?(options["type"])
      raise InvalidOption, "No plugin sources available for #{options["type"]}"
    end

    unless block_given?
      raise InvalidOption, "You need to pass a block to #source with :type option"
    end

    source_opts = options.merge("uri" => source)
    with_source(@sources.add_plugin_source(options["type"], source_opts), &blk)
  elsif block_given?
    with_source(@sources.add_rubygems_source("remotes" => source), &blk)
  else
    @sources.add_global_rubygems_remote(source)
  end
end
to_definition(lockfile, unlock) click to toggle source
# File bundler/dsl.rb, line 229
def to_definition(lockfile, unlock)
  check_primary_source_safety
  Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles)
end

Private Instance Methods

add_git_sources() click to toggle source
# File bundler/dsl.rb, line 288
def add_git_sources
  git_source(:github) do |repo_name|
    if repo_name =~ GITHUB_PULL_REQUEST_URL
      {
        "git" => "https://github.com/#{$1}.git",
        "branch" => nil,
        "ref" => "refs/pull/#{$2}/head",
        "tag" => nil,
      }
    else
      repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
      "https://github.com/#{repo_name}.git"
    end
  end

  git_source(:gist) do |repo_name|
    "https://gist.github.com/#{repo_name}.git"
  end

  git_source(:bitbucket) do |repo_name|
    user_name, repo_name = repo_name.split("/")
    repo_name ||= user_name
    "https://#{user_name}@bitbucket.org/#{user_name}/#{repo_name}.git"
  end

  git_source(:gitlab) do |repo_name|
    if repo_name =~ GITLAB_MERGE_REQUEST_URL
      {
        "git" => "https://gitlab.com/#{$1}.git",
        "branch" => nil,
        "ref" => "refs/merge-requests/#{$2}/head",
        "tag" => nil,
      }
    else
      repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
      "https://gitlab.com/#{repo_name}.git"
    end
  end
end
check_path_source_safety() click to toggle source
# File bundler/dsl.rb, line 465
def check_path_source_safety
  return if @sources.global_path_source.nil?

  msg = "You can no longer specify a path source by itself. Instead, \n" \
          "either use the :path option on a gem, or specify the gems that \n" \
          "bundler should find in the path source by passing a block to \n" \
          "the path method, like: \n\n" \
          "    path 'dir/containing/rails' do\n" \
          "      gem 'rails'\n" \
          "    end\n\n"

  SharedHelpers.major_deprecation(2, msg.strip)
end
check_rubygems_source_safety() click to toggle source
# File bundler/dsl.rb, line 479
def check_rubygems_source_safety
  if @sources.implicit_global_source?
    implicit_global_source_warning
  elsif @sources.aggregate_global_source?
    multiple_global_source_warning
  end
end
gemfile_root() click to toggle source
# File bundler/dsl.rb, line 614
def gemfile_root
  @gemfile ||= Bundler.default_gemfile
  @gemfile.dirname
end
implicit_global_source_warning() click to toggle source
# File bundler/dsl.rb, line 487
def implicit_global_source_warning
  Bundler::SharedHelpers.major_deprecation 2, "This Gemfile does not include an explicit global source. " \
    "Not using an explicit global source may result in a different lockfile being generated depending on " \
    "the gems you have installed locally before bundler is run. " \
    "Instead, define a global source in your Gemfile like this: source \"https://rubygems.org\"."
end
multiple_global_source_warning() click to toggle source
# File bundler/dsl.rb, line 494
def multiple_global_source_warning
  if Bundler.feature_flag.bundler_3_mode?
    msg = "This Gemfile contains multiple global sources. " \
      "Each source after the first must include a block to indicate which gems " \
      "should come from that source"
    raise GemfileEvalError, msg
  else
    message =
      "Your Gemfile contains multiple global sources. " \
      "Using `source` more than once without a block is a security risk, and " \
      "may result in installing unexpected gems. To resolve this warning, use " \
      "a block to indicate which gems should come from the secondary source."
    removed_message =
      "Your Gemfile contains multiple global sources. " \
      "Using `source` more than once without a block is a security risk, and " \
      "may result in installing unexpected gems. To resolve this error, use " \
      "a block to indicate which gems should come from the secondary source."
    Bundler::SharedHelpers.major_deprecation 2, message, removed_message: removed_message
  end
end
normalize_group_options(opts, groups) click to toggle source
# File bundler/dsl.rb, line 417
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
normalize_hash(opts) click to toggle source
# File bundler/dsl.rb, line 339
def normalize_hash(opts)
  opts.keys.each do |k|
    opts[k.to_s] = opts.delete(k) unless k.is_a?(String)
  end
  opts
end
normalize_options(name, version, opts) click to toggle source
# File bundler/dsl.rb, line 350
def normalize_options(name, version, opts)
  if name.is_a?(Symbol)
    raise GemfileError, %(You need to specify gem names as Strings. Use 'gem "#{name}"' instead)
  end
  if /\s/.match?(name)
    raise GemfileError, %('#{name}' is not a valid gem name because it contains whitespace)
  end
  raise GemfileError, %(an empty gem name is not valid) if name.empty?

  normalize_hash(opts)

  git_names = @git_sources.keys.map(&:to_s)
  validate_keys("gem '#{name}'", opts, valid_keys + git_names)

  groups = @groups.dup
  opts["group"] = opts.delete("groups") || opts["group"]
  groups.concat Array(opts.delete("group"))
  groups = [:default] if groups.empty?

  install_if = @install_conditionals.dup
  install_if.concat Array(opts.delete("install_if"))
  install_if = install_if.reduce(true) do |memo, val|
    memo && (val.respond_to?(:call) ? val.call : val)
  end

  platforms = @platforms.dup
  opts["platforms"] = opts["platform"] || opts["platforms"]
  platforms.concat Array(opts.delete("platforms"))
  platforms.map!(&:to_sym)
  platforms.each do |p|
    next if VALID_PLATFORMS.include?(p)
    raise GemfileError, "`#{p}` is not a valid platform. The available options are: #{VALID_PLATFORMS.inspect}"
  end

  # Save sources passed in a key
  if opts.key?("source")
    source = normalize_source(opts["source"])
    opts["source"] = @sources.add_rubygems_source("remotes" => source)
  end

  git_name = (git_names & opts.keys).last
  if @git_sources[git_name]
    git_opts = @git_sources[git_name].call(opts[git_name])
    git_opts = { "git" => git_opts } if git_opts.is_a?(String)
    opts.merge!(git_opts) do |key, _gemfile_value, _git_source_value|
      raise GemfileError, %(The :#{key} option can't be used with `#{git_name}: #{opts[git_name].inspect}`)
    end
  end

  %w[git path].each do |type|
    next unless param = opts[type]
    if version.first && version.first =~ /^\s*=?\s*(\d[^\s]*)\s*$/
      options = opts.merge("name" => name, "version" => $1)
    else
      options = opts.dup
    end
    source = send(type, param, options) {}
    opts["source"] = source
  end

  opts["source"]         ||= @source
  opts["env"]            ||= @env
  opts["platforms"]      = platforms.dup
  opts["group"]          = groups
  opts["should_include"] = install_if
end
normalize_source(source) click to toggle source
# File bundler/dsl.rb, line 447
def normalize_source(source)
  case source
  when :gemcutter, :rubygems, :rubyforge
    message =
      "The source :#{source} is deprecated because HTTP requests are insecure.\n" \
      "Please change your source to 'https://rubygems.org' if possible, or 'http://rubygems.org' if not."
    removed_message =
      "The source :#{source} is disallowed because HTTP requests are insecure.\n" \
      "Please change your source to 'https://rubygems.org' if possible, or 'http://rubygems.org' if not."
    Bundler::SharedHelpers.major_deprecation 2, message, removed_message: removed_message
    "http://rubygems.org"
  when String
    source
  else
    raise GemfileError, "Unknown source '#{source}'"
  end
end
valid_keys() click to toggle source
# File bundler/dsl.rb, line 346
def valid_keys
  @valid_keys ||= VALID_KEYS
end
validate_keys(command, opts, valid_keys) click to toggle source
# File bundler/dsl.rb, line 426
def validate_keys(command, opts, valid_keys)
  if opts["branch"] && !(opts["git"] || opts["github"] || (opts.keys & @git_sources.keys.map(&:to_s)).any?)
    raise GemfileError, %(The `branch` option for `#{command}` is not allowed. Only gems with a git source can specify a branch)
  end

  invalid_keys = opts.keys - valid_keys
  return true unless invalid_keys.any?

  message = String.new
  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(", ")}."
  message << " You may be able to resolve this by upgrading Bundler to the newest version."
  raise InvalidOption, message
end
with_source(source) { || ... } click to toggle source
# File bundler/dsl.rb, line 328
def with_source(source)
  old_source = @source
  if block_given?
    @source = source
    yield
  end
  source
ensure
  @source = old_source
end