Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
formulary.rb 5.89 KiB
# The Formulary is responsible for creating instances of Formula.
# It is not meant to be used directy from formulae.

class Formulary

  def self.unload_formula formula_name
    Object.send(:remove_const, class_s(formula_name))
  end

  def self.formula_class_defined? class_name
    Object.const_defined?(class_name)
  end

  def self.get_formula_class class_name
    Object.const_get(class_name)
  end

  def self.restore_formula formula_name, value
    old_verbose, $VERBOSE = $VERBOSE, nil
    Object.const_set(class_s(formula_name), value)
  ensure
    $VERBOSE = old_verbose
  end

  def self.class_s name
    class_name = name.capitalize
    class_name.gsub!(/[-_.\s]([a-zA-Z0-9])/) { $1.upcase }
    class_name.gsub!('+', 'x')
    class_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
    # The ruby constant name of the formula's class
    attr_reader :class_name

    def initialize(name, path)
      @name = name
      @path = path.resolved_path
      @class_name = Formulary.class_s(name)
    end

    # Gets the formula instance.
    def get_formula
      klass.new(name, path)
    end

    # Return the Class for this formula, `require`-ing it if
    # it has not been parsed before.
    def klass
      begin
        have_klass = Formulary.formula_class_defined? class_name
      rescue NameError
        raise FormulaUnavailableError.new(name)
      end

      unless have_klass
        STDERR.puts "#{$0} (#{self.class.name}): loading #{path}" if ARGV.debug?
        begin
          require path
        rescue NoMethodError
          # This is a programming error in an existing formula, and should not
          # have a "no such formula" message.
          raise
        rescue LoadError, NameError
          raise if ARGV.debug?  # let's see the REAL error
          raise FormulaUnavailableError.new(name)
        end
      end

      klass = Formulary.get_formula_class(class_name)
      if klass == Formula || !(klass < 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
      name_without_version = bottle_filename_formula_name @bottle_filename
      if name_without_version.empty?
        if ARGV.homebrew_developer?
          opoo "Add a new regex to bottle_version.rb to parse this filename."
        end
        name = bottle_name
      else
        name = name_without_version
      end

      super name, Formula.path(name)
    end

    def get_formula
      formula = super
      formula.local_bottle_path = @bottle_filename
      formula
    end
  end

  # Loads formulae from Homebrew's provided Library
  class StandardLoader < FormulaLoader
    def initialize name, path=Formula.path(name)
      super
    end
  end

  class AliasLoader < FormulaLoader
    def initialize alias_path
      path = alias_path.resolved_path
      name = path.basename(".rb").to_s
      super name, path
    end
  end

  # Loads formulae from disk using a path
  class FromPathLoader < FormulaLoader
    def initialize path
      path = Pathname.new(path).expand_path
      super path.basename(".rb").to_s, path
    end
  end

  # Loads formulae from URLs
  class FromUrlLoader < FormulaLoader
    attr_reader :url

    def initialize url
      @url = url
      uri = URI(url)
      formula = File.basename(uri.path, ".rb")
      super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri.path)
    end

    # Downloads the formula's .rb file
    def fetch
      begin
        have_klass = Formulary.formula_class_defined? class_name
      rescue NameError
        raise FormulaUnavailableError.new(name)
      end

      unless have_klass
        HOMEBREW_CACHE_FORMULA.mkpath
        FileUtils.rm path.to_s, :force => true
        curl url, '-o', path.to_s
      end
    end

    def get_formula
      fetch
      super
    end
  end

  # Loads tapped formulae.
  class TapLoader < FormulaLoader
    attr_reader :tapped_name

    def initialize tapped_name
      @tapped_name = tapped_name
      user, repo, name = tapped_name.split("/", 3).map(&:downcase)
      tap = Pathname.new("#{HOMEBREW_LIBRARY}/Taps/#{user}/homebrew-#{repo}")
      path = tap.join("#{name}.rb")

      if tap.directory?
        tap.find_formula do |file|
          if file.basename(".rb").to_s == name
            path = file
          end
        end
      end

      super name, path
    end

    def get_formula
      super
    rescue FormulaUnavailableError
      raise TapFormulaUnavailableError.new(tapped_name)
    end
  end

  # Return a Formula instance for the given reference.
  # `ref` is string containing:
  # * a formula name
  # * a formula pathname
  # * a formula URL
  # * a local bottle reference
  def self.factory ref
    loader_for(ref).get_formula
  end

  def self.canonical_name(ref)
    loader_for(ref).name
  end

  def self.loader_for(ref)
    case ref
    when %r[(https?|ftp)://]
      return FromUrlLoader.new(ref)
    when Pathname::BOTTLE_EXTNAME_RX
      return BottleLoader.new(ref)
    when HOMEBREW_TAP_FORMULA_REGEX
      return TapLoader.new(ref)
    end

    if File.extname(ref) == ".rb"
      return FromPathLoader.new(ref)
    end

    formula_with_that_name = Formula.path(ref)
    if formula_with_that_name.file?
      return StandardLoader.new(ref, formula_with_that_name)
    end

    possible_alias = Pathname.new("#{HOMEBREW_LIBRARY}/Aliases/#{ref}")
    if possible_alias.file?
      return AliasLoader.new(possible_alias)
    end

    possible_cached_formula = Pathname.new("#{HOMEBREW_CACHE_FORMULA}/#{ref}.rb")
    if possible_cached_formula.file?
      return StandardLoader.new(ref, possible_cached_formula)
    end

    return StandardLoader.new(ref)
  end
end