brew/Library/Homebrew/formula.rb
Jack Nagel cf08b71bf8 FormulaInstaller: construct new ARGV from an Options collection
The array of options that is passed to the spawned build process is a
combination of the current ARGV, options passed in by a dependent
formula, and an existing install receipt. The objects that are
interacting here each expect the resulting collection to have certain
properties, and the expectations are not consistent.

Clear up this confusing mess by only dealing with Options collections.
This keeps our representation of options uniform across the codebase.

We can remove BuildOptions dependency on HomebrewArgvExtension, which
allows us to pass any Array-like collection to Tab.create. The only
other site inside of FormulaInstaller that uses the array is the #exec
call, and there it is splatted and thus we can substitute our Options
collection there as well.
2013-01-26 12:14:47 -06:00

828 lines
22 KiB
Ruby

require 'download_strategy'
require 'dependencies'
require 'formula_support'
require 'hardware'
require 'bottles'
require 'patches'
require 'compilers'
require 'build_environment'
require 'build_options'
require 'extend/set'
class Formula
include FileUtils
extend BuildEnvironmentDSL
attr_reader :name, :path, :homepage, :downloader
attr_reader :stable, :bottle, :devel, :head, :active_spec
# The current working directory during builds and tests.
# Will only be non-nil inside #stage and #test.
attr_reader :buildpath, :testpath
# Homebrew determines the name
def initialize name='__UNKNOWN__', path=nil
set_instance_variable :homepage
set_instance_variable :stable
set_instance_variable :bottle
set_instance_variable :devel
set_instance_variable :head
@name = name
validate_variable :name
# If a checksum or version was set in the DSL, but no stable URL
# was defined, make @stable nil and save callers some trouble
@stable = nil if @stable and @stable.url.nil?
# Ensure the bottle URL is set. If it does not have a checksum,
# then a bottle is not available for the current platform.
if @bottle and not (@bottle.checksum.nil? or @bottle.checksum.empty?)
@bottle.url ||= bottle_base_url + bottle_filename(self)
else
@bottle = nil
end
@active_spec = if @head and ARGV.build_head? then @head # --HEAD
elsif @devel and ARGV.build_devel? then @devel # --devel
elsif @bottle and install_bottle?(self) then @bottle # bottle available
elsif @stable.nil? and @head then @head # head-only
else @stable # default
end
@version = @active_spec.version
validate_variable :version if @version
raise "No url provided for formula #{name}" if @active_spec.url.nil?
# If we got an explicit path, use that, else determine from the name
@path = path.nil? ? self.class.path(name) : Pathname.new(path)
@downloader = download_strategy.new(name, @active_spec)
# Combine DSL `option` and `def options`
options.each do |opt, desc|
# make sure to strip "--" from the start of options
self.class.build.add opt[/--(.+)$/, 1], desc
end
end
def url; @active_spec.url; end
def version; @active_spec.version; end
def specs; @active_spec.specs; end
def mirrors; @active_spec.mirrors; end
# if the dir is there, but it's empty we consider it not installed
def installed?
installed_prefix.children.length > 0 rescue false
end
def linked_keg
HOMEBREW_REPOSITORY/'Library/LinkedKegs'/@name
end
def installed_prefix
devel_prefix = unless @devel.nil?
HOMEBREW_CELLAR/@name/@devel.version
end
head_prefix = unless @head.nil?
HOMEBREW_CELLAR/@name/@head.version
end
if @active_spec == @head || @head and head_prefix.directory?
head_prefix
elsif @active_spec == @devel || @devel and devel_prefix.directory?
devel_prefix
else
prefix
end
end
def installed_version
require 'keg'
Keg.new(installed_prefix).version
end
def prefix
validate_variable :name
validate_variable :version
HOMEBREW_CELLAR/@name/@version
end
def rack; prefix.parent end
def bin; prefix+'bin' end
def doc; share+'doc'+name end
def include; prefix+'include' end
def info; share+'info' end
def lib; prefix+'lib' end
def libexec; prefix+'libexec' end
def man; share+'man' end
def man1; man+'man1' end
def man2; man+'man2' end
def man3; man+'man3' end
def man4; man+'man4' end
def man5; man+'man5' end
def man6; man+'man6' end
def man7; man+'man7' end
def man8; man+'man8' end
def sbin; prefix+'sbin' end
def share; prefix+'share' end
# configuration needs to be preserved past upgrades
def etc; HOMEBREW_PREFIX+'etc' end
# generally we don't want var stuff inside the keg
def var; HOMEBREW_PREFIX+'var' end
# override this to provide a plist
def plist; nil; end
alias :startup_plist :plist
# plist name, i.e. the name of the launchd service
def plist_name; 'homebrew.mxcl.'+name end
def plist_path; prefix+(plist_name+'.plist') end
def plist_manual; self.class.plist_manual end
def plist_startup; self.class.plist_startup end
def build
self.class.build
end
def opt_prefix; HOMEBREW_PREFIX/:opt/name end
# Use the @active_spec to detect the download strategy.
# Can be overriden to force a custom download strategy
def download_strategy
@active_spec.download_strategy
end
def cached_download
@downloader.cached_location
end
# tell the user about any caveats regarding this package, return a string
def caveats; nil end
# any e.g. configure options for this package
def options; [] end
# patches are automatically applied after extracting the tarball
# return an array of strings, or if you need a patch level other than -p1
# return a Hash eg.
# {
# :p0 => ['http://foo.com/patch1', 'http://foo.com/patch2'],
# :p1 => 'http://bar.com/patch2',
# :p2 => ['http://moo.com/patch5', 'http://moo.com/patch6']
# }
# The final option is to return DATA, then put a diff after __END__. You
# can still return a Hash with DATA as the value for a patch level key.
def patches; end
# rarely, you don't want your library symlinked into the main prefix
# see gettext.rb for an example
def keg_only?
kor = self.class.keg_only_reason
not kor.nil? and kor.valid?
end
def keg_only_reason
self.class.keg_only_reason
end
def fails_with? cc
return false if self.class.cc_failures.nil?
cc = Compiler.new(cc) unless cc.is_a? Compiler
return self.class.cc_failures.find do |failure|
next unless failure.compiler == cc.name
failure.build.zero? or failure.build >= cc.build
end
end
# sometimes the clean process breaks things
# skip cleaning paths in a formula with a class method like this:
# skip_clean [bin+"foo", lib+"bar"]
# redefining skip_clean? now deprecated
def skip_clean? path
return true if self.class.skip_clean_all?
return true if path.extname == '.la' and self.class.skip_clean_paths.include? :la
to_check = path.relative_path_from(prefix).to_s
self.class.skip_clean_paths.include? to_check
end
# yields self with current working directory set to the uncompressed tarball
def brew
validate_variable :name
validate_variable :version
stage do
begin
patch
# we allow formulae to do anything they want to the Ruby process
# so load any deps before this point! And exit asap afterwards
yield self
rescue RuntimeError, SystemCallError => e
%w(config.log CMakeCache.txt).each do |fn|
(HOMEBREW_LOGS/name).install(fn) if File.file?(fn)
end
raise
end
end
end
def lock
lockpath = HOMEBREW_CACHE_FORMULA/"#{@name}.brewing"
@lockfile = lockpath.open(File::RDWR | File::CREAT)
unless @lockfile.flock(File::LOCK_EX | File::LOCK_NB)
raise OperationInProgressError, @name
end
end
def unlock
unless @lockfile.nil?
@lockfile.flock(File::LOCK_UN)
@lockfile.close
end
end
def == b
name == b.name
end
def eql? b
self == b and self.class.equal? b.class
end
def hash
name.hash
end
def <=> b
name <=> b.name
end
def to_s
name
end
# Standard parameters for CMake builds.
# Using Build Type "None" tells cmake to use our CFLAGS,etc. settings.
# Setting it to Release would ignore our flags.
# Setting CMAKE_FIND_FRAMEWORK to "LAST" tells CMake to search for our
# libraries before trying to utilize Frameworks, many of which will be from
# 3rd party installs.
# Note: there isn't a std_autotools variant because autotools is a lot
# less consistent and the standard parameters are more memorable.
def std_cmake_args
%W[
-DCMAKE_INSTALL_PREFIX=#{prefix}
-DCMAKE_BUILD_TYPE=None
-DCMAKE_FIND_FRAMEWORK=LAST
-Wno-dev
]
end
def self.class_s name
#remove invalid characters and then camelcase it
name.capitalize.gsub(/[-_.\s]([a-zA-Z0-9])/) { $1.upcase } \
.gsub('+', 'x')
end
# an array of all Formula names
def self.names
Dir["#{HOMEBREW_REPOSITORY}/Library/Formula/*.rb"].map{ |f| File.basename f, '.rb' }.sort
end
def self.each
names.each do |name|
yield begin
Formula.factory(name)
rescue => e
# Don't let one broken formula break commands. But do complain.
onoe "Failed to import: #{name}"
next
end
end
end
class << self
include Enumerable
end
def self.all
opoo "Formula.all is deprecated, simply use Formula.map"
map
end
def self.installed
HOMEBREW_CELLAR.children.map{ |rack| factory(rack.basename) rescue nil }.compact
end
def inspect
name
end
def self.aliases
Dir["#{HOMEBREW_REPOSITORY}/Library/Aliases/*"].map{ |f| File.basename f }.sort
end
def self.canonical_name name
name = name.to_s if name.kind_of? Pathname
formula_with_that_name = HOMEBREW_REPOSITORY+"Library/Formula/#{name}.rb"
possible_alias = HOMEBREW_REPOSITORY+"Library/Aliases/#{name}"
possible_cached_formula = HOMEBREW_CACHE_FORMULA+"#{name}.rb"
if name.include? "/"
if name =~ %r{(.+)/(.+)/(.+)}
tapd = HOMEBREW_REPOSITORY/"Library/Taps"/"#$1-#$2".downcase
tapd.find_formula do |relative_pathname|
return "#{tapd}/#{relative_pathname}" if relative_pathname.stem.to_s == $3
end if tapd.directory?
end
# Otherwise don't resolve paths or URLs
name
elsif formula_with_that_name.file? and formula_with_that_name.readable?
name
elsif possible_alias.file?
possible_alias.realpath.basename('.rb').to_s
elsif possible_cached_formula.file?
possible_cached_formula.to_s
else
name
end
end
def self.factory name
# If an instance of Formula is passed, just return it
return name if name.kind_of? Formula
# Otherwise, convert to String in case a Pathname comes in
name = name.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? self.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
name = bottle_filename.basename.to_s.rpartition('-').first
path = Formula.path(name)
install_type = :from_local_bottle
else
name = Formula.canonical_name(name)
if name =~ %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? "/"
# 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
else
# For names, map to the path and then require
path = Formula.path(name)
install_type = :from_name
end
end
klass_name = self.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
klass.finalize_dsl
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)
end
def tap
if path.realpath.to_s =~ %r{#{HOMEBREW_REPOSITORY}/Library/Taps/(\w+)-(\w+)}
"#$1/#$2"
else
# remotely installed formula are not mxcl/master but this will do for now
"mxcl/master"
end
end
def self.path name
HOMEBREW_REPOSITORY+"Library/Formula/#{name.downcase}.rb"
end
def deps; self.class.dependencies.deps; end
def requirements; self.class.dependencies.requirements; end
def env
@env ||= self.class.env
end
def conflicts
requirements.select { |r| r.is_a? ConflictRequirement }
end
# Returns a list of Dependency objects in an installable order, which
# means if a depends on b then b will be ordered before a in this list
def recursive_dependencies(&block)
Dependency.expand(self, &block)
end
# The full set of Requirements for this formula's dependency tree.
def recursive_requirements
reqs = ComparableSet.new
recursive_deps.each { |dep| reqs.merge dep.requirements }
reqs.merge requirements
end
def to_hash
hsh = {
"name" => name,
"homepage" => homepage,
"versions" => {
"stable" => (stable.version.to_s if stable),
"bottle" => bottle && MacOS.bottles_supported? || false,
"devel" => (devel.version.to_s if devel),
"head" => (head.version.to_s if head)
},
"installed" => [],
"linked_keg" => (linked_keg.realpath.basename.to_s if linked_keg.exist?),
"keg_only" => keg_only?,
"dependencies" => deps.map {|dep| dep.to_s},
"conflicts_with" => conflicts.map {|c| c.formula},
"options" => [],
"caveats" => caveats
}
build.each do |opt|
hsh["options"] << {
"option" => "--"+opt.name,
"description" => opt.description
}
end
if rack.directory?
rack.children.each do |keg|
next if keg.basename.to_s == '.DS_Store'
tab = Tab.for_keg keg
hsh["installed"] << {
"version" => keg.basename.to_s,
"used_options" => tab.used_options,
"built_as_bottle" => tab.built_bottle
}
end
end
hsh
end
protected
# Pretty titles the command and buffers stdout/stderr
# Throws if there's an error
def system cmd, *args
# remove "boring" arguments so that the important ones are more likely to
# be shown considering that we trim long ohai lines to the terminal width
pretty_args = args.dup
if cmd == "./configure" and not ARGV.verbose?
pretty_args.delete "--disable-dependency-tracking"
pretty_args.delete "--disable-debug"
end
ohai "#{cmd} #{pretty_args*' '}".strip
removed_ENV_variables = case if args.empty? then cmd.split(' ').first else cmd end
when "xcodebuild"
ENV.remove_cc_etc
end
if ARGV.verbose?
safe_system cmd, *args
else
@exec_count ||= 0
@exec_count += 1
logd = HOMEBREW_LOGS/name
logfn = "#{logd}/%02d.%s" % [@exec_count, File.basename(cmd).split(' ').first]
mkdir_p(logd)
rd, wr = IO.pipe
pid = fork do
rd.close
$stdout.reopen wr
$stderr.reopen wr
args.collect!{|arg| arg.to_s}
exec(cmd, *args) rescue nil
puts "Failed to execute: #{cmd}"
exit! 1 # never gets here unless exec threw or failed
end
wr.close
f = File.open(logfn, 'w')
f.write(rd.read) until rd.eof?
Process.wait
unless $?.success?
unless ARGV.verbose?
f.flush
Kernel.system "/usr/bin/tail", "-n", "5", logfn
end
f.puts
require 'cmd/--config'
Homebrew.write_build_config(f)
raise ErrorDuringExecution
end
end
rescue ErrorDuringExecution => e
raise BuildError.new(self, cmd, args, $?)
ensure
f.close if f and not f.closed?
removed_ENV_variables.each do |key, value|
ENV[key] = value
end if removed_ENV_variables
end
public
# For brew-fetch and others.
def fetch
# Ensure the cache exists
HOMEBREW_CACHE.mkpath
return @downloader.fetch, @downloader
end
# For FormulaInstaller.
def verify_download_integrity fn
@active_spec.verify_download_integrity(fn)
end
def test
ret = nil
mktemp do
@testpath = Pathname.pwd
ret = instance_eval(&self.class.test)
@testpath = nil
end
ret
end
def test_defined?
not self.class.instance_variable_get(:@test_defined).nil?
end
private
def stage
fetched, downloader = fetch
verify_download_integrity fetched if fetched.kind_of? Pathname
mktemp do
downloader.stage
# Set path after the downloader changes the working folder.
@buildpath = Pathname.pwd
yield
@buildpath = nil
end
end
def patch
patch_list = Patches.new(patches)
return if patch_list.empty?
if patch_list.external_patches?
ohai "Downloading patches"
patch_list.download!
end
ohai "Patching"
patch_list.each do |p|
case p.compression
when :gzip then safe_system "/usr/bin/gunzip", p.compressed_filename
when :bzip2 then safe_system "/usr/bin/bunzip2", p.compressed_filename
end
# -f means don't prompt the user if there are errors; just exit with non-zero status
safe_system '/usr/bin/patch', '-f', *(p.patch_args)
end
end
def validate_variable name
v = instance_variable_get("@#{name}")
raise "Invalid @#{name}" if v.to_s.empty? or v.to_s =~ /\s/
end
def set_instance_variable(type)
return if instance_variable_defined? "@#{type}"
class_value = self.class.send(type)
instance_variable_set("@#{type}", class_value) if class_value
end
def self.method_added method
case method
when :brew
raise "You cannot override Formula#brew"
when :test
@test_defined = true
end
end
class << self
# The methods below define the formula DSL.
def self.attr_rw(*attrs)
attrs.each do |attr|
class_eval %Q{
def #{attr}(val=nil)
val.nil? ? @#{attr} : @#{attr} = val
end
}
end
end
attr_rw :homepage, :keg_only_reason, :skip_clean_all, :cc_failures
attr_rw :plist_startup, :plist_manual
Checksum::TYPES.each do |cksum|
class_eval %Q{
def #{cksum}(val=nil)
unless val.nil?
@stable ||= SoftwareSpec.new
@stable.#{cksum}(val)
end
return @stable ? @stable.#{cksum} : @#{cksum}
end
}
end
def build
@build ||= BuildOptions.new(ARGV.options_only)
end
def url val=nil, specs=nil
if val.nil?
return @stable.url if @stable
return @url if @url
end
@stable ||= SoftwareSpec.new
@stable.url(val, specs)
end
def stable &block
return @stable unless block_given?
instance_eval(&block)
end
def bottle url=nil, &block
return @bottle unless block_given?
@bottle ||= Bottle.new
@bottle.instance_eval(&block)
end
def devel &block
return @devel unless block_given?
@devel ||= SoftwareSpec.new
@devel.instance_eval(&block)
end
def head val=nil, specs=nil
return @head if val.nil?
@head ||= HeadSoftwareSpec.new
@head.url(val, specs)
end
def version val=nil
return @version if val.nil?
@stable ||= SoftwareSpec.new
@stable.version(val)
end
def mirror val
@stable ||= SoftwareSpec.new
@stable.mirror(val)
end
def dependencies
@dependencies ||= DependencyCollector.new
end
def depends_on dep
dependencies.add(dep)
end
def option name, description=nil
# Support symbols
name = name.to_s
raise "Option name is required." if name.empty?
raise "Options should not start with dashes." if name[0, 1] == "-"
build.add name, description
end
def plist_options options
@plist_startup = options[:startup]
@plist_manual = options[:manual]
end
def conflicts_with formula, opts={}
dependencies.add ConflictRequirement.new(formula, name, opts)
end
def skip_clean *paths
paths = [paths].flatten
# :all is deprecated though
if paths.include? :all
@skip_clean_all = true
return
end
@skip_clean_paths ||= []
paths.each do |p|
p = p.to_s unless p == :la # Keep :la in paths as a symbol
@skip_clean_paths << p unless @skip_clean_paths.include? p
end
end
def skip_clean_all?
@skip_clean_all
end
def skip_clean_paths
@skip_clean_paths or []
end
def keg_only reason, explanation=nil
@keg_only_reason = KegOnlyReason.new(reason, explanation.to_s.chomp)
end
def fails_with compiler, &block
@cc_failures ||= CompilerFailures.new
@cc_failures << if block_given?
CompilerFailure.new(compiler, &block)
else
CompilerFailure.new(compiler)
end
end
def test &block
return @test unless block_given?
@test_defined = true
@test = block
end
# This method is called once by `factory` before creating any instances.
# It allows the DSL to finalize itself, reducing complexity in the constructor.
def finalize_dsl
# Synthesize options for optional dependencies
dependencies.deps.select(&:optional?).each do |dep|
unless build.has_option? "with-#{dep.name}"
option "with-#{dep.name}", "Build with #{dep.name} support"
end
end
# Synthesize options for recommended dependencies
dependencies.deps.select(&:recommended?).each do |dep|
unless build.has_option? "without-#{dep.name}"
option "without-#{dep.name}", "Build without #{dep.name} support"
end
end
end
end
end
require 'formula_specialties'