diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 6cdbe95887..eb86a1fb83 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -1,6 +1,142 @@ +# The Formulary is responsible for creating instances +# of Formula. class Formulary - # Return a Formula instance for the given `name`. - # `name` may be: + + def self.formula_class_defined? formula_name + Object.const_defined?(Formula.class_s(formula_name)) + end + + def self.get_formula_class formula_name + Object.const_get(Formula.class_s(formula_name)) + end + + # A FormulaLoader returns instances of formulae. + # Subclasses implement loaders for particular sources of formulae. + class FormulaLoader + # The formula's name + attr_reader :name + # The formula's ruby file's path or filename + attr_reader :path + + # Gets the formula instance. + # Subclasses must define this. + def get_formula; end + + # Return the Class for this formula, `require`-ing it if + # it has not been parsed before. + def klass + unless Formulary.formula_class_defined? name + puts "#{$0}: loading #{path}" if ARGV.debug? + begin + require path.to_s + rescue NoMethodError + # This is a programming error in an existing formula, and should not + # have a "no such formula" message. + raise + rescue LoadError, NameError + # TODO - show exception details + raise FormulaUnavailableError.new(name) + end + end + klass = Formulary.get_formula_class(name) + if (klass == Formula) || !klass.ancestors.include?(Formula) + raise FormulaUnavailableError.new(name) + end + klass + end + end + + # Loads formulae from bottles. + class BottleLoader < FormulaLoader + def initialize bottle_name + @bottle_filename = Pathname(bottle_name).realpath + version = Version.parse(@bottle_filename).to_s + bottle_basename = @bottle_filename.basename.to_s + name_without_version = bottle_basename.rpartition("-#{version}").first + if name_without_version.empty? + if ARGV.homebrew_developer? + opoo "Add a new version regex to version.rb to parse this filename." + end + @name = name + else + @name = name_without_version + end + @path = Formula.path(@name) + end + + def get_formula + formula = klass.new(name) + formula.downloader.local_bottle_path = @bottle_filename + return formula + end + end + + # Loads formulae from Homebrew's provided Library + class StandardLoader < FormulaLoader + def initialize name + @name = name + @path = Formula.path(name) + end + + def get_formula + return klass.new(name) + end + end + + # Loads formulae from disk using a path + class FromPathLoader < FormulaLoader + def initialize path + # require allows filenames to drop the .rb extension, but everything else + # in our codebase will require an exact and fullpath. + path = "#{name}.rb" unless path =~ /\.rb$/ + + @path = Pathname.new(path) + @name = @path.stem + end + + def get_formula + klass.new(name, path.to_s) + end + end + + # Loads formulae from URLs. + class FromUrlLoader < FormulaLoader + attr_reader :url + + def initialize url + @url = url + @path = (HOMEBREW_CACHE_FORMULA/(File.basename(url))) + @name = File.basename(url, '.rb') + end + + # Downloads the formula's .rb file + def fetch + unless Formulary.formula_class_defined? name + HOMEBREW_CACHE_FORMULA.mkpath + FileUtils.rm path.to_s, :force => true + curl url, '-o', path.to_s + end + end + + def get_formula + return klass.new(name, path.to_s) + end + end + + # Loads tapped formulae. + class TapLoader < FormulaLoader + def initialize tapped_name + @name = tapped_name + @path = Pathname.new(tapped_name) + end + + def get_formula + klass.new(tapped_name, path.to_s) + end + end + + # Return a Formula instance for the given reference. + # `ref` may be: # * a Formula instance, in which case it is returned # TODO: is this code path used? # * a Pathname to a local formula @@ -8,99 +144,35 @@ class Formulary # * a string containing a formula URL # * a string containing a formula name # * a string containing a local bottle reference - def self.factory name + def self.factory ref # If an instance of Formula is passed, just return it - return name if name.kind_of? Formula + return ref if ref.kind_of? Formula # Otherwise, convert to String in case a Pathname comes in - name = name.to_s + # TODO - do we call with a Pathname instead of a string anywhere? + ref = ref.to_s # If a URL is passed, download to the cache and install - if name =~ %r[(https?|ftp)://] - url = name - name = Pathname.new(name).basename - path = HOMEBREW_CACHE_FORMULA+name - name = name.basename(".rb").to_s - - unless Object.const_defined? Formula.class_s(name) - HOMEBREW_CACHE_FORMULA.mkpath - FileUtils.rm path, :force => true - curl url, '-o', path - end - - install_type = :from_url - elsif name.match bottle_regex - bottle_filename = Pathname(name).realpath - version = Version.parse(bottle_filename).to_s - bottle_basename = bottle_filename.basename.to_s - name_without_version = bottle_basename.rpartition("-#{version}").first - if name_without_version.empty? - if ARGV.homebrew_developer? - opoo "Add a new version regex to version.rb to parse this filename." - end - else - name = name_without_version - end - path = Formula.path(name) - install_type = :from_local_bottle + if ref =~ %r[(https?|ftp)://] + f = FromUrlLoader.new(ref) + f.fetch + elsif ref =~ Pathname::BOTTLE_EXTNAME_RX + f = BottleLoader.new(ref) else - name = Formula.canonical_name(name) - - if name =~ %r{^(\w+)/(\w+)/([^/])+$} + name_or_path = Formula.canonical_name(ref) + if name_or_path =~ %r{^(\w+)/(\w+)/([^/])+$} # name appears to be a tapped formula, so we don't munge it # in order to provide a useful error message when require fails. - path = Pathname.new(name) - elsif name.include? "/" + f = TapLoader.new(name_or_path) + elsif name_or_path.include? "/" # If name was a path or mapped to a cached formula - - # require allows filenames to drop the .rb extension, but everything else - # in our codebase will require an exact and fullpath. - name = "#{name}.rb" unless name =~ /\.rb$/ - - path = Pathname.new(name) - name = path.stem - install_type = :from_path + f = FromPathLoader.new(name_or_path) else # For names, map to the path and then require - path = Formula.path(name) - install_type = :from_name + f = StandardLoader.new(name_or_path) end end - klass_name = Formula.class_s(name) - unless Object.const_defined? klass_name - puts "#{$0}: loading #{path}" if ARGV.debug? - require path - end - - begin - klass = Object.const_get klass_name - rescue NameError - # TODO really this text should be encoded into the exception - # and only shown if the UI deems it correct to show it - onoe "class \"#{klass_name}\" expected but not found in #{name}.rb" - puts "Double-check the name of the class in that formula." - raise LoadError - end - - if install_type == :from_local_bottle - formula = klass.new(name) - formula.downloader.local_bottle_path = bottle_filename - return formula - end - - raise NameError if !klass.ancestors.include? Formula - raise NameError if klass == Formula - - return klass.new(name) if install_type == :from_name - return klass.new(name, path.to_s) - rescue NoMethodError - # This is a programming error in an existing formula, and should not - # have a "no such formula" message. - raise - rescue LoadError, NameError - # Catch NameError so that things that are invalid symbols still get - # a useful error message. - raise FormulaUnavailableError.new(name) + f.get_formula end end