diff options
Diffstat (limited to 'lib/rubygems/request_set.rb')
| -rw-r--r-- | lib/rubygems/request_set.rb | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb new file mode 100644 index 0000000000..eb8b4658f3 --- /dev/null +++ b/lib/rubygems/request_set.rb @@ -0,0 +1,514 @@ +# frozen_string_literal: true + +require_relative "vendored_tsort" + +## +# A RequestSet groups a request to activate a set of dependencies. +# +# nokogiri = Gem::Dependency.new 'nokogiri', '~> 1.6' +# pg = Gem::Dependency.new 'pg', '~> 0.14' +# +# set = Gem::RequestSet.new nokogiri, pg +# +# requests = set.resolve +# +# p requests.map { |r| r.full_name } +# #=> ["nokogiri-1.6.0", "mini_portile-0.5.1", "pg-0.17.0"] + +class Gem::RequestSet + include Gem::TSort + + ## + # Array of gems to install even if already installed + + attr_accessor :always_install + + attr_reader :dependencies + + attr_accessor :development + + ## + # Errors fetching gems during resolution. + + attr_reader :errors + + ## + # Set to true if you want to install only direct development dependencies. + + attr_accessor :development_shallow + + ## + # The set of git gems imported via load_gemdeps. + + attr_reader :git_set # :nodoc: + + ## + # When true, dependency resolution is not performed, only the requested gems + # are installed. + + attr_accessor :ignore_dependencies + + attr_reader :install_dir # :nodoc: + + ## + # If true, allow dependencies to match prerelease gems. + + attr_accessor :prerelease + + ## + # When false no remote sets are used for resolving gems. + + attr_accessor :remote + + attr_reader :resolver # :nodoc: + + ## + # Sets used for resolution + + attr_reader :sets # :nodoc: + + ## + # Treat missing dependencies as silent errors + + attr_accessor :soft_missing + + ## + # The set of vendor gems imported via load_gemdeps. + + attr_reader :vendor_set # :nodoc: + + ## + # The set of source gems imported via load_gemdeps. + + attr_reader :source_set + + ## + # Creates a RequestSet for a list of Gem::Dependency objects, +deps+. You + # can then #resolve and #install the resolved list of dependencies. + # + # nokogiri = Gem::Dependency.new 'nokogiri', '~> 1.6' + # pg = Gem::Dependency.new 'pg', '~> 0.14' + # + # set = Gem::RequestSet.new nokogiri, pg + + def initialize(*deps) + @dependencies = deps + + @always_install = [] + @conservative = false + @dependency_names = {} + @development = false + @development_shallow = false + @errors = [] + @git_set = nil + @ignore_dependencies = false + @install_dir = Gem.dir + @prerelease = false + @remote = true + @requests = [] + @sets = [] + @soft_missing = false + @sorted_requests = nil + @specs = nil + @vendor_set = nil + @source_set = nil + + yield self if block_given? + end + + ## + # Declare that a gem of name +name+ with +reqs+ requirements is needed. + + def gem(name, *reqs) + if dep = @dependency_names[name] + dep.requirement.concat reqs + else + dep = Gem::Dependency.new name, *reqs + @dependency_names[name] = dep + @dependencies << dep + end + end + + ## + # Add +deps+ Gem::Dependency objects to the set. + + def import(deps) + @dependencies.concat deps + end + + ## + # Installs gems for this RequestSet using the Gem::Installer +options+. + # + # If a +block+ is given an activation +request+ and +installer+ are yielded. + # The +installer+ will be +nil+ if a gem matching the request was already + # installed. + + def install(options, &block) # :yields: request, installer + if dir = options[:install_dir] + requests = install_into dir, false, options, &block + return requests + end + + @prerelease = options[:prerelease] + + requests = [] + download_queue = Thread::Queue.new + + # Create a thread-safe list of gems to download + sorted_requests.each do |req| + download_queue << req + end + + # Create N threads in a pool, have them download all the gems + threads = Array.new(Gem.configuration.concurrent_downloads) do + # When a thread pops this item, it knows to stop running. The symbol + # is queued here so that there will be one symbol per thread. + download_queue << :stop + + Thread.new do + # The pop method will block waiting for items, so the only way + # to stop a thread from running is to provide a final item that + # means the thread should stop. + while req = download_queue.pop + break if req == :stop + req.spec.download options unless req.installed? + end + end + end + + # Wait for all the downloads to finish before continuing + threads.each(&:value) + + # Install requested gems after they have been downloaded + sorted_requests.each do |req| + if req.installed? && @always_install.none? {|spec| spec == req.spec.spec } + req.spec.spec.build_extensions unless options[:build_extension] == false + yield req, nil if block_given? + next + end + + spec = + begin + req.spec.install options do |installer| + yield req, installer if block_given? + end + rescue Gem::RuntimeRequirementNotMetError => e + suggestion = "There are no versions of #{req.request} compatible with your Ruby & RubyGems" + suggestion += ". Maybe try installing an older version of the gem you're looking for?" unless @always_install.include?(req.spec.spec) + e.suggestion = suggestion + raise + end + + requests << spec + end + + return requests if options[:gemdeps] + + install_hooks requests, options + + requests + end + + ## + # Installs from the gem dependencies files in the +:gemdeps+ option in + # +options+, yielding to the +block+ as in #install. + # + # If +:without_groups+ is given in the +options+, those groups in the gem + # dependencies file are not used. See Gem::Installer for other +options+. + + def install_from_gemdeps(options, &block) + gemdeps = options[:gemdeps] + + @install_dir = options[:install_dir] || Gem.dir + @prerelease = options[:prerelease] + @remote = options[:domain] != :local + @conservative = true if options[:conservative] + + gem_deps_api = load_gemdeps gemdeps, options[:without_groups], true + + resolve + + if options[:explain] + puts "Gems to install:" + + sorted_requests.each do |spec| + puts " #{spec.full_name}" + end + else + installed = install options, &block + + if options.fetch :lock, true + lockfile = + Gem::RequestSet::Lockfile.build self, gemdeps, gem_deps_api.dependencies + lockfile.write + end + + installed + end + end + + def install_into(dir, force = true, options = {}) + gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = dir + + existing = force ? [] : specs_in(dir) + existing.delete_if {|s| @always_install.include? s } + + dir = File.expand_path dir + + installed = [] + + options[:development] = false + options[:install_dir] = dir + options[:only_install_dir] = true + @prerelease = options[:prerelease] + + sorted_requests.each do |request| + spec = request.spec + + if existing.find {|s| s.full_name == spec.full_name } + yield request, nil if block_given? + next + end + + spec.install options do |installer| + yield request, installer if block_given? + end + + installed << request + end + + install_hooks installed, options + + installed + ensure + ENV["GEM_HOME"] = gem_home + end + + ## + # Call hooks on installed gems + + def install_hooks(requests, options) + specs = requests.map do |request| + case request + when Gem::Resolver::ActivationRequest then + request.spec.spec + else + request + end + end + + require_relative "dependency_installer" + inst = Gem::DependencyInstaller.new options + inst.installed_gems.replace specs + + Gem.done_installing_hooks.each do |hook| + hook.call inst, specs + end unless Gem.done_installing_hooks.empty? + end + + ## + # Load a dependency management file. + + def load_gemdeps(path, without_groups = [], installing = false) + @git_set = Gem::Resolver::GitSet.new + @vendor_set = Gem::Resolver::VendorSet.new + @source_set = Gem::Resolver::SourceSet.new + + @git_set.root_dir = @install_dir + + lock_file = "#{File.expand_path(path)}.lock" + if File.exist?(lock_file) + load_lockfile lock_file + end + + gf = Gem::RequestSet::GemDependencyAPI.new self, path + gf.installing = installing + gf.without_groups = without_groups if without_groups + gf.load + end + + def load_lockfile(lock_file) # :nodoc: + require "bundler" + require "bundler/lockfile_parser" + + # Bundler::Source::Path resolves relative `remote:` paths against + # Bundler.root, which raises when there is no Gemfile in the working + # directory. Anchor it to the lockfile's directory so PATH sections in a + # `gem install -g` lockfile can be parsed without a Bundler environment. + previous_root = Bundler.instance_variable_get(:@root) + Bundler.instance_variable_set(:@root, Pathname.new(File.expand_path(File.dirname(lock_file)))) + + parser = Bundler::LockfileParser.new(File.read(lock_file), lockfile_path: lock_file) + + parser.specs.group_by(&:source).each do |source, specs| + case source + when Bundler::Source::Rubygems + remotes = source.remotes.map {|remote| Gem::Source.new(remote.to_s) } + remotes << Gem::Source.new(Gem::DEFAULT_HOST) if remotes.empty? + lock_set = Gem::Resolver::LockSet.new(remotes) + specs.each do |spec| + added = lock_set.add(spec.name, spec.version.to_s, spec.platform) + spec.dependencies.each do |dep| + added.each {|s| s.add_dependency dep } + end + end + @sets << lock_set + when Bundler::Source::Git + git_set = Gem::Resolver::GitSet.new + git_set.root_dir = @install_dir + specs.each do |spec| + git_spec = git_set.add_git_spec( + spec.name, + spec.version.to_s, + source.uri.to_s, + source.revision, + source.submodules || false + ) + spec.dependencies.each {|dep| git_spec.add_dependency dep } + end + @sets << git_set + when Bundler::Source::Path + vendor_set = Gem::Resolver::VendorSet.new + specs.each do |spec| + loaded = vendor_set.add_vendor_gem(spec.name, source.path.to_s) + spec.dependencies.each {|dep| loaded.dependencies << dep } + end + @sets << vendor_set + end + end + + parser.dependencies.each_value do |dep| + gem dep.name, *dep.requirement.as_list + end + ensure + Bundler.instance_variable_set(:@root, previous_root) if defined?(previous_root) + end + + def pretty_print(q) # :nodoc: + q.group 2, "[RequestSet:", "]" do + q.breakable + + if @remote + q.text "remote" + q.breakable + end + + if @prerelease + q.text "prerelease" + q.breakable + end + + if @development_shallow + q.text "shallow development" + q.breakable + elsif @development + q.text "development" + q.breakable + end + + if @soft_missing + q.text "soft missing" + end + + q.group 2, "[dependencies:", "]" do + q.breakable + @dependencies.map do |dep| + q.text dep.to_s + q.breakable + end + end + + q.breakable + q.text "sets:" + + q.breakable + q.pp @sets.map(&:class) + end + end + + ## + # Resolve the requested dependencies and return an Array of Specification + # objects to be activated. + + def resolve(set = Gem::Resolver::BestSet.new) + @sets << set + @sets << @git_set + @sets << @vendor_set + @sets << @source_set + + set = Gem::Resolver.compose_sets(*@sets) + set.remote = @remote + set.prerelease = @prerelease + + resolver = Gem::Resolver.new @dependencies, set + resolver.development = @development + resolver.development_shallow = @development_shallow + resolver.ignore_dependencies = @ignore_dependencies + resolver.soft_missing = @soft_missing + + if @conservative + installed_gems = {} + Gem::Specification.find_all do |spec| + (installed_gems[spec.name] ||= []) << spec + end + resolver.skip_gems = installed_gems + end + + @resolver = resolver + + @requests = resolver.resolve + + @errors = set.errors + + @requests + end + + ## + # Resolve the requested dependencies against the gems available via Gem.path + # and return an Array of Specification objects to be activated. + + def resolve_current + resolve Gem::Resolver::CurrentSet.new + end + + def sorted_requests + @sorted_requests ||= strongly_connected_components.flatten + end + + def specs + @specs ||= @requests.map(&:full_spec) + end + + def specs_in(dir) + Gem::Util.glob_files_in_dir("*.gemspec", File.join(dir, "specifications")).map do |g| + Gem::Specification.load g + end + end + + def tsort_each_node(&block) # :nodoc: + @requests.each(&block) + end + + def tsort_each_child(node) # :nodoc: + node.spec.dependencies.each do |dep| + next if dep.type == :development && !@development + + match = @requests.find do |r| + dep.match?(r.spec.name, r.spec.version, r.spec.is_a?(Gem::Resolver::InstalledSpecification) || @prerelease) + end + + unless match + next if dep.type == :development && @development_shallow + next if @soft_missing + raise Gem::DependencyError, + "Unresolved dependency found during sorting - #{dep} (requested by #{node.spec.full_name})" + end + + yield match + end + end +end + +require_relative "request_set/gem_dependency_api" +require_relative "request_set/lockfile" |
