1require 'rubygems' 2require 'rubygems/dependency_list' 3require 'rubygems/package' 4require 'rubygems/installer' 5require 'rubygems/spec_fetcher' 6require 'rubygems/user_interaction' 7require 'rubygems/source_local' 8require 'rubygems/source_specific_file' 9require 'rubygems/available_set' 10 11## 12# Installs a gem along with all its dependencies from local and remote gems. 13 14class Gem::DependencyInstaller 15 16 include Gem::UserInteraction 17 18 attr_reader :gems_to_install 19 attr_reader :installed_gems 20 21 ## 22 # Documentation types. For use by the Gem.done_installing hook 23 24 attr_reader :document 25 26 DEFAULT_OPTIONS = { 27 :env_shebang => false, 28 :document => %w[ri], 29 :domain => :both, # HACK dup 30 :force => false, 31 :format_executable => false, # HACK dup 32 :ignore_dependencies => false, 33 :prerelease => false, 34 :security_policy => nil, # HACK NoSecurity requires OpenSSL. AlmostNo? Low? 35 :wrappers => true, 36 :build_args => nil, 37 :build_docs_in_background => false, 38 }.freeze 39 40 ## 41 # Creates a new installer instance. 42 # 43 # Options are: 44 # :cache_dir:: Alternate repository path to store .gem files in. 45 # :domain:: :local, :remote, or :both. :local only searches gems in the 46 # current directory. :remote searches only gems in Gem::sources. 47 # :both searches both. 48 # :env_shebang:: See Gem::Installer::new. 49 # :force:: See Gem::Installer#install. 50 # :format_executable:: See Gem::Installer#initialize. 51 # :ignore_dependencies:: Don't install any dependencies. 52 # :install_dir:: See Gem::Installer#install. 53 # :prerelease:: Allow prerelease versions. See #install. 54 # :security_policy:: See Gem::Installer::new and Gem::Security. 55 # :user_install:: See Gem::Installer.new 56 # :wrappers:: See Gem::Installer::new 57 # :build_args:: See Gem::Installer::new 58 59 def initialize(options = {}) 60 @install_dir = options[:install_dir] || Gem.dir 61 62 if options[:install_dir] then 63 # HACK shouldn't change the global settings, needed for -i behavior 64 # maybe move to the install command? See also github #442 65 Gem::Specification.dirs = @install_dir 66 end 67 68 options = DEFAULT_OPTIONS.merge options 69 70 @bin_dir = options[:bin_dir] 71 @dev_shallow = options[:dev_shallow] 72 @development = options[:development] 73 @document = options[:document] 74 @domain = options[:domain] 75 @env_shebang = options[:env_shebang] 76 @force = options[:force] 77 @format_executable = options[:format_executable] 78 @ignore_dependencies = options[:ignore_dependencies] 79 @prerelease = options[:prerelease] 80 @security_policy = options[:security_policy] 81 @user_install = options[:user_install] 82 @wrappers = options[:wrappers] 83 @build_args = options[:build_args] 84 @build_docs_in_background = options[:build_docs_in_background] 85 86 # Indicates that we should not try to update any deps unless 87 # we absolutely must. 88 @minimal_deps = options[:minimal_deps] 89 90 @available = nil 91 @installed_gems = [] 92 @toplevel_specs = nil 93 94 @cache_dir = options[:cache_dir] || @install_dir 95 96 # Set with any errors that SpecFetcher finds while search through 97 # gemspecs for a dep 98 @errors = nil 99 end 100 101 attr_reader :errors 102 103 ## 104 # Creates an AvailableSet to install from based on +dep_or_name+ and 105 # +version+ 106 107 def available_set_for dep_or_name, version # :nodoc: 108 if String === dep_or_name then 109 find_spec_by_name_and_version dep_or_name, version, @prerelease 110 else 111 dep = dep_or_name.dup 112 dep.prerelease = @prerelease 113 @available = find_gems_with_sources dep 114 end 115 116 @available.pick_best! 117 end 118 119 ## 120 # Indicated, based on the requested domain, if local 121 # gems should be considered. 122 123 def consider_local? 124 @domain == :both or @domain == :local 125 end 126 127 ## 128 # Indicated, based on the requested domain, if remote 129 # gems should be considered. 130 131 def consider_remote? 132 @domain == :both or @domain == :remote 133 end 134 135 ## 136 # Returns a list of pairs of gemspecs and source_uris that match 137 # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources) 138 # sources. Gems are sorted with newer gems preferred over older gems, and 139 # local gems preferred over remote gems. 140 141 def find_gems_with_sources(dep) 142 set = Gem::AvailableSet.new 143 144 if consider_local? 145 sl = Gem::Source::Local.new 146 147 if spec = sl.find_gem(dep.name) 148 if dep.matches_spec? spec 149 set.add spec, sl 150 end 151 end 152 end 153 154 if consider_remote? 155 begin 156 found, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep 157 158 if @errors 159 @errors += errors 160 else 161 @errors = errors 162 end 163 164 set << found 165 166 rescue Gem::RemoteFetcher::FetchError => e 167 # FIX if there is a problem talking to the network, we either need to always tell 168 # the user (no really_verbose) or fail hard, not silently tell them that we just 169 # couldn't find their requested gem. 170 if Gem.configuration.really_verbose then 171 say "Error fetching remote data:\t\t#{e.message}" 172 say "Falling back to local-only install" 173 end 174 @domain = :local 175 end 176 end 177 178 set 179 end 180 181 ## 182 # Gathers all dependencies necessary for the installation from local and 183 # remote sources unless the ignore_dependencies was given. 184 185 def gather_dependencies 186 specs = @available.all_specs 187 188 # these gems were listed by the user, always install them 189 keep_names = specs.map { |spec| spec.full_name } 190 191 if @dev_shallow 192 @toplevel_specs = keep_names 193 end 194 195 dependency_list = Gem::DependencyList.new @development 196 dependency_list.add(*specs) 197 to_do = specs.dup 198 add_found_dependencies to_do, dependency_list unless @ignore_dependencies 199 200 # REFACTOR maybe abstract away using Gem::Specification.include? so 201 # that this isn't dependent only on the currently installed gems 202 dependency_list.specs.reject! { |spec| 203 not keep_names.include?(spec.full_name) and 204 Gem::Specification.include?(spec) 205 } 206 207 unless dependency_list.ok? or @ignore_dependencies or @force then 208 reason = dependency_list.why_not_ok?.map { |k,v| 209 "#{k} requires #{v.join(", ")}" 210 }.join("; ") 211 raise Gem::DependencyError, "Unable to resolve dependencies: #{reason}" 212 end 213 214 @gems_to_install = dependency_list.dependency_order.reverse 215 end 216 217 def add_found_dependencies to_do, dependency_list 218 seen = {} 219 dependencies = Hash.new { |h, name| h[name] = Gem::Dependency.new name } 220 221 until to_do.empty? do 222 spec = to_do.shift 223 224 # HACK why is spec nil? 225 next if spec.nil? or seen[spec.name] 226 seen[spec.name] = true 227 228 deps = spec.runtime_dependencies 229 230 if @development 231 if @dev_shallow 232 if @toplevel_specs.include? spec.full_name 233 deps |= spec.development_dependencies 234 end 235 else 236 deps |= spec.development_dependencies 237 end 238 end 239 240 deps.each do |dep| 241 dependencies[dep.name] = dependencies[dep.name].merge dep 242 243 if @minimal_deps 244 next if Gem::Specification.any? do |installed_spec| 245 dep.name == installed_spec.name and 246 dep.requirement.satisfied_by? installed_spec.version 247 end 248 end 249 250 results = find_gems_with_sources(dep) 251 252 results.sorted.each do |t| 253 to_do.push t.spec 254 end 255 256 results.remove_installed! dep 257 258 @available << results 259 results.inject_into_list dependency_list 260 end 261 end 262 263 dependency_list.remove_specs_unsatisfied_by dependencies 264 end 265 266 ## 267 # Finds a spec and the source_uri it came from for gem +gem_name+ and 268 # +version+. Returns an Array of specs and sources required for 269 # installation of the gem. 270 271 def find_spec_by_name_and_version(gem_name, 272 version = Gem::Requirement.default, 273 prerelease = false) 274 275 set = Gem::AvailableSet.new 276 277 if consider_local? 278 if gem_name =~ /\.gem$/ and File.file? gem_name then 279 src = Gem::Source::SpecificFile.new(gem_name) 280 set.add src.spec, src 281 elsif gem_name =~ /\.gem$/ then 282 Dir[gem_name].each do |name| 283 begin 284 src = Gem::Source::SpecificFile.new name 285 set.add src.spec, src 286 rescue Gem::Package::FormatError 287 end 288 end 289 else 290 local = Gem::Source::Local.new 291 292 if s = local.find_gem(gem_name, version) 293 set.add s, local 294 end 295 end 296 end 297 298 if set.empty? 299 dep = Gem::Dependency.new gem_name, version 300 # HACK Dependency objects should be immutable 301 dep.prerelease = true if prerelease 302 303 set = find_gems_with_sources(dep) 304 set.match_platform! 305 end 306 307 if set.empty? 308 raise Gem::SpecificGemNotFoundException.new(gem_name, version, @errors) 309 end 310 311 @available = set 312 end 313 314 ## 315 # Installs the gem +dep_or_name+ and all its dependencies. Returns an Array 316 # of installed gem specifications. 317 # 318 # If the +:prerelease+ option is set and there is a prerelease for 319 # +dep_or_name+ the prerelease version will be installed. 320 # 321 # Unless explicitly specified as a prerelease dependency, prerelease gems 322 # that +dep_or_name+ depend on will not be installed. 323 # 324 # If c-1.a depends on b-1 and a-1.a and there is a gem b-1.a available then 325 # c-1.a, b-1 and a-1.a will be installed. b-1.a will need to be installed 326 # separately. 327 328 def install dep_or_name, version = Gem::Requirement.default 329 available_set_for dep_or_name, version 330 331 @installed_gems = [] 332 333 gather_dependencies 334 335 # REFACTOR is the last gem always the one that the user requested? 336 # This code assumes that but is that actually validated by the code? 337 338 last = @gems_to_install.size - 1 339 @gems_to_install.each_with_index do |spec, index| 340 # REFACTOR more current spec set hardcoding, should be abstracted? 341 next if Gem::Specification.include?(spec) and index != last 342 343 # TODO: make this sorta_verbose so other users can benefit from it 344 say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose 345 346 source = @available.source_for spec 347 348 begin 349 # REFACTOR make the fetcher to use configurable 350 local_gem_path = source.download spec, @cache_dir 351 rescue Gem::RemoteFetcher::FetchError 352 # TODO I doubt all fetch errors are recoverable, we should at least 353 # report the errors probably. 354 next if @force 355 raise 356 end 357 358 if @development 359 if @dev_shallow 360 is_dev = @toplevel_specs.include? spec.full_name 361 else 362 is_dev = true 363 end 364 end 365 366 inst = Gem::Installer.new local_gem_path, 367 :bin_dir => @bin_dir, 368 :development => is_dev, 369 :env_shebang => @env_shebang, 370 :force => @force, 371 :format_executable => @format_executable, 372 :ignore_dependencies => @ignore_dependencies, 373 :install_dir => @install_dir, 374 :security_policy => @security_policy, 375 :user_install => @user_install, 376 :wrappers => @wrappers, 377 :build_args => @build_args 378 379 spec = inst.install 380 381 @installed_gems << spec 382 end 383 384 # Since this is currently only called for docs, we can be lazy and just say 385 # it's documentation. Ideally the hook adder could decide whether to be in 386 # the background or not, and what to call it. 387 in_background "Installing documentation" do 388 Gem.done_installing_hooks.each do |hook| 389 hook.call self, @installed_gems 390 end 391 end unless Gem.done_installing_hooks.empty? 392 393 @installed_gems 394 end 395 396 def in_background what 397 fork_happened = false 398 if @build_docs_in_background and Process.respond_to?(:fork) 399 begin 400 Process.fork do 401 yield 402 end 403 fork_happened = true 404 say "#{what} in a background process." 405 rescue NotImplementedError 406 end 407 end 408 yield unless fork_happened 409 end 410end 411