diff options
-rw-r--r-- | railties/guides/source/initialization.textile | 553 |
1 files changed, 3 insertions, 550 deletions
diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 19a4d5157e..14849fdd57 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -1792,556 +1792,9 @@ Now that Rails has finished loading all the Railties by way of +require 'rails/a NOTE: It is worth mentioning here that you are not tied to using Bundler with Rails 3, but it is (of course) advised that you do. To "turn off" Bundler, comment out or remove the corresponding lines in _config/application.rb_ and _config/boot.rb_. -Bundler was +require+'d back in _config/boot.rb_ and now we'll dive into the internals of Bundler to determine precisely what this line accomplishes. - -h4. +Bundler.require+ - -+Bundler.require+ is defined in _lib/bundler.rb_: - -<ruby> - def require(*groups) - gemfile = default_gemfile - load(gemfile).require(*groups) - end -</ruby> - -The +groups+ variable here would be a two-element array of the arguments passed to +Bundler.require+. In this case we're going to assume, +Rails.env+ is +"development"+. - -h4. Locating the Gemfile - -+default_gemfile+ is defined in _lib/bundler.rb_ and makes a call out to the +SharedHelpers+ module: - -<ruby> - def default_gemfile - SharedHelpers.default_gemfile - end -</ruby> - -+SharedHelpers+ defines +default_gemfile+ like this: - -<ruby> - def default_gemfile - gemfile = find_gemfile - gemfile or raise GemfileNotFound, "The default Gemfile was not found" - Pathname.new(gemfile) - end -</ruby> - -+find_gemfile+ is defined like this: - -<ruby> - def find_gemfile - return ENV['BUNDLE_GEMFILE'] if ENV['BUNDLE_GEMFILE'] - - previous = nil - current = File.expand_path(Dir.pwd) - - until !File.directory?(current) || current == previous - filename = File.join(current, 'Gemfile') - return filename if File.file?(filename) - current, previous = File.expand_path("..", current), current - end - end -</ruby> - -The first line of course means if you define the environment variable +BUNDLE_GEMFILE+ this is the name of the file that will be used and returned. If not, then Bundler will look for a file called _Gemfile_ in the current directory and if it can find it then it will return the filename. If it cannot, it will recurse up the directory structure until it does. Once the file is found a +Pathname+ is made from the expanded path to _Gemfile_. - -If the file cannot be found at all then +GemfileNotFound+ will be raised back in +default_gemfile+. - -h4. Loading the Gemfile - -Now that Bundler has determined what the _Gemfile_ is, it goes about loading it: - -<ruby> - def require(*groups) - gemfile = default_gemfile - load(gemfile).require(*groups) - end -</ruby> - -+load+ is defined like this in _lib/bundler.rb_: - -<ruby> - def load(gemfile = default_gemfile) - root = Pathname.new(gemfile).dirname - Runtime.new root, definition(gemfile) - end -</ruby> - -The next method to be called here would be +definition+ and it is defined like this: - -<ruby> - def definition(gemfile = default_gemfile) - configure - root = Pathname.new(gemfile).dirname - lockfile = root.join("Gemfile.lock") - if lockfile.exist? - Definition.from_lock(lockfile) - else - Definition.from_gemfile(gemfile) - end - end -</ruby> - -+configure+ is responsible for setting up the path to gem home and gem path: - -<ruby> - def configure - @configured ||= begin - configure_gem_home_and_path - true - end - end -</ruby> - -+configure_gem_home_and_path+ defined like this: - -<ruby> - def configure_gem_home_and_path - if settings[:disable_shared_gems] - ENV['GEM_HOME'] = File.expand_path(bundle_path, root) - ENV['GEM_PATH'] = '' - else - gem_home, gem_path = Gem.dir, Gem.path - ENV["GEM_PATH"] = [gem_home, gem_path].flatten.compact.join(File::PATH_SEPARATOR) - ENV["GEM_HOME"] = bundle_path.to_s - end - - Gem.clear_paths - end -</ruby> - -We do not have +settings[:disabled_shared_gems]+ set to true so this will execute the code under the +else+. The +ENV["GEM_PATH"]+ will resemble +/usr/local/lib/ruby/gems/1.9.1:/home/you/.gem/ruby/1.9.1+ - -And +ENV["GEM_HOME"]+ will be the path to the gems installed into your home directory by Bundler, something resembling +/home/you/.bundle/ruby/1.9.1+. - -After +configure_gem_home_and_path+ is done the +definition+ method goes about creating a +Definition+ from either +Gemfile.lock+ if it exists, or the +gemfile+ previously located. +Gemfile.lock+ only exists if +bundle lock+ has been ran and so far it has not. - -+Definition.from_gemfile+ is defined in _lib/bundler/definition.rb_: - -<ruby> - def self.from_gemfile(gemfile) - gemfile = Pathname.new(gemfile).expand_path - - unless gemfile.file? - raise GemfileNotFound, "#{gemfile} not found" - end - - Dsl.evaluate(gemfile) - end -</ruby> - -Now that the +gemfile+ is located +Dsl.evaluate+ goes about loading it. The code for this can be found in _lib/bundler/dsl.rb_: - -<ruby> - def self.evaluate(gemfile) - builder = new - builder.instance_eval(File.read(gemfile.to_s), gemfile.to_s, 1) - builder.to_definition - end -</ruby> - -+new+ here will, of course, call +initialize+ which sets up a couple of variables: - -<ruby> - def initialize - @source = nil - @sources = [] - @dependencies = [] - @group = nil - end -</ruby> - -When Bundler calls +instance_eval+ on the new +Bundler::Dsl+ object it evaluates the content of the +gemfile+ file within the context of this instance. The Gemfile for a default Rails 3 project with all the comments stripped out looks like this: - -<ruby> - source 'http://rubygems.org' - - gem 'rails', '3.0.0.beta1' - - # Bundle edge Rails instead: - # gem 'rails', :git => 'git://github.com/rails/rails.git' - - gem 'sqlite3-ruby', :require => 'sqlite3' -</ruby> - -When Bundler loads this file it firstly calls the +source+ method on the +Bundler::Dsl+ object: - -<ruby> - def source(source, options = {}) - @source = case source - when :gemcutter, :rubygems, :rubyforge then Source::Rubygems.new("uri" => "http://gemcutter.org") - when String then Source::Rubygems.new("uri" => source) - else source - end - - options[:prepend] ? @sources.unshift(@source) : @sources << @source - - yield if block_given? - @source - ensure - @source = nil - end -</ruby> - -TODO: Perhaps make this a side-note. However you do that. - -The interesting thing to note about this method is that it takes a block, so you may do: - -<ruby> - source 'http://someothergemhost.com' do - gem 'your_favourite_gem' - end -</ruby> - -if you wish to install _your_favourite_gem_ from _http://someothergemhost.com_. - -In this instance however a block is not specified so this sets up the +@source+ instance variable to be +'http://rubygems.org'+. - -The next method that is called is +gem+: - -<ruby> - def gem(name, *args) - options = Hash === args.last ? args.pop : {} - version = args.last || ">= 0" - if options[:group] - options[:group] = options[:group].to_sym - end - - _deprecated_options(options) - _normalize_options(name, version, options) - - @dependencies << Dependency.new(name, version, options) - end -</ruby> - -This sets up a couple of important things initially. If you specify a gem like the following: - -<ruby> - gem 'rails', "2.3.4" -</ruby> - -This sets +options+ to be an empty hash, but +version+ to be +"2.3.4"+. TODO: How does one pass options and versions at the same time? - -In the Gemfile for a default Rails project, the first +gem+ line is: - -<ruby> - gem 'rails', '3.0.0.beta2' -</ruby> - -TODO: change version number. - -This line will check that +options+ contains no deprecated options by using the +_deprecated_options+ method, but the +options+ hash is empty. This is of course until +_normalize_options+ has its way: - -<ruby> - def _normalize_options(name, version, opts) - _normalize_hash(opts) - - group = opts.delete("group") || @group - - # Normalize git and path options - ["git", "path"].each do |type| - if param = opts[type] - options = _version?(version) ? opts.merge("name" => name, "version" => version) : opts.dup - source = send(type, param, options, :prepend => true) - opts["source"] = source - end - end - - opts["source"] ||= @source - - opts["group"] = group - end -</ruby> - -+_normalize_hash+ will convert all the keys in the +opts+ hash to strings. There is neither a +git+ or a +path+ key in the +opts+ hash so the next couple of lines are ignored, then the +source+ and +group+ keys are set up. - -TODO: Maybe it is best to cover what would happen in the case these lines did exist? - -The next line goes about defining a dependency for this gem: - -<ruby> - @dependencies << Dependency.new(name, version, options) -</ruby> - -This class is defined like this: - -<ruby> - module Bundler - class Dependency < Gem::Dependency - attr_reader :autorequire - attr_reader :groups - - def initialize(name, version, options = {}, &blk) - super(name, version) - - @autorequire = nil - @groups = Array(options["group"] || :default).map { |g| g.to_sym } - @source = options["source"] - - if options.key?('require') - @autorequire = Array(options['require'] || []) - end - end - end - end -</ruby> - -The +initialize+ method in +Gem::Dependency+ is defined: - -<ruby> - def initialize(name, version_requirements, type=:runtime) - @name = name - unless TYPES.include? type - raise ArgumentError, "Valid types are #{TYPES.inspect}, not #{@type.inspect}" - end - @type = type - @version_requirements = Gem::Requirement.create version_requirements - @version_requirement = nil # Avoid warnings. - end -</ruby> - -The +version_requirements+ that was passed in here will be inspected by +Gem::Requirement.create+ and return, for our +3.0.0beta2+ version string a +Gem::Requirement+ object: - -<ruby> - #<Gem::Requirement:0x101dd8c20 @requirements=[["=", #<Gem::Version "3.0.0beta2">]]> -</ruby> - -Going back to +Bundler::Dependency+, the next line simply sets +@autorequire+ to +nil+ and the next line is a little more interesting: - -<ruby> - @autorequire = nil - @groups = Array(options["group"] || :default).map { |g| g.to_sym } -</ruby> - -Here, bundler sets the +groups+ variable to be whatever +group+ we've set for this gem and also demonstrates through code that the +group+ option allows for multiple groups, so in the _Gemfile_ you can specify the same gem for multiple groups: - -<ruby> - group :test, :cucumber do - gem 'faker' - end -</ruby> - -The final lines in +initialize+ work on the +require+ option which is not passed: - -<ruby> - if options.key?('require') - @autorequire = Array(options['require'] || []) - end -</ruby> - -If it were to be used in the _Gemfile_, it would look like this: - -<ruby> - gem 'thinking-sphinx', :require => "thinking_sphinx" -</ruby> - -So far, this is what simply loading the _Gemfile_ does. - -h3. Bring forth the gems - -Now that the _Gemfile_ has finished being parsed, the next line is: - -<ruby> - builder.to_definition -</ruby> - -This method is defined in _lib/bundler/dsl.rb_ and does this: - -<ruby> - def to_definition - Definition.new(@dependencies, @sources) - end -</ruby> - -The +Bundler::Definition#initialize+ method is this: - -<ruby> - def initialize(dependencies, sources) - @dependencies = dependencies - @sources = sources - end -</ruby> - -Now Bundler has a +Bundler::Definition+ object to be passed back to the +load+ method from _lib/bundler.rb_: - -<ruby> - def load(gemfile = default_gemfile) - root = Pathname.new(gemfile).dirname - Runtime.new root, definition(gemfile) - end -</ruby> - -The +Bundler::Runtime+ class inherits from +Bundler::Environment+ and the reason this is pointed out is because +super+ is used in the +initialize+ method in +Bundler::Runtime+: - -<ruby> - super - if locked? - write_rb_lock - end -</ruby> - -Thankfully, the +Bundler::Environment#initialize+ method is nothing too complex: - -<ruby> - def initialize(root, definition) - @root = root - @definition = definition - end -</ruby> - -The +locked?+ method checks if the _Gemfile.lock_ or _.bundler/environment.rb_ files exist: - -<ruby> - def locked? - File.exist?("#{root}/Gemfile.lock") || File.exist?("#{root}/.bundle/environment.rb") - end -</ruby> - -And if they do will call +write_rb_lock+: - -<ruby> - def write_rb_lock - shared_helpers = File.read(File.expand_path("../shared_helpers.rb", __FILE__)) - template = File.read(File.expand_path("../templates/environment.erb", __FILE__)) - erb = ERB.new(template, nil, '-') - FileUtils.mkdir_p(rb_lock_file.dirname) - File.open(rb_lock_file, 'w') do |f| - f.puts erb.result(binding) - end - end -</ruby> - -This will write out to _.bundler/environment.rb_ the state of the current environment. - -Now a quick refresher. Bundler is still evaulating the code for the +require+ in _lib/bundler.rb_, and the +groups+ variable here is an +Array+ containing two elements: +:default+ and the current Rails environment: +development+: - -<ruby> - def require(*groups) - gemfile = default_gemfile - load(gemfile).require(*groups) - end -</ruby> - -The +load+ method returns a +Bundler::Runtime+ object. The second +require+ method here: - -<ruby> - load(gemfile).require(*groups) -</ruby> - -Is defined on _bundler/runtime.rb_: - -<ruby> - def require(*groups) - groups.map! { |g| g.to_sym } - groups = [:default] if groups.empty? - autorequires = autorequires_for_groups(*groups) - - groups.each do |group| - (autorequires[group] || [[]]).each do |path, explicit| - if explicit - Kernel.require(path) - else - begin - Kernel.require(path) - rescue LoadError - end - end - end - end - end -</ruby> - -This method does TODO: Describe what magic this undertakes when you've gone through the rest of the source. - -The first method to be called here is +autorequires_for_groups+ and this method is defined in _bundler/environment.rb_: - -<ruby> - def autorequires_for_groups(*groups) - groups.map! { |g| g.to_sym } - autorequires = Hash.new { |h,k| h[k] = [] } - - ordered_deps = [] - specs_for(*groups).each do |g| - dep = @definition.dependencies.find{|d| d.name == g.name } - ordered_deps << dep if dep && !ordered_deps.include?(dep) - end -</ruby> - -The +specs_for+ method looks like this: - -<ruby> - def specs_for(groups) - deps = dependencies.select { |d| (d.groups & groups).any? } - specs.for(deps) - end -</ruby> - -The first line here: - -<ruby> - deps = dependencies.select { |d| (d.groups & groups).any? } -</ruby> - -Goes through all the +dependencies+ defined and checks to see if their +groups+ match any of the +groups+ defined and returns the ones that are for the groups that are requested. - -The next line firstly calls the +specs+ method: - -<ruby> - specs.for(deps) -</ruby> - -Which is defined like this: - -<ruby> - def specs - @specs ||= resolve_locally || resolve_remotely - end -</ruby> - -This attempts to resolve the dependencies locally, and if it cannot then it will attempt to resolve them remotely. Firstly, `resolve_locally`: - -<ruby> - def resolve_locally - resolve(:local_specs, index) - end -</ruby> - -The resolve method is defined like this: - -<ruby> - def resolve(type, index) - source_requirements = {} - actual_dependencies.each do |dep| - next unless dep.source && dep.source.respond_to?(type) - source_requirements[dep.name] = dep.source.send(type) - end - - # Run a resolve against the locally available gems - Resolver.resolve(actual_dependencies, index, source_requirements) - end -</ruby> - -The +actual_dependencies+ referenced is defined back in _bundler/runtime.rb_: - -<ruby> - def actual_dependencies - @definition.actual_dependencies - end -</ruby> - -And the +@definition+ here is a +Bundler::Definition+ object, so that means the +actual_dependencies+ method called here is defined in _bundler/definition.rb_: - -<ruby> - def actual_dependencies - @actual_dependencies ||= @details["specs"].map do |args| - name, details = args.to_a.flatten - details["source"] = sources[details["source"]] if details.include?("source") - Bundler::Dependency.new(name, details.delete("version"), details) - end - end -</ruby> +Bundler was +require+'d back in _config/boot.rb_, and so that is what makes it available here. This guide does not dive into the internals of Bundler; it's really it's own separate guide. +The +Bundler.require+ method adds all the gems not specified inside a +group+ in the +Gemfile+ and the ones specified in groups for the +Rails.env+ (in this case, _development_), to the load path. This is how an application is able to find them. @@ -2712,7 +2165,7 @@ The method +find_with_root_flag+ is defined on +Rails::Engine+ (the superclass o root = File.exist?("#{root_path}/#{flag}") ? root_path : default raise "Could not find root path for #{self}" unless root - RUBY_PLATFORM =~ /mswin|mingw/ ? + RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? Pathname.new(root).expand_path : Pathname.new(root).realpath end </ruby> |