1# frozen_string_literal: true
2require 'rubygems'
3require 'rubygems/dependency_list'
4require 'rubygems/package'
5require 'rubygems/installer'
6require 'rubygems/spec_fetcher'
7require 'rubygems/user_interaction'
8require 'rubygems/source'
9require 'rubygems/available_set'
10require 'rubygems/deprecate'
11
12##
13# Installs a gem along with all its dependencies from local and remote gems.
14
15class Gem::DependencyInstaller
16
17  include Gem::UserInteraction
18  extend Gem::Deprecate
19
20  DEFAULT_OPTIONS = { # :nodoc:
21    :env_shebang         => false,
22    :document            => %w[ri],
23    :domain              => :both, # HACK dup
24    :force               => false,
25    :format_executable   => false, # HACK dup
26    :ignore_dependencies => false,
27    :prerelease          => false,
28    :security_policy     => nil, # HACK NoSecurity requires OpenSSL. AlmostNo? Low?
29    :wrappers            => true,
30    :build_args          => nil,
31    :build_docs_in_background => false,
32    :install_as_default  => false
33  }.freeze
34
35  ##
36  # Documentation types.  For use by the Gem.done_installing hook
37
38  attr_reader :document
39
40  ##
41  # Errors from SpecFetcher while searching for remote specifications
42
43  attr_reader :errors
44
45  ##
46  # List of gems installed by #install in alphabetic order
47
48  attr_reader :installed_gems
49
50  ##
51  # Creates a new installer instance.
52  #
53  # Options are:
54  # :cache_dir:: Alternate repository path to store .gem files in.
55  # :domain:: :local, :remote, or :both.  :local only searches gems in the
56  #           current directory.  :remote searches only gems in Gem::sources.
57  #           :both searches both.
58  # :env_shebang:: See Gem::Installer::new.
59  # :force:: See Gem::Installer#install.
60  # :format_executable:: See Gem::Installer#initialize.
61  # :ignore_dependencies:: Don't install any dependencies.
62  # :install_dir:: See Gem::Installer#install.
63  # :prerelease:: Allow prerelease versions.  See #install.
64  # :security_policy:: See Gem::Installer::new and Gem::Security.
65  # :user_install:: See Gem::Installer.new
66  # :wrappers:: See Gem::Installer::new
67  # :build_args:: See Gem::Installer::new
68
69  def initialize(options = {})
70    @only_install_dir = !!options[:install_dir]
71    @install_dir = options[:install_dir] || Gem.dir
72    @build_root = options[:build_root]
73
74    options = DEFAULT_OPTIONS.merge options
75
76    @bin_dir             = options[:bin_dir]
77    @dev_shallow         = options[:dev_shallow]
78    @development         = options[:development]
79    @document            = options[:document]
80    @domain              = options[:domain]
81    @env_shebang         = options[:env_shebang]
82    @force               = options[:force]
83    @format_executable   = options[:format_executable]
84    @ignore_dependencies = options[:ignore_dependencies]
85    @prerelease          = options[:prerelease]
86    @security_policy     = options[:security_policy]
87    @user_install        = options[:user_install]
88    @wrappers            = options[:wrappers]
89    @build_args          = options[:build_args]
90    @build_docs_in_background = options[:build_docs_in_background]
91    @install_as_default  = options[:install_as_default]
92    @dir_mode            = options[:dir_mode]
93    @data_mode           = options[:data_mode]
94    @prog_mode           = options[:prog_mode]
95
96    # Indicates that we should not try to update any deps unless
97    # we absolutely must.
98    @minimal_deps        = options[:minimal_deps]
99
100    @available      = nil
101    @installed_gems = []
102    @toplevel_specs = nil
103
104    @cache_dir = options[:cache_dir] || @install_dir
105
106    @errors = []
107  end
108
109  ##
110  #--
111  # TODO remove at RubyGems 4, no longer used
112
113  def add_found_dependencies(to_do, dependency_list) # :nodoc:
114    seen = {}
115    dependencies = Hash.new { |h, name| h[name] = Gem::Dependency.new name }
116
117    until to_do.empty? do
118      spec = to_do.shift
119
120      # HACK why is spec nil?
121      next if spec.nil? or seen[spec.name]
122      seen[spec.name] = true
123
124      deps = spec.runtime_dependencies
125
126      if @development
127        if @dev_shallow
128          if @toplevel_specs.include? spec.full_name
129            deps |= spec.development_dependencies
130          end
131        else
132          deps |= spec.development_dependencies
133        end
134      end
135
136      deps.each do |dep|
137        dependencies[dep.name] = dependencies[dep.name].merge dep
138
139        if @minimal_deps
140          next if Gem::Specification.any? do |installed_spec|
141                    dep.name == installed_spec.name and
142                      dep.requirement.satisfied_by? installed_spec.version
143                  end
144        end
145
146        results = find_gems_with_sources(dep)
147
148        results.sorted.each do |t|
149          to_do.push t.spec
150        end
151
152        results.remove_installed! dep
153
154        @available << results
155        results.inject_into_list dependency_list
156      end
157    end
158
159    dependency_list.remove_specs_unsatisfied_by dependencies
160  end
161  deprecate :add_found_dependencies, :none, 2018, 12
162
163  ##
164  # Creates an AvailableSet to install from based on +dep_or_name+ and
165  # +version+
166
167  def available_set_for(dep_or_name, version) # :nodoc:
168    if String === dep_or_name
169      find_spec_by_name_and_version dep_or_name, version, @prerelease
170    else
171      dep = dep_or_name.dup
172      dep.prerelease = @prerelease
173      @available = find_gems_with_sources dep
174    end
175
176    @available.pick_best!
177  end
178
179  ##
180  # Indicated, based on the requested domain, if local
181  # gems should be considered.
182
183  def consider_local?
184    @domain == :both or @domain == :local
185  end
186
187  ##
188  # Indicated, based on the requested domain, if remote
189  # gems should be considered.
190
191  def consider_remote?
192    @domain == :both or @domain == :remote
193  end
194
195  ##
196  # Returns a list of pairs of gemspecs and source_uris that match
197  # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources)
198  # sources.  Gems are sorted with newer gems preferred over older gems, and
199  # local gems preferred over remote gems.
200
201  def find_gems_with_sources(dep, best_only=false) # :nodoc:
202    set = Gem::AvailableSet.new
203
204    if consider_local?
205      sl = Gem::Source::Local.new
206
207      if spec = sl.find_gem(dep.name)
208        if dep.matches_spec? spec
209          set.add spec, sl
210        end
211      end
212    end
213
214    if consider_remote?
215      begin
216        # TODO this is pulled from #spec_for_dependency to allow
217        # us to filter tuples before fetching specs.
218        #
219        tuples, errors = Gem::SpecFetcher.fetcher.search_for_dependency dep
220
221        if best_only && !tuples.empty?
222          tuples.sort! do |a,b|
223            if b[0].version == a[0].version
224              if b[0].platform != Gem::Platform::RUBY
225                1
226              else
227                -1
228              end
229            else
230              b[0].version <=> a[0].version
231            end
232          end
233          tuples = [tuples.first]
234        end
235
236        specs = []
237        tuples.each do |tup, source|
238          begin
239            spec = source.fetch_spec(tup)
240          rescue Gem::RemoteFetcher::FetchError => e
241            errors << Gem::SourceFetchProblem.new(source, e)
242          else
243            specs << [spec, source]
244          end
245        end
246
247        if @errors
248          @errors += errors
249        else
250          @errors = errors
251        end
252
253        set << specs
254
255      rescue Gem::RemoteFetcher::FetchError => e
256        # FIX if there is a problem talking to the network, we either need to always tell
257        # the user (no really_verbose) or fail hard, not silently tell them that we just
258        # couldn't find their requested gem.
259        verbose do
260          "Error fetching remote data:\t\t#{e.message}\n" \
261            "Falling back to local-only install"
262        end
263        @domain = :local
264      end
265    end
266
267    set
268  end
269
270  ##
271  # Finds a spec and the source_uri it came from for gem +gem_name+ and
272  # +version+.  Returns an Array of specs and sources required for
273  # installation of the gem.
274
275  def find_spec_by_name_and_version(gem_name,
276                                    version = Gem::Requirement.default,
277                                    prerelease = false)
278    set = Gem::AvailableSet.new
279
280    if consider_local?
281      if gem_name =~ /\.gem$/ and File.file? gem_name
282        src = Gem::Source::SpecificFile.new(gem_name)
283        set.add src.spec, src
284      elsif gem_name =~ /\.gem$/
285        Dir[gem_name].each do |name|
286          begin
287            src = Gem::Source::SpecificFile.new name
288            set.add src.spec, src
289          rescue Gem::Package::FormatError
290          end
291        end
292      else
293        local = Gem::Source::Local.new
294
295        if s = local.find_gem(gem_name, version)
296          set.add s, local
297        end
298      end
299    end
300
301    if set.empty?
302      dep = Gem::Dependency.new gem_name, version
303      dep.prerelease = true if prerelease
304
305      set = find_gems_with_sources(dep, true)
306      set.match_platform!
307    end
308
309    if set.empty?
310      raise Gem::SpecificGemNotFoundException.new(gem_name, version, @errors)
311    end
312
313    @available = set
314  end
315
316  ##
317  # Gathers all dependencies necessary for the installation from local and
318  # remote sources unless the ignore_dependencies was given.
319  #--
320  # TODO remove at RubyGems 4
321
322  def gather_dependencies # :nodoc:
323    specs = @available.all_specs
324
325    # these gems were listed by the user, always install them
326    keep_names = specs.map { |spec| spec.full_name }
327
328    if @dev_shallow
329      @toplevel_specs = keep_names
330    end
331
332    dependency_list = Gem::DependencyList.new @development
333    dependency_list.add(*specs)
334    to_do = specs.dup
335    add_found_dependencies to_do, dependency_list unless @ignore_dependencies
336
337    # REFACTOR maybe abstract away using Gem::Specification.include? so
338    # that this isn't dependent only on the currently installed gems
339    dependency_list.specs.reject! { |spec|
340      not keep_names.include?(spec.full_name) and
341      Gem::Specification.include?(spec)
342    }
343
344    unless dependency_list.ok? or @ignore_dependencies or @force
345      reason = dependency_list.why_not_ok?.map { |k,v|
346        "#{k} requires #{v.join(", ")}"
347      }.join("; ")
348      raise Gem::DependencyError, "Unable to resolve dependencies: #{reason}"
349    end
350
351    @gems_to_install = dependency_list.dependency_order.reverse
352  end
353  deprecate :gather_dependencies, :none, 2018, 12
354
355  def in_background(what) # :nodoc:
356    fork_happened = false
357    if @build_docs_in_background and Process.respond_to?(:fork)
358      begin
359        Process.fork do
360          yield
361        end
362        fork_happened = true
363        say "#{what} in a background process."
364      rescue NotImplementedError
365      end
366    end
367    yield unless fork_happened
368  end
369
370  ##
371  # Installs the gem +dep_or_name+ and all its dependencies.  Returns an Array
372  # of installed gem specifications.
373  #
374  # If the +:prerelease+ option is set and there is a prerelease for
375  # +dep_or_name+ the prerelease version will be installed.
376  #
377  # Unless explicitly specified as a prerelease dependency, prerelease gems
378  # that +dep_or_name+ depend on will not be installed.
379  #
380  # If c-1.a depends on b-1 and a-1.a and there is a gem b-1.a available then
381  # c-1.a, b-1 and a-1.a will be installed.  b-1.a will need to be installed
382  # separately.
383
384  def install(dep_or_name, version = Gem::Requirement.default)
385    request_set = resolve_dependencies dep_or_name, version
386
387    @installed_gems = []
388
389    options = {
390      :bin_dir             => @bin_dir,
391      :build_args          => @build_args,
392      :document            => @document,
393      :env_shebang         => @env_shebang,
394      :force               => @force,
395      :format_executable   => @format_executable,
396      :ignore_dependencies => @ignore_dependencies,
397      :prerelease          => @prerelease,
398      :security_policy     => @security_policy,
399      :user_install        => @user_install,
400      :wrappers            => @wrappers,
401      :build_root          => @build_root,
402      :install_as_default  => @install_as_default,
403      :dir_mode            => @dir_mode,
404      :data_mode           => @data_mode,
405      :prog_mode           => @prog_mode,
406    }
407    options[:install_dir] = @install_dir if @only_install_dir
408
409    request_set.install options do |_, installer|
410      @installed_gems << installer.spec if installer
411    end
412
413    @installed_gems.sort!
414
415    # Since this is currently only called for docs, we can be lazy and just say
416    # it's documentation. Ideally the hook adder could decide whether to be in
417    # the background or not, and what to call it.
418    in_background "Installing documentation" do
419      Gem.done_installing_hooks.each do |hook|
420        hook.call self, @installed_gems
421      end
422    end unless Gem.done_installing_hooks.empty?
423
424    @installed_gems
425  end
426
427  def install_development_deps # :nodoc:
428    if @development and @dev_shallow
429      :shallow
430    elsif @development
431      :all
432    else
433      :none
434    end
435  end
436
437  def resolve_dependencies(dep_or_name, version) # :nodoc:
438    request_set = Gem::RequestSet.new
439    request_set.development         = @development
440    request_set.development_shallow = @dev_shallow
441    request_set.soft_missing = @force
442    request_set.prerelease = @prerelease
443    request_set.remote = false unless consider_remote?
444
445    installer_set = Gem::Resolver::InstallerSet.new @domain
446    installer_set.ignore_installed = @only_install_dir
447
448    if consider_local?
449      if dep_or_name =~ /\.gem$/ and File.file? dep_or_name
450        src = Gem::Source::SpecificFile.new dep_or_name
451        installer_set.add_local dep_or_name, src.spec, src
452        version = src.spec.version if version == Gem::Requirement.default
453      elsif dep_or_name =~ /\.gem$/
454        Dir[dep_or_name].each do |name|
455          begin
456            src = Gem::Source::SpecificFile.new name
457            installer_set.add_local dep_or_name, src.spec, src
458          rescue Gem::Package::FormatError
459          end
460        end
461        # else This is a dependency. InstallerSet handles this case
462      end
463    end
464
465    dependency =
466      if spec = installer_set.local?(dep_or_name)
467        Gem::Dependency.new spec.name, version
468      elsif String === dep_or_name
469        Gem::Dependency.new dep_or_name, version
470      else
471        dep_or_name
472      end
473
474    dependency.prerelease = @prerelease
475
476    request_set.import [dependency]
477
478    installer_set.add_always_install dependency
479
480    request_set.always_install = installer_set.always_install
481
482    if @ignore_dependencies
483      installer_set.ignore_dependencies = true
484      request_set.ignore_dependencies   = true
485      request_set.soft_missing          = true
486    end
487
488    request_set.resolve installer_set
489
490    @errors.concat request_set.errors
491
492    request_set
493  end
494
495end
496