brew/Library/Homebrew/formula.rb
Max Howell 760c083c0c Refactor
Large refactor to Formula, mostly improving reliability and error handling but
also layout and readability.

General improvements so testing can be more complete.

Patches are automatically downloaded and applied for Formula that return a
list of urls from Formula::patches.

Split out the brew command logic to facilitate testing.

Facility from Adam Vandenberg to allow selective cleaning of files, added
because Python doesn't work when stripped.
2009-08-10 18:11:17 +01:00

276 lines
7.2 KiB
Ruby
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright 2009 Max Howell <max@methylblue.com>
#
# This file is part of Homebrew.
#
# Homebrew is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Homebrew is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Homebrew. If not, see <http://www.gnu.org/licenses/>.
class ExecutionError <RuntimeError
def initialize cmd, args=[]
super "#{cmd} #{args*' '}"
end
end
class BuildError <ExecutionError; end
class FormulaUnavailableError <RuntimeError
def initialize name
super "No available formula for #{name}"
end
end
# the base class variety of formula, you don't get a prefix, so it's not
# useful. See the derived classes for fun and games.
class AbstractFormula
def initialize noop=nil
@version=self.class.version unless @version
@url=self.class.url unless @url
@homepage=self.class.homepage unless @homepage
@md5=self.class.md5 unless @md5
@sha1=self.class.sha1 unless @sha1
raise "@url is nil" if @url.nil?
end
# if the dir is there, but it's empty we consider it not installed
def installed?
return prefix.children.length > 0
rescue
return false
end
def prefix
raise "Invalid @name" if @name.nil? or @name.empty?
raise "Invalid @version" if @version.nil? or @version.empty?
HOMEBREW_CELLAR+@name+@version
end
def path
Formula.path name
end
attr_reader :url, :version, :url, :homepage, :name
def bin; prefix+'bin' end
def doc; prefix+'share'+'doc'+name end
def lib; prefix+'lib' end
def man; prefix+'share'+'man' end
def man1; man+'man1' end
def info; prefix+'share'+'info' end
def include; prefix+'include' end
# tell the user about any caveats regarding this package
def caveats; nil end
# patches are automatically applied after extracting the tarball
def patches; [] end
# reimplement and specify dependencies
def deps; end
# sometimes the clean process breaks things, return true to skip anything
def skip_clean? path; false end
# yields self with current working directory set to the uncompressed tarball
def brew
ohai "Downloading #{@url}"
tgz=HOMEBREW_CACHE+File.basename(@url)
unless tgz.exist?
HOMEBREW_CACHE.mkpath
curl @url, '-o', tgz
else
puts "File already downloaded and cached"
end
verify_download_integrity tgz
mktemp do
Dir.chdir uncompress(tgz)
begin
patch
yield self
rescue Interrupt, RuntimeError, SystemCallError => e
raise unless ARGV.debug?
onoe e.inspect
puts e.backtrace
ohai "Rescuing build..."
puts "Type `exit' and Homebrew will attempt to finalize the installation"
puts "If nothing is installed to #{prefix}, then Homebrew will abort"
interactive_shell
end
end
end
protected
# Pretty titles the command and buffers stdout/stderr
# Throws if there's an error
def system cmd, *args
full="#{cmd} #{args*' '}".strip
ohai full
if ARGV.verbose?
safe_system cmd, *args
else
out=''
# TODO write a ruby extension that does a good popen :P
IO.popen "#{full} 2>&1" do |f|
until f.eof?
out+=f.gets
end
end
unless $? == 0
puts out
raise
end
end
rescue
raise BuildError.new(cmd, args)
end
private
def mktemp
tmp=Pathname.new `mktemp -dt #{File.basename @url}`.strip
raise if not tmp.directory? or $? != 0
begin
wd=Dir.pwd
Dir.chdir tmp
yield
ensure
Dir.chdir wd
tmp.rmtree
end
end
# Kernel.system but with exceptions
def safe_system cmd, *args
puts "#{cmd} #{args*' '}" if ARGV.verbose?
# stderr is shown, so hopefully that will explain the problem
raise ExecutionError.new(cmd, args) unless Kernel.system cmd, *args and $? == 0
end
def curl url, *args
safe_system 'curl', '-f#LA', HOMEBREW_USER_AGENT, url, *args
end
def verify_download_integrity fn
require 'digest'
type='MD5'
type='SHA1' if @sha1
supplied=eval "@#{type.downcase}"
hash=eval("Digest::#{type}").hexdigest(fn.read)
if supplied and not supplied.empty?
raise "#{type} mismatch: #{hash}" unless supplied.upcase == hash.upcase
else
opoo "Cannot verify package integrity"
puts "The formula did not provide a download checksum"
puts "For your reference the #{type} is: #{hash}"
end
end
def patch
unless patches.empty?
ohai "Patching"
ff=(1..patches.length).collect {|n| '%03d-homebrew.patch'%n}
curl *patches+ff.collect {|f|"-o#{f}"}
ff.each {|f| safe_system 'patch', '-p0', '-i', f}
end
end
class <<self
attr_reader :url, :version, :md5, :url, :homepage, :sha1
end
end
# This is the meat. See the examples.
class Formula <AbstractFormula
def initialize name=nil
super
@name=name
@version=Pathname.new(@url).version unless @version
end
def self.class name
#remove invalid characters and camelcase
name.capitalize.gsub(/[-_\s]([a-zA-Z0-9])/) { $1.upcase }
end
def self.factory name
require self.path(name)
return eval(self.class(name)).new(name)
rescue LoadError
raise FormulaUnavailableError.new(name)
end
def self.path name
HOMEBREW_PREFIX+'Library'+'Formula'+"#{name.downcase}.rb"
end
# we don't have a std_autotools variant because autotools is a lot less
# consistent and the standard parameters are more memorable
# really Homebrew should determine what works inside brew() then
# we could add --disable-dependency-tracking when it will work
def std_cmake_parameters
# The None part makes cmake use the environment's CFLAGS etc. settings
"-DCMAKE_INSTALL_PREFIX='#{prefix}' -DCMAKE_BUILD_TYPE=None"
end
private
def uncompress_args
rx=%r[http://(www.)?github.com/.*/(zip|tar)ball/]
if rx.match @url and $2 == '.zip' or Pathname.new(@url).extname == '.zip'
%w[unzip -qq]
else
%w[tar xf]
end
end
def uncompress path
safe_system *uncompress_args<<path
entries=Dir['*']
if entries.length == 0
raise "Empty archive"
elsif entries.length == 1
# if one dir enter it as that will be where the build is
entries.first
else
# if there's more than one dir, then this is the build directory already
Dir.pwd
end
end
def method_added method
raise 'You cannot override Formula.brew' if method == 'brew'
end
end
# see ack.rb for an example usage
class ScriptFileFormula <AbstractFormula
def initialize name=nil
super
@name=name
end
def uncompress path
path.dirname
end
def install
bin.install File.basename(@url)
end
end
# see flac.rb for example usage
class GithubGistFormula <ScriptFileFormula
def initialize name=nil
super
@name=name
@version=File.basename(File.dirname(url))[0,6]
end
end