Gastón Ramos

Blog personal

← Volver al blog

¿Cómo funciona RubyGems?

Recuperado del archivo original de WordPress.

Desde hace un tiempo que estoy interesado en conocer un poco más en detalle como funciona rubygems, así que hoy decidí comenzar a aprender más sobre rubygems y cómo las gemas son cargadas en memoria.

Bueno, comencemos: Entro al intérprete interactivo de ruby e intento requerir la bioblioteca activemodel:


ruby-1.9.2-p136 :001 > Gem.loaded_specs
 => {}
ruby-1.9.2-p136 :002 > require 'active_model'
 => true
ruby-1.9.2-p136 :003 > Gem.loaded_specs
 => {"activemodel"=>#<Gem::Specification name=activemodel version=3.0.4>,
"activesupport"=>#<Gem::Specification name=activesupport version=3.0.4>,
"builder"=>#<Gem::Specification name=builder version=2.1.2>,
"i18n"=>#<Gem::Specification name=i18n version=0.5.0>}

Cómo podemos observar aparecen varias gemas nuevas cargadas, active_model y sus dependencias, pero ¿cómo es que esto se lleva cabo?

Bien si nos fijamos en /lib/rubygems.rb en el repo de rubygems al final de todo hace esto:


require 'rubygems/custom_require' unless Gem::GEM_PRELUDE_SUCKAGE

y si vamos al archivo lib/rubygems/custom_require.rb vemos que lo hace es redefinir el método require de Kernel, y activa la gema de esta forma:


Gem.try_activate(path)

podemos probar esto directamente en el intérprete:


ruby-1.9.2-p136 > Gem.try_activate 'active_resource'
=> true
ruby-1.9.2-p136 :007 > Gem.loaded_specs['activeresource']
 => #<Gem::Specification name=activeresource version=3.0.4>

Entonces vamos de nuevo al archivo rubygems.rb y miramos como es que funciona Gem.try_activate:

Lo primero que hace es buscar la gema que estamos intentando requerir, y lo hace así:


 spec = Gem.searcher.find path

Gem.searcher es una instancia de Gem::GemPathSearcher entre otras cosas tiene la lista de todas las gemspecs instaldas para después poder hacér búsqueda con Gem::GemPathSearcher#find. Entonces siguiendo con Gem.try_activate, si encuentra alguna coincidencia ejecuta

Gem.activate spec.name, "= #{spec.version}"

como podemos ver saca la versión de la spec encontrada (que es la más alta instalada), entonces ahora vamos a ver

Gem.activate

Éste método retorna true si la gema fué activada, false si ya está cargada o una excepción en cualquier otro caso (no se pudo cargar por algún motivo). La manera en que activa la gema es agregando el path de la misma al $LOAD_PATH de ruby.


 def self.activate(gem, *requirements)
   if requirements.last.is_a?(Hash)
      options = requirements.pop
   else
      options = {}
   end

# Esta línea parsea los requerimientos que vienen como argumento # y si no viene ninguno pone los defaults, los requerimientos no son # otra cosa que restricciones de la versión de la gema, # el requerimiento por defecto es >= 0

requirements = Gem::Requirement.default if requirements.empty?

# Básicamente Gem::Dependency es un contenedor de # Gem name, requirement

dep = Gem::Dependency.new(gem, requirements)

sources = options[:sources] || []

# creo que esta línea require especial antención # (veremos los detalles más adelante) pero por ahora nos # alcanza con saber que Gem._unresolved crea una lista # de objetos Gem::DependencyList (que primero está vacía) # de dependecias sin resolver

matches = _unresolved.find_all { |spec| spec.satisfies_requirement? dep }

if matches.empty? then matches = Gem.source_index.find_name(dep.name, dep.requirement)

# Si no ecuentra la gema buscando con el nombre y los # requerimientos reporta el error report_activate_error(dep) if matches.empty?

# Si la gema ya está cargada (cualquier versión) if @loaded_specs[dep.name] then # This gem is already loaded. If the currently loaded gem is not in the # list of candidate gems, then we have a version conflict. existing_spec = @loaded_specs[dep.name]

# Pregunta si la gema que está cargada es la misma versión # que la quiero activar unless matches.any? { |spec| spec.version == existing_spec.version } then sources_message = sources.map { |spec| spec.full_name } stack_message = @loaded_stacks[dep.name].map { |spec| spec.full_name }

msg = "can't activate #{dep} for #{sources_message.inspect}, " msg << "already activated #{existing_spec.full_name} for " msg << "#{stack_message.inspect}"

e = Gem::LoadError.new msg e.name = dep.name e.requirement = dep.requirement

raise e end

# La gema que quiero cargar ya está cargada return false end end

# La gema no está cargada entonces activa la última versión spec = matches.last

conf = spec.conflicts unless conf.empty? then why = conf.map { |k,v| "#{k} depends on #{v.join(", ")}" }.join ", "

raise LoadError, "Unable to activate #{spec.full_name}, but #{why}" end

return false if spec.loaded?

spec.loaded = true @loaded_specs[spec.name] = spec @loaded_stacks[spec.name] = sources.dup

spec.runtime_dependencies.each do |spec_dep| next if Gem.loaded_specs.include? spec_dep.name specs = Gem.source_index.search spec_dep, true

_unresolved.add(*specs) end

current = Hash.new { |h, name| h[name] = Gem::Dependency.new name } Gem.loaded_specs.each do |n,s| s.runtime_dependencies.each do |s_dep| current[s_dep.name] = current[s_dep.name].merge s_dep end end

_unresolved.remove_specs_unsatisfied_by current

require_paths = spec.require_paths.map do |path| File.join spec.full_gem_path, path end

# gem directories must come after -I and ENV['RUBYLIB'] insert_index = load_path_insert_index

if insert_index then # gem directories must come after -I and ENV['RUBYLIB'] $LOAD_PATH.insert(insert_index, *require_paths) else # we are probably testing in core, -I and RUBYLIB don't apply $LOAD_PATH.unshift(*require_paths) end

return true end

Si alguien encuentra algún en error en mi seguimiento hasta acá le pido que haga un comentario en este post, Continuará...

También puede interesarte

Los comentarios históricos se conservan como archivo. Para conversar hoy, podés escribirme.