brew/Library/Homebrew/formulary.rb

382 lines
11 KiB
Ruby
Raw Normal View History

require "digest/md5"
require "tap"
# The Formulary is responsible for creating instances of Formula.
2016-05-22 21:29:22 +01:00
# It is not meant to be used directly from formulae.
2014-05-02 07:39:23 -07:00
class Formulary
FORMULAE = {}
2015-01-01 01:21:59 -05:00
def self.formula_class_defined?(path)
FORMULAE.key?(path)
2013-06-18 10:11:06 -07:00
end
def self.formula_class_get(path)
FORMULAE.fetch(path)
2013-06-18 10:11:06 -07:00
end
def self.load_formula(name, path, contents, namespace)
if ENV["HOMEBREW_DISABLE_LOAD_FORMULA"]
raise "Formula loading disabled by HOMEBREW_DISABLE_LOAD_FORMULA!"
end
mod = Module.new
const_set(namespace, mod)
2015-06-06 18:10:47 -04:00
mod.module_eval(contents, path)
class_name = class_s(name)
begin
mod.const_get(class_name)
rescue NameError => original_exception
class_list = mod.constants
.map { |const_name| mod.const_get(const_name) }
.select { |const| const.is_a?(Class) }
e = FormulaClassUnavailableError.new(name, path, class_name, class_list)
raise e, "", original_exception.backtrace
end
end
def self.load_formula_from_path(name, path)
contents = path.open("r") { |f| set_encoding(f).read }
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(path.to_s)}"
klass = load_formula(name, path, contents, namespace)
FORMULAE[path] = klass
end
2015-06-06 18:10:47 -04:00
if IO.method_defined?(:set_encoding)
def self.set_encoding(io)
io.set_encoding(Encoding::UTF_8)
end
else
def self.set_encoding(io)
io
end
end
def self.class_s(name)
class_name = name.capitalize
class_name.gsub!(/[-_.\s]([a-zA-Z0-9])/) { $1.upcase }
class_name.tr!("+", "x")
2016-08-29 16:00:40 +01:00
class_name.sub!(/(.)@(\d)/, "\\1AT\\2")
class_name
2014-02-21 00:43:58 -05:00
end
2013-06-18 10:11:06 -07:00
# 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 name used to install the formula
attr_reader :alias_path
def initialize(name, path)
@name = name
@path = path.resolved_path
end
2013-06-18 10:11:06 -07:00
# Gets the formula instance.
def get_formula(spec)
klass.new(name, path, spec, alias_path: alias_path)
end
2013-06-18 10:11:06 -07:00
def klass
load_file unless Formulary.formula_class_defined?(path)
Formulary.formula_class_get(path)
2013-06-18 10:11:06 -07:00
end
2014-12-29 14:53:22 -05:00
private
def load_file
$stderr.puts "#{$0} (#{self.class.name}): loading #{path}" if ARGV.debug?
raise FormulaUnavailableError, name unless path.file?
Formulary.load_formula_from_path(name, path)
2014-12-29 14:53:22 -05:00
end
2013-06-18 10:11:06 -07:00
end
2013-06-18 10:11:06 -07:00
# Loads formulae from bottles.
class BottleLoader < FormulaLoader
def initialize(bottle_name)
2013-06-18 10:11:06 -07:00
@bottle_filename = Pathname(bottle_name).realpath
2016-04-25 17:57:51 +01:00
name, full_name = Utils::Bottles.resolve_formula_names @bottle_filename
super name, Formulary.path(full_name)
2013-06-18 10:11:06 -07:00
end
def get_formula(spec)
formula = super
formula.local_bottle_path = @bottle_filename
formula_version = formula.pkg_version
2016-04-25 17:57:51 +01:00
bottle_version = Utils::Bottles.resolve_version(@bottle_filename)
unless formula_version == bottle_version
raise BottleVersionMismatchError.new(@bottle_filename, bottle_version, formula, formula_version)
end
formula
2013-06-18 10:11:06 -07:00
end
end
class AliasLoader < FormulaLoader
def initialize(alias_path)
path = alias_path.resolved_path
name = path.basename(".rb").to_s
super name, path
@alias_path = alias_path
end
end
2013-06-18 10:11:06 -07:00
# 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
2013-06-18 10:11:06 -07:00
end
end
# Loads formulae from URLs
2013-06-18 10:11:06 -07:00
class FromUrlLoader < FormulaLoader
attr_reader :url
def initialize(url)
2013-06-18 10:11:06 -07:00
@url = url
uri = URI(url)
formula = File.basename(uri.path, ".rb")
super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri.path)
2013-06-18 10:11:06 -07:00
end
def load_file
HOMEBREW_CACHE_FORMULA.mkpath
FileUtils.rm_f(path)
curl url, "-o", path
super
end
2013-06-18 10:11:06 -07:00
end
2013-06-18 10:11:06 -07:00
# Loads tapped formulae.
class TapLoader < FormulaLoader
attr_reader :tap
2014-04-05 22:03:33 -05:00
def initialize(tapped_name)
2014-04-05 22:03:33 -05:00
user, repo, name = tapped_name.split("/", 3).map(&:downcase)
@tap = Tap.fetch user, repo
formula_dir = @tap.formula_dir || @tap.path
path = formula_dir/"#{name}.rb"
2015-09-12 18:04:06 +08:00
unless path.file?
2015-12-06 21:44:18 +08:00
if (possible_alias = @tap.alias_dir/name).file?
2015-09-12 18:04:06 +08:00
path = possible_alias.resolved_path
name = path.basename(".rb").to_s
elsif (new_name = @tap.formula_renames[name]) &&
(new_path = formula_dir/"#{new_name}.rb").file?
path = new_path
name = new_name
2015-09-12 18:04:06 +08:00
end
end
2014-04-05 22:03:33 -05:00
super name, path
end
def get_formula(spec)
super
rescue FormulaUnavailableError => e
raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace
2013-06-18 10:11:06 -07:00
end
end
class NullLoader < FormulaLoader
def initialize(name)
super name, Formulary.core_path(name)
end
def get_formula(_spec)
raise FormulaUnavailableError, name
end
end
2015-09-02 16:25:46 +08:00
# Load formulae directly from their contents
class FormulaContentsLoader < FormulaLoader
# The formula's contents
attr_reader :contents
def initialize(name, path, contents)
@contents = contents
super name, path
end
def klass
$stderr.puts "#{$0} (#{self.class.name}): loading #{path}" if ARGV.debug?
2015-09-02 16:25:46 +08:00
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents)}"
Formulary.load_formula(name, path, contents, namespace)
end
end
2013-06-18 10:11:06 -07:00
# 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, spec = :stable)
loader_for(ref).get_formula(spec)
end
2015-05-16 11:26:26 +08:00
# Return a Formula instance for the given rack.
2015-07-30 16:33:19 +08:00
# It will auto resolve formula's spec when requested spec is nil
def self.from_rack(rack, spec = nil)
2015-05-16 11:26:26 +08:00
kegs = rack.directory? ? rack.subdirs.map { |d| Keg.new(d) } : []
keg = kegs.detect(&:linked?) || kegs.detect(&:optlinked?) || kegs.max_by(&:version)
2016-07-15 17:45:21 +08:00
if keg
from_keg(keg, spec)
else
factory(rack.basename.to_s, spec || :stable)
end
end
# Return a Formula instance for the given keg.
# It will auto resolve formula's spec when requested spec is nil
def self.from_keg(keg, spec = nil)
2015-07-30 16:33:19 +08:00
tab = Tab.for_keg(keg)
tap = tab.tap
spec ||= tab.spec
2015-05-16 11:26:26 +08:00
f = if tap.nil?
2016-07-15 19:59:35 +08:00
factory(keg.rack.basename.to_s, spec)
2015-05-16 11:26:26 +08:00
else
begin
2016-07-15 17:45:21 +08:00
factory("#{tap}/#{keg.rack.basename}", spec)
rescue FormulaUnavailableError
# formula may be migrated to different tap. Try to search in core and all taps.
2016-07-15 17:45:21 +08:00
factory(keg.rack.basename.to_s, spec)
end
2015-05-16 11:26:26 +08:00
end
f.build = tab
f.version.update_commit(keg.version.version.commit) if f.head? && keg.version.head?
f
2015-05-16 11:26:26 +08:00
end
2015-09-02 16:25:46 +08:00
# Return a Formula instance directly from contents
def self.from_contents(name, path, contents, spec = :stable)
FormulaContentsLoader.new(name, path, contents).get_formula(spec)
end
def self.to_rack(ref)
# If using a fully-scoped reference, check if the formula can be resolved.
factory(ref) if ref.include? "/"
# Check whether the rack with the given name exists.
if (rack = HOMEBREW_CELLAR/File.basename(ref, ".rb")).directory?
2015-08-15 16:12:42 +08:00
return rack.resolved_path
end
# Use canonical name to locate rack.
2015-08-15 16:12:42 +08:00
(HOMEBREW_CELLAR/canonical_name(ref)).resolved_path
end
2014-04-05 22:03:34 -05:00
def self.canonical_name(ref)
loader_for(ref).name
rescue TapFormulaAmbiguityError
# If there are multiple tap formulae with the name of ref,
# then ref is the canonical name
ref.downcase
2014-04-05 22:03:34 -05:00
end
def self.path(ref)
loader_for(ref).path
end
def self.loader_for(ref)
case ref
2015-09-02 20:21:43 +08:00
when %r{(https?|ftp|file)://}
return FromUrlLoader.new(ref)
when Pathname::BOTTLE_EXTNAME_RX
return BottleLoader.new(ref)
2014-04-05 22:03:33 -05:00
when HOMEBREW_TAP_FORMULA_REGEX
return TapLoader.new(ref)
end
if File.extname(ref) == ".rb"
return FromPathLoader.new(ref)
end
formula_with_that_name = core_path(ref)
if formula_with_that_name.file?
2015-01-01 01:21:59 -05:00
return FormulaLoader.new(ref, formula_with_that_name)
end
possible_alias = CoreTap.instance.alias_dir/ref
if possible_alias.file?
return AliasLoader.new(possible_alias)
end
2015-08-22 13:15:33 +08:00
possible_tap_formulae = tap_paths(ref)
2015-05-08 19:16:06 +08:00
if possible_tap_formulae.size > 1
raise TapFormulaAmbiguityError.new(ref, possible_tap_formulae)
elsif possible_tap_formulae.size == 1
2015-09-12 18:04:06 +08:00
path = possible_tap_formulae.first.resolved_path
name = path.basename(".rb").to_s
return FormulaLoader.new(name, path)
2015-05-08 19:16:06 +08:00
end
if newref = CoreTap.instance.formula_renames[ref]
formula_with_that_oldname = core_path(newref)
if formula_with_that_oldname.file?
return FormulaLoader.new(newref, formula_with_that_oldname)
end
end
possible_tap_newname_formulae = []
Tap.each do |tap|
if newref = tap.formula_renames[ref]
possible_tap_newname_formulae << "#{tap.name}/#{newref}"
end
end
if possible_tap_newname_formulae.size > 1
raise TapFormulaWithOldnameAmbiguityError.new(ref, possible_tap_newname_formulae)
elsif !possible_tap_newname_formulae.empty?
return TapLoader.new(possible_tap_newname_formulae.first)
end
possible_cached_formula = Pathname.new("#{HOMEBREW_CACHE_FORMULA}/#{ref}.rb")
if possible_cached_formula.file?
2015-01-01 01:21:59 -05:00
return FormulaLoader.new(ref, possible_cached_formula)
end
NullLoader.new(ref)
end
def self.core_path(name)
CoreTap.instance.formula_dir/"#{name.downcase}.rb"
end
2015-05-08 19:16:06 +08:00
2015-08-22 13:15:33 +08:00
def self.tap_paths(name, taps = Dir["#{HOMEBREW_LIBRARY}/Taps/*/*/"])
2015-05-08 19:16:06 +08:00
name = name.downcase
2015-08-09 22:43:01 +08:00
taps.map do |tap|
Pathname.glob([
"#{tap}Formula/#{name}.rb",
"#{tap}HomebrewFormula/#{name}.rb",
"#{tap}#{name}.rb",
"#{tap}Aliases/#{name}",
]).detect(&:file?)
end.compact
2015-05-08 19:16:06 +08:00
end
2015-08-09 22:43:01 +08:00
2015-08-22 13:15:33 +08:00
def self.find_with_priority(ref, spec = :stable)
2015-08-09 22:43:01 +08:00
possible_pinned_tap_formulae = tap_paths(ref, Dir["#{HOMEBREW_LIBRARY}/PinnedTaps/*/*/"]).map(&:realpath)
if possible_pinned_tap_formulae.size > 1
raise TapFormulaAmbiguityError.new(ref, possible_pinned_tap_formulae)
elsif possible_pinned_tap_formulae.size == 1
selected_formula = factory(possible_pinned_tap_formulae.first, spec)
if core_path(ref).file?
opoo <<-EOS.undent
#{ref} is provided by core, but is now shadowed by #{selected_formula.full_name}.
To refer to the core formula, use Homebrew/core/#{ref} instead.
2015-08-09 22:43:01 +08:00
EOS
end
selected_formula
else
factory(ref, spec)
end
end
end