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