aboutsummaryrefslogtreecommitdiffstats
path: root/lib/bundler/runtime.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/runtime.rb')
-rw-r--r--lib/bundler/runtime.rb320
1 files changed, 320 insertions, 0 deletions
diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb
new file mode 100644
index 0000000000..5540509d74
--- /dev/null
+++ b/lib/bundler/runtime.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+require "digest/sha1"
+
+module Bundler
+ class Runtime
+ include SharedHelpers
+
+ def initialize(root, definition)
+ @root = root
+ @definition = definition
+ end
+
+ def setup(*groups)
+ @definition.ensure_equivalent_gemfile_and_lockfile if Bundler.settings[:frozen]
+
+ groups.map!(&:to_sym)
+
+ # Has to happen first
+ clean_load_path
+
+ specs = groups.any? ? @definition.specs_for(groups) : requested_specs
+
+ SharedHelpers.set_bundle_environment
+ Bundler.rubygems.replace_entrypoints(specs)
+
+ # Activate the specs
+ load_paths = specs.map do |spec|
+ unless spec.loaded_from
+ raise GemNotFound, "#{spec.full_name} is missing. Run `bundle install` to get it."
+ end
+
+ check_for_activated_spec!(spec)
+
+ Bundler.rubygems.mark_loaded(spec)
+ spec.load_paths.reject {|path| $LOAD_PATH.include?(path) }
+ end.reverse.flatten
+
+ # See Gem::Specification#add_self_to_load_path (since RubyGems 1.8)
+ if insert_index = Bundler.rubygems.load_path_insert_index
+ # Gem directories must come after -I and ENV['RUBYLIB']
+ $LOAD_PATH.insert(insert_index, *load_paths)
+ else
+ # We are probably testing in core, -I and RUBYLIB don't apply
+ $LOAD_PATH.unshift(*load_paths)
+ end
+
+ setup_manpath
+
+ lock(:preserve_unknown_sections => true)
+
+ self
+ end
+
+ REQUIRE_ERRORS = [
+ /^no such file to load -- (.+)$/i,
+ /^Missing \w+ (?:file\s*)?([^\s]+.rb)$/i,
+ /^Missing API definition file in (.+)$/i,
+ /^cannot load such file -- (.+)$/i,
+ /^dlopen\([^)]*\): Library not loaded: (.+)$/i,
+ ].freeze
+
+ def require(*groups)
+ groups.map!(&:to_sym)
+ groups = [:default] if groups.empty?
+
+ @definition.dependencies.each do |dep|
+ # Skip the dependency if it is not in any of the requested groups, or
+ # not for the current platform, or doesn't match the gem constraints.
+ next unless (dep.groups & groups).any? && dep.should_include?
+
+ required_file = nil
+
+ begin
+ # Loop through all the specified autorequires for the
+ # dependency. If there are none, use the dependency's name
+ # as the autorequire.
+ Array(dep.autorequire || dep.name).each do |file|
+ # Allow `require: true` as an alias for `require: <name>`
+ file = dep.name if file == true
+ required_file = file
+ begin
+ Kernel.require file
+ rescue => e
+ raise e if e.is_a?(LoadError) # we handle this a little later
+ raise Bundler::GemRequireError.new e,
+ "There was an error while trying to load the gem '#{file}'."
+ end
+ end
+ rescue LoadError => e
+ REQUIRE_ERRORS.find {|r| r =~ e.message }
+ raise if dep.autorequire || $1 != required_file
+
+ if dep.autorequire.nil? && dep.name.include?("-")
+ begin
+ namespaced_file = dep.name.tr("-", "/")
+ Kernel.require namespaced_file
+ rescue LoadError => e
+ REQUIRE_ERRORS.find {|r| r =~ e.message }
+ raise if $1 != namespaced_file
+ end
+ end
+ end
+ end
+ end
+
+ def self.definition_method(meth)
+ define_method(meth) do
+ raise ArgumentError, "no definition when calling Runtime##{meth}" unless @definition
+ @definition.send(meth)
+ end
+ end
+ private_class_method :definition_method
+
+ definition_method :requested_specs
+ definition_method :specs
+ definition_method :dependencies
+ definition_method :current_dependencies
+ definition_method :requires
+
+ def lock(opts = {})
+ return if @definition.nothing_changed? && !@definition.unlocking?
+ @definition.lock(Bundler.default_lockfile, opts[:preserve_unknown_sections])
+ end
+
+ alias_method :gems, :specs
+
+ def cache(custom_path = nil)
+ cache_path = Bundler.app_cache(custom_path)
+ SharedHelpers.filesystem_access(cache_path) do |p|
+ FileUtils.mkdir_p(p)
+ end unless File.exist?(cache_path)
+
+ Bundler.ui.info "Updating files in #{Bundler.settings.app_cache_path}"
+
+ specs_to_cache = Bundler.settings[:cache_all_platforms] ? @definition.resolve.materialized_for_all_platforms : specs
+ specs_to_cache.each do |spec|
+ next if spec.name == "bundler"
+ next if spec.source.is_a?(Source::Gemspec)
+ spec.source.send(:fetch_gem, spec) if Bundler.settings[:cache_all_platforms] && spec.source.respond_to?(:fetch_gem, true)
+ spec.source.cache(spec, custom_path) if spec.source.respond_to?(:cache)
+ end
+
+ Dir[cache_path.join("*/.git")].each do |git_dir|
+ FileUtils.rm_rf(git_dir)
+ FileUtils.touch(File.expand_path("../.bundlecache", git_dir))
+ end
+
+ prune_cache(cache_path) unless Bundler.settings[:no_prune]
+ end
+
+ def prune_cache(cache_path)
+ SharedHelpers.filesystem_access(cache_path) do |p|
+ FileUtils.mkdir_p(p)
+ end unless File.exist?(cache_path)
+ resolve = @definition.resolve
+ prune_gem_cache(resolve, cache_path)
+ prune_git_and_path_cache(resolve, cache_path)
+ end
+
+ def clean(dry_run = false)
+ gem_bins = Dir["#{Gem.dir}/bin/*"]
+ git_dirs = Dir["#{Gem.dir}/bundler/gems/*"]
+ git_cache_dirs = Dir["#{Gem.dir}/cache/bundler/git/*"]
+ gem_dirs = Dir["#{Gem.dir}/gems/*"]
+ gem_files = Dir["#{Gem.dir}/cache/*.gem"]
+ gemspec_files = Dir["#{Gem.dir}/specifications/*.gemspec"]
+ spec_gem_paths = []
+ # need to keep git sources around
+ spec_git_paths = @definition.spec_git_paths
+ spec_git_cache_dirs = []
+ spec_gem_executables = []
+ spec_cache_paths = []
+ spec_gemspec_paths = []
+ specs.each do |spec|
+ spec_gem_paths << spec.full_gem_path
+ # need to check here in case gems are nested like for the rails git repo
+ md = %r{(.+bundler/gems/.+-[a-f0-9]{7,12})}.match(spec.full_gem_path)
+ spec_git_paths << md[1] if md
+ spec_gem_executables << spec.executables.collect do |executable|
+ e = "#{Bundler.rubygems.gem_bindir}/#{executable}"
+ [e, "#{e}.bat"]
+ end
+ spec_cache_paths << spec.cache_file
+ spec_gemspec_paths << spec.spec_file
+ spec_git_cache_dirs << spec.source.cache_path.to_s if spec.source.is_a?(Bundler::Source::Git)
+ end
+ spec_gem_paths.uniq!
+ spec_gem_executables.flatten!
+
+ stale_gem_bins = gem_bins - spec_gem_executables
+ stale_git_dirs = git_dirs - spec_git_paths - ["#{Gem.dir}/bundler/gems/extensions"]
+ stale_git_cache_dirs = git_cache_dirs - spec_git_cache_dirs
+ stale_gem_dirs = gem_dirs - spec_gem_paths
+ stale_gem_files = gem_files - spec_cache_paths
+ stale_gemspec_files = gemspec_files - spec_gemspec_paths
+
+ removed_stale_gem_dirs = stale_gem_dirs.collect {|dir| remove_dir(dir, dry_run) }
+ removed_stale_git_dirs = stale_git_dirs.collect {|dir| remove_dir(dir, dry_run) }
+ output = removed_stale_gem_dirs + removed_stale_git_dirs
+
+ unless dry_run
+ stale_files = stale_gem_bins + stale_gem_files + stale_gemspec_files
+ stale_files.each do |file|
+ SharedHelpers.filesystem_access(File.dirname(file)) do |_p|
+ FileUtils.rm(file) if File.exist?(file)
+ end
+ end
+ stale_git_cache_dirs.each do |cache_dir|
+ SharedHelpers.filesystem_access(cache_dir) do |dir|
+ FileUtils.rm_rf(dir) if File.exist?(dir)
+ end
+ end
+ end
+
+ output
+ end
+
+ private
+
+ def prune_gem_cache(resolve, cache_path)
+ cached = Dir["#{cache_path}/*.gem"]
+
+ cached = cached.delete_if do |path|
+ spec = Bundler.rubygems.spec_from_gem path
+
+ resolve.any? do |s|
+ s.name == spec.name && s.version == spec.version && !s.source.is_a?(Bundler::Source::Git)
+ end
+ end
+
+ if cached.any?
+ Bundler.ui.info "Removing outdated .gem files from #{Bundler.settings.app_cache_path}"
+
+ cached.each do |path|
+ Bundler.ui.info " * #{File.basename(path)}"
+ File.delete(path)
+ end
+ end
+ end
+
+ def prune_git_and_path_cache(resolve, cache_path)
+ cached = Dir["#{cache_path}/*/.bundlecache"]
+
+ cached = cached.delete_if do |path|
+ name = File.basename(File.dirname(path))
+
+ resolve.any? do |s|
+ source = s.source
+ source.respond_to?(:app_cache_dirname) && source.app_cache_dirname == name
+ end
+ end
+
+ if cached.any?
+ Bundler.ui.info "Removing outdated git and path gems from #{Bundler.settings.app_cache_path}"
+
+ cached.each do |path|
+ path = File.dirname(path)
+ Bundler.ui.info " * #{File.basename(path)}"
+ FileUtils.rm_rf(path)
+ end
+ end
+ end
+
+ def setup_manpath
+ # Store original MANPATH for restoration later in with_clean_env()
+ ENV["BUNDLER_ORIG_MANPATH"] = ENV["MANPATH"]
+
+ # Add man/ subdirectories from activated bundles to MANPATH for man(1)
+ manuals = $LOAD_PATH.map do |path|
+ man_subdir = path.sub(/lib$/, "man")
+ man_subdir unless Dir[man_subdir + "/man?/"].empty?
+ end.compact
+
+ return if manuals.empty?
+ ENV["MANPATH"] = manuals.concat(
+ ENV["MANPATH"].to_s.split(File::PATH_SEPARATOR)
+ ).uniq.join(File::PATH_SEPARATOR)
+ end
+
+ def remove_dir(dir, dry_run)
+ full_name = Pathname.new(dir).basename.to_s
+
+ parts = full_name.split("-")
+ name = parts[0..-2].join("-")
+ version = parts.last
+ output = "#{name} (#{version})"
+
+ if dry_run
+ Bundler.ui.info "Would have removed #{output}"
+ else
+ Bundler.ui.info "Removing #{output}"
+ FileUtils.rm_rf(dir)
+ end
+
+ output
+ end
+
+ def check_for_activated_spec!(spec)
+ return unless activated_spec = Bundler.rubygems.loaded_specs(spec.name)
+ return if activated_spec.version == spec.version
+
+ suggestion = if Bundler.rubygems.spec_default_gem?(activated_spec)
+ "Since #{spec.name} is a default gem, you can either remove your dependency on it" \
+ " or try updating to a newer version of bundler that supports #{spec.name} as a default gem."
+ else
+ "Prepending `bundle exec` to your command may solve this."
+ end
+
+ e = Gem::LoadError.new "You have already activated #{activated_spec.name} #{activated_spec.version}, " \
+ "but your Gemfile requires #{spec.name} #{spec.version}. #{suggestion}"
+ e.name = spec.name
+ if e.respond_to?(:requirement=)
+ e.requirement = Gem::Requirement.new(spec.version.to_s)
+ else
+ e.version_requirement = Gem::Requirement.new(spec.version.to_s)
+ end
+ raise e
+ end
+ end
+end