¿Cómo funciona RubyGems?
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á…