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.
This commit is contained in:
Max Howell 2009-08-10 16:48:30 +01:00
parent 5a396fd8b4
commit 760c083c0c
10 changed files with 677 additions and 591 deletions

View File

@ -35,7 +35,7 @@ _brew_to_completion()
;;
# Commands that take an existing brew...
abv|info|list|link|ls|ln|rm|uninstall)
abv|info|list|link|ls|ln|rm|remove|uninstall)
cellar_contents=`ls ${brew_base}/Cellar/`
COMPREPLY=( $(compgen -W "${cellar_contents}" -- ${cur}) )
return 0

238
Library/Homebrew/brew.h.rb Normal file
View File

@ -0,0 +1,238 @@
# 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/>.
def make url
require 'formula'
path=Pathname.new url
/(.*?)[-_.]?#{path.version}/.match path.basename
raise "Couldn't parse name from #{url}" if $1.nil? or $1.empty?
path=Formula.path $1
raise "#{path} already exists" if path.exist?
template=<<-EOS
require 'brewkit'
class #{Formula.class $1} <Formula
@url='#{url}'
@homepage=''
@md5=''
cmake def deps
cmake BinaryDep.new 'cmake'
cmake end
cmake
def install
autotools system "./configure --prefix='\#{prefix}' --disable-debug --disable-dependency-tracking"
cmake system "cmake . \#{cmake_std_parameters}"
system "make install"
end
end
EOS
mode=nil
if ARGV.include? '--cmake'
mode= :cmake
elsif ARGV.include? '--autotools'
mode= :autotools
end
f=File.new path, 'w'
template.each_line do |s|
if s.strip.empty?
f.puts
next
end
cmd=s[0..11].strip
if cmd.empty?
cmd=nil
else
cmd=cmd.to_sym
end
out=s[12..-1] || ''
if mode.nil?
# we show both but comment out cmake as it is less common
# the implication being the pacakger should remove whichever is not needed
if cmd == :cmake and not out.empty?
f.print '#'
out = out[1..-1]
end
elsif cmd != mode and not cmd.nil?
next
end
f.puts out
end
f.close
return path
end
def info name
require 'formula'
history="http://github.com/mxcl/homebrew/commits/masterbrew/Library/Formula/#{Formula.path(name).basename}"
exec 'open', history if ARGV.flag? '--github'
f=Formula.factory name
puts "#{f.name} #{f.version}"
puts f.homepage
if f.prefix.parent.directory?
kids=f.prefix.parent.children
kids.each do |keg|
print "#{keg} (#{keg.abv})"
print " *" if f.prefix == keg and kids.length > 1
puts
end
else
puts "Not installed"
end
if f.caveats
puts
puts f.caveats
puts
end
puts history
rescue FormulaUnavailableError
# check for DIY installation
d=HOMEBREW_PREFIX+name
if d.directory?
ohai "DIY Installation"
d.children.each {|keg| puts "#{keg} (#{keg.abv})"}
else
raise "No such formula or keg"
end
end
def clean f
Cleaner.new f
# remove empty directories TODO Rubyize!
`perl -MFile::Find -e"finddepth(sub{rmdir},'#{f.prefix}')"`
end
def install f
f.brew do
if ARGV.flag? '--interactive'
ohai "Entering interactive mode"
puts "Type `exit' to return and finalize the installation"
puts "Install to this prefix: #{f.prefix}"
interactive_shell
elsif ARGV.include? '--help'
system './configure --help'
exit $?
else
f.prefix.mkpath
f.install
%w[README ChangeLog COPYING LICENSE COPYRIGHT AUTHORS].each do |file|
f.prefix.install file if File.file? file
end
end
end
end
def prune
$n=0
$d=0
dirs=Array.new
paths=%w[bin etc lib include share].collect {|d| HOMEBREW_PREFIX+d}
paths.each do |path|
path.find do |path|
path.extend ObserverPathnameExtension
if path.symlink?
path.unlink unless path.resolved_path_exists?
elsif path.directory?
dirs<<path
end
end
end
dirs.sort.reverse_each {|d| d.rmdir_if_possible}
if $n == 0 and $d == 0
puts "Nothing pruned" if ARGV.verbose?
else
# always showing symlinks text is deliberate
print "Pruned #{$n} symbolic links "
print "and #{$n} directories " if $d > 0
puts "from #{HOMEBREW_PREFIX}"
end
end
################################################################ class Cleaner
class Cleaner
def initialize f
@f=f
[f.bin, f.lib].each {|d| clean_dir d}
end
private
def strip path, args=''
return if @f.skip_clean? path
puts "strip #{path}" if ARGV.verbose?
path.chmod 0644 # so we can strip
unless path.stat.nlink > 1
`strip #{args} #{path}`
else
# strip unlinks the file and recreates it, thus breaking hard links!
# is this expected behaviour? patch does it too… still,mktm this fixes it
tmp=`mktemp -t #{path.basename}`.strip
`strip #{args} -o #{tmp} #{path}`
`cat #{tmp} > #{path}`
File.unlink tmp
end
end
def clean_file path
perms=0444
case `file -h #{path}`
when /Mach-O dynamically linked shared library/
strip path, '-SxX'
when /Mach-O [^ ]* ?executable/
strip path
perms=0544
when /script text executable/
perms=0544
end
path.chmod perms
end
def clean_dir d
d.find do |path|
if not path.file?
next
elsif path.extname == '.la' and not @f.skip_clean? path
# *.la files are stupid
path.unlink
else
clean_file path
end
end
end
end

View File

@ -1,25 +0,0 @@
# 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/>.
require 'pathname+yeast'
require 'utils'
# TODO if whoami == root then use /Library/Caches/Homebrew instead
HOMEBREW_VERSION='0.3'
HOMEBREW_CACHE=Pathname.new("~/Library/Caches/Homebrew").expand_path
HOMEBREW_PREFIX=Pathname.new(__FILE__).dirname.parent.parent.cleanpath
HOMEBREW_CELLAR=HOMEBREW_PREFIX+'Cellar'

View File

@ -15,30 +15,31 @@
# You should have received a copy of the GNU General Public License
# along with Homebrew. If not, see <http://www.gnu.org/licenses/>.
require 'utils'
class BuildError <RuntimeError
def initialize cmd
super "Build failed during: #{cmd}"
class ExecutionError <RuntimeError
def initialize cmd, args=[]
super "#{cmd} #{args*' '}"
end
end
# the base class variety of formula, you don't get a prefix, so it's not really
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
require 'find'
require 'fileutils'
private
class <<self
attr_reader :url, :version, :md5, :url, :homepage, :sha1
end
public
attr_reader :url, :version, :url, :homepage, :name
# reimplement if your package has dependencies
def deps
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
@ -48,50 +49,167 @@ public
return false
end
def initialize name=nil
@name=name
@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.nil?" if @url.nil?
end
def prefix
raise "@name.nil!" if @name.nil?
raise "@version.nil?" if @version.nil?
raise "Invalid @name" if @name.nil? or @name.empty?
raise "Invalid @version" if @version.nil? or @version.empty?
HOMEBREW_CELLAR+@name+@version
end
def bin; prefix+'bin' end
def doc; prefix+'share'+'doc'+name end
def lib; prefix+'lib' end
def man; prefix+'share'+'man' 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
def caveats
nil
# 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
ohai cmd
if ARGV.include? '--verbose'
Kernel.system cmd
def system cmd, *args
full="#{cmd} #{args*' '}".strip
ohai full
if ARGV.verbose?
safe_system cmd, *args
else
out=''
IO.popen "#{cmd} 2>&1" do |f|
# 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
puts out unless $? == 0
unless $? == 0
puts out
raise
end
end
rescue
raise BuildError.new(cmd, args)
end
raise BuildError.new(cmd) unless $? == 0
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
@ -102,108 +220,23 @@ public
# The None part makes cmake use the environment's CFLAGS etc. settings
"-DCMAKE_INSTALL_PREFIX='#{prefix}' -DCMAKE_BUILD_TYPE=None"
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
raise "#{type} mismatch: #{hash}" unless supplied.upcase == hash.upcase
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
opoo "Cannot verify package integrity"
puts "The formula did not provide a download checksum"
puts "For your reference the #{type} is: #{hash}"
%w[tar xf]
end
end
# yields self with current working directory set to the uncompressed tarball
def brew
ohai "Downloading #{@url}"
HOMEBREW_CACHE.mkpath
Dir.chdir HOMEBREW_CACHE do
tmp=nil
tgz=Pathname.new(fetch()).realpath
begin
verify_download_integrity tgz
# we make an additional subdirectory so know exactly what we are
# recursively deleting later
# we use mktemp rather than appsupport/blah because some build scripts
# can't handle being built in a directory with spaces in it :P
tmp=`mktemp -dt #{File.basename @url}`.strip
Dir.chdir tmp do
Dir.chdir uncompress(tgz) do
yield self
end
end
rescue Interrupt, RuntimeError
if ARGV.include? '--debug'
# debug mode allows the packager to intercept a failed build and
# investigate the problems
puts "Rescued build at: #{tmp}"
exit! 1
else
raise
end
ensure
FileUtils.rm_rf tmp if tmp
end
end
end
protected
# returns the directory where the archive was uncompressed
# in this Abstract case we assume there is no archive
def uncompress path
path.dirname
end
private
def fetch
%r[http://(www.)?github.com/.*/(zip|tar)ball/].match @url
if $2
# curl doesn't do the redirect magic that we would like, so we get a
# stupidly named file, this is why wget would be beter, but oh well
tgz="#{@name}-#{@version}.#{$2=='tar' ? 'tgz' : $2}"
oarg="-o #{tgz}"
else
oarg='-O' #use the filename that curl gets
tgz=File.expand_path File.basename(@url)
end
unless File.exists? tgz
`curl -#LA "#{HOMEBREW_USER_AGENT}" #{oarg} "#{@url}"`
raise "Download failed" unless $? == 0
else
puts "File already downloaded and cached"
end
return tgz
end
end
# somewhat useful, it'll raise if you call prefix, but it'll unpack a tar/zip
# for you, check the md5, and allow you to yield from brew
class UnidentifiedFormula <AbstractFormula
def initialize name=nil
super name
end
private
def uncompress(path)
if path.extname == '.zip'
`unzip -qq "#{path}"`
else
`tar xf "#{path}"`
end
raise "Compression tool failed" if $? != 0
safe_system *uncompress_args<<path
entries=Dir['*']
if entries.nil? or entries.length == 0
raise "Empty tarball!"
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
@ -211,30 +244,6 @@ private
# if there's more than one dir, then this is the build directory already
Dir.pwd
end
end
end
# this is what you will mostly use, reimplement install, prefix won't raise
class Formula <UnidentifiedFormula
def initialize name
super 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.path name
Pathname.new(HOMEBREW_PREFIX)+'Library'+'Formula'+(name.downcase+'.rb')
end
def self.create name
require Formula.path(name)
return eval(Formula.class(name)).new(name)
rescue LoadError
raise "No formula for #{name}"
end
def method_added method
@ -243,16 +252,24 @@ class Formula <UnidentifiedFormula
end
# see ack.rb for an example usage
# you need to set @version and @name
class ScriptFileFormula <AbstractFormula
def initialize name=nil
super
@name=name
end
def uncompress path
path.dirname
end
def install
bin.install name
bin.install File.basename(@url)
end
end
# see flac.rb for example usage
class GithubGistFormula <ScriptFileFormula
def initialize
super File.basename(self.class.url)
def initialize name=nil
super
@name=name
@version=File.basename(File.dirname(url))[0,6]
end
end

View File

@ -14,146 +14,61 @@
#
# You should have received a copy of the GNU General Public License
# along with Homebrew. If not, see <http://www.gnu.org/licenses/>.
require 'formula'
class Keg
attr_reader :path, :version, :name
def initialize formula
if formula.is_a? AbstractFormula
@path=formula.prefix
@name=formula.name
@version=formula.version
elsif formula.is_a? Pathname
# TODO
elsif formula.is_a? String
path=HOMEBREW_CELLAR+formula
kids=path.children
raise "Empty installation: #{path}" if kids.length < 1
raise "Multiple versions installed" if kids.length > 1
@path=kids[0]
@name=formula
@version=@path.basename
end
#
class Keg <Pathname
def initialize path
super path
raise "#{to_s} is not a valid keg" unless parent.parent == HOMEBREW_CELLAR
raise "#{to_s} is not a directory" unless directory?
end
def clean
# TODO unset write permission more
%w[bin lib].each {|d| (Pathname.new(path)+d).find do |path|
if not path.file?
next
elsif path.extname == '.la'
# .la files are stupid
path.unlink
else
fo=`file -h #{path}`
args=nil
perms=0444
if fo =~ /Mach-O dynamically linked shared library/
args='-SxX'
elsif fo =~ /Mach-O [^ ]* ?executable/
args='' # use strip defaults
perms=0544
elsif fo =~ /script text executable/
perms=0544
end
if args
puts "Stripping: #{path}" if ARGV.include? '--verbose'
path.chmod 0644 # so we can strip
unless path.stat.nlink > 1
`strip #{args} #{path}`
else
# strip unlinks the file and recreates it, thus breaking hard links!
# is this expected behaviour? patch does it too… still,mktm this fixes it
tmp=`mktemp -t #{path.basename}`.strip
`strip -o #{tmp} #{path}`
`cat #{tmp} > #{path}`
File.unlink tmp
end
end
path.chmod perms
end
end}
# remove empty directories TODO Rubyize!
`perl -MFile::Find -e"finddepth(sub{rmdir},'#{path}')"`
def uninstall
chmod_R 0777 # ensure we have permission to delete
rmtree
parent.rmdir_if_possible
end
def rm
# don't rmtree shit if we aren't positive about our location!
raise "Bad stuff!" unless path.parent.parent == HOMEBREW_CELLAR
def link
$n=0
$d=0
if path.directory?
FileUtils.chmod_R 0777, path # ensure we have permission to delete
path.rmtree # HOMEBREW_CELLAR/foo/1.2.0
path.parent.rmdir if path.parent.children.length == 0 # HOMEBREW_CELLAR/foo
end
mkpaths=(1..9).collect {|x| "man/man#{x}"} <<'man'<<'doc'<<'locale'<<'info'<<'aclocal'
# yeah indeed, you have to force anything you need in the main tree into
# these dirs REMEMBER that *NOT* everything needs to be in the main tree
link_dir('etc') {:mkpath}
link_dir('bin') {:link}
link_dir('lib') {|path| :mkpath if %w[pkgconfig php].include? path.to_s}
link_dir('include') {:link}
link_dir('share') {|path| :mkpath if mkpaths.include? path.to_s}
return $n+$d
end
private
def __symlink_relative_to from, to
tod=to.dirname
tod.mkpath
Dir.chdir(tod) do
#TODO use Ruby function so we get exceptions
#NOTE Ruby functions are fucked up!
`ln -sf "#{from.relative_path_from tod}"`
@n+=1
end
end
# symlinks the contents of self+foo recursively into /usr/local/foo
def link_dir foo
root=self+foo
# symlinks a directory recursively into our FHS tree
def __ln start
start=path+start
return unless start.directory?
root.find do |src|
next if src == root
root=Pathname.new HOMEBREW_PREFIX
start.find do |from|
next if from == start
dst=HOMEBREW_PREFIX+src.relative_path_from(self)
dst.extend ObserverPathnameExtension
prune=false
relative_path=from.relative_path_from path
to=root+relative_path
if from.file?
__symlink_relative_to from, to
elsif from.directory?
if src.file?
dst.make_relative_symlink src
elsif src.directory?
# no need to put .app bundles in the path, the user can just use
# spotlight, or the open command and actual mac apps use an equivalent
Find.prune if from.extname.to_s == '.app'
Find.prune if src.extname.to_s == '.app'
branch=from.relative_path_from start
case yield branch when :skip
Find.prune
when :mkpath
to.mkpath
@n+=1
else
__symlink_relative_to from, to
Find.prune
case yield src.relative_path_from(root)
when :skip then Find.prune
when :mkpath then dst.mkpath
else dst.make_relative_symlink src; Find.prune
end
end
end
end
public
def ln
# yeah indeed, you have to force anything you need in the main tree into
# these dirs REMEMBER that *NOT* everything needs to be in the main tree
# TODO consider using hardlinks
@n=0
__ln('etc') {:mkpath}
__ln('bin') {:link}
__ln('lib') {|path| :mkpath if ['pkgconfig','php'].include? path.to_s}
__ln('include') {:link}
mkpaths=(1..9).collect {|x| "man/man#{x}"} <<'man'<<'doc'<<'locale'<<'info'<<'aclocal'
__ln('share') {|path| :mkpath if mkpaths.include? path.to_s}
return @n
end
end
end

View File

@ -68,6 +68,24 @@ class Pathname
return File.basename(to_s, extname)
end
# I don't trust the children.length == 0 check particularly, not to mention
# it is slow to enumerate the whole directory just to see if it is empty,
# instead rely on good ol' libc and the filesystem
def rmdir_if_possible
rmdir
rescue SystemCallError => e
raise unless e.errno == Errno::ENOTEMPTY::Errno
end
def chmod_R perms
require 'fileutils'
FileUtils.chmod_R perms, to_s
end
def abv
`find #{to_s} -type f | wc -l`.strip+' files, '+`du -hd0 #{to_s} | cut -d"\t" -f1`.strip
end
def version
# eg. boost_1_39_0
/((\d+_)+\d+)$/.match stem
@ -100,3 +118,39 @@ class Pathname
end
end
end
# sets $n and $d so you can observe creation of stuff
module ObserverPathnameExtension
def unlink
super
puts "rm #{to_s}" if ARGV.verbose?
$n+=1
end
def rmdir
super
puts "rmdir #{to_s}" if ARGV.verbose?
$d+=1
end
def resolved_path_exists?
(dirname+readlink).exist?
end
def mkpath
super
puts "mkpath #{to_s}" if ARGV.verbose?
$d+=1
end
def make_relative_symlink src
dirname.mkpath
Dir.chdir dirname do
# TODO use Ruby function so we get exceptions
# NOTE Ruby functions may work, but I had a lot of problems
rv=system 'ln', '-sf', src.relative_path_from(dirname)
raise "Could not create symlink #{to_s}" unless rv and $? == 0
puts "ln #{to_s}" if ARGV.verbose?
$n+=1
end
end
end
$n=0
$d=0

View File

@ -1,9 +1,8 @@
#!/usr/bin/ruby
$:.unshift File.dirname(__FILE__)
require 'pathname+yeast'
require 'formula'
require 'keg'
require 'pathname+yeast'
require 'stringio'
require 'utils'
# these are defined in env.rb usually, but we don't want to break our actual
@ -17,6 +16,7 @@ HOMEBREW_CELLAR.mkpath
raise "HOMEBREW_CELLAR couldn't be created!" unless HOMEBREW_CELLAR.directory?
at_exit { HOMEBREW_PREFIX.parent.rmtree }
require 'test/unit' # must be after at_exit
require 'ARGV+yeast' # needs to be after test/unit to avoid conflict with OptionsParser
class MockFormula <Formula
@ -26,12 +26,16 @@ class MockFormula <Formula
end
end
class MostlyAbstractFormula <AbstractFormula
@url=''
end
class TestBall <Formula
def initialize
@url="file:///#{Pathname.new(__FILE__).parent.realpath}/testball-0.1.tbz"
super "testball"
end
def install
prefix.install "bin"
prefix.install "libexec"
@ -51,16 +55,28 @@ class TestBallOverrideBrew <Formula
super "foo"
end
def brew
puts "We can't override brew"
# We can't override brew
end
end
class TestScriptFileFormula <ScriptFileFormula
@url="file:///#{Pathname.new(__FILE__).realpath}"
@version="1"
def initialize
super
@name='test-script-formula'
end
end
def nostdout
tmp=$stdout
require 'stringio'
tmpo=$stdout
tmpe=$stderr
$stdout=StringIO.new
yield
$stdout=tmp
ensure
$stdout=tmpo
end
@ -162,9 +178,10 @@ class BeerTasting <Test::Unit::TestCase
def test_install
f=TestBall.new
assert_equal Formula.path(f.name), f.path
assert !f.installed?
nostdout do
nostdout do
f.brew do
f.install
end
@ -180,17 +197,29 @@ class BeerTasting <Test::Unit::TestCase
assert !(f.prefix+'main.c').exist?
assert f.installed?
keg=Keg.new f
keg.ln
keg=Keg.new f.prefix
keg.link
assert_equal 2, HOMEBREW_PREFIX.children.length
assert (HOMEBREW_PREFIX+'bin').directory?
assert_equal 3, (HOMEBREW_PREFIX+'bin').children.length
keg.rm
assert !keg.path.exist?
keg.uninstall
assert !keg.exist?
assert !f.installed?
end
def test_script_install
f=TestScriptFileFormula.new
nostdout do
f.brew do
f.install
end
end
assert_equal 1, f.bin.children.length
end
def test_md5
assert_nothing_raised { nostdout { TestBallValidMd5.new.brew {} } }
end
@ -213,10 +242,17 @@ class BeerTasting <Test::Unit::TestCase
path.dirname.mkpath
`echo "require 'brewkit'; class #{classname} <Formula; @url=''; end" > #{path}`
assert_not_nil Formula.create(FOOBAR)
assert_not_nil Formula.factory(FOOBAR)
end
def test_cant_override_brew
assert_raises(RuntimeError) { TestBallOverrideBrew.new }
end
def test_abstract_formula
f=MostlyAbstractFormula.new
assert_nil f.name
assert_raises(RuntimeError) { f.prefix }
nostdout { assert_raises(ExecutionError) { f.brew } }
end
end

View File

@ -22,5 +22,24 @@ end
# shows a warning in delicious pink
def opoo warning
puts "WARNING \033[1;35m#{warning}\033[0;0m"
puts "\033[1;35m==>\033[0;0;1m Warning\033[0;0m: #{warning}"
end
def onoe error
puts "\033[1;31m==>\033[0;0;1m Error\033[0;0m: #{error}"
end
def pretty_duration s
return "#{(s*1000).to_i} milliseconds" if s < 3
return "#{s.to_i} seconds" if s < 10*60
return "#{(s/60).to_i} minutes"
end
def interactive_shell
pid=fork
if pid.nil?
exec ENV['SHELL']
else
Process.wait pid
end
end

5
README
View File

@ -53,7 +53,10 @@ Here's why you may prefer Homebrew to the alternatives:
We optimise for Leopard Intel, binaries are stripped, compile flags
tweaked. Nobody wants crappy, slow software. Apart from MacPorts and Fink.
8. Integration with existing OS X technologies
8. Making the most of OS X
Homebrew knows how many cores you have thanks to RubyCocoa, so it makes
sure when it builds it uses all of them, (unless you don't want it to of
course).
Homebrew integrates with Ruby gems, CPAN and Python disttools. These tools
exist already and do the job great. We don't reinvent the wheel, we just
improve it by making these tools install with more management options.

349
bin/brew
View File

@ -1,306 +1,135 @@
#!/usr/bin/ruby
$:.unshift __FILE__+'/../../Library/Homebrew'
require 'env'
require 'find'
$:.unshift ENV['RUBYLIB']=File.expand_path(__FILE__+'/../../Library/Homebrew')
PRISTINE_ARGV=ARGV.dup
require 'pathname+yeast'
require 'ARGV+yeast'
require 'utils'
require 'brew.h'
# TODO if whoami == root then use /Library/Caches/Homebrew instead
HOMEBREW_CACHE=Pathname.new("~/Library/Caches/Homebrew").expand_path
HOMEBREW_PREFIX=Pathname.new(__FILE__).dirname.parent.cleanpath
HOMEBREW_CELLAR=HOMEBREW_PREFIX+'Cellar'
HOMEBREW_VERSION='0.4'
HOMEBREW_WWW='http://bit.ly/Homebrew'
HOMEBREW_USER_AGENT="Homebrew #{HOMEBREW_VERSION} (Ruby #{VERSION}; Mac OS X 10.5 Leopard)"
# often causes Ruby to throw exception ffs
if %w[/ /usr].include? HOMEBREW_PREFIX.to_s then abort <<-troba
You have placed Homebrew at the prefix: #{HOMEBREW_PREFIX}
This is not currently supported. Voice your support for this feature at:
#{HOMEBREW_WWW}
troba
end
# Pathname often throws if CWD doesn't exist
Dir.chdir '/' unless File.directory? ENV['PWD']
######################################################################## funcs
# remove symlinks that no longer point to files
def prune
n=0
dirs=Array.new
HOMEBREW_PREFIX.find do |path|
if path.directory?
name=path.relative_path_from(HOMEBREW_PREFIX).to_s
if name == '.git' or name == 'Cellar' or name == 'Library'
Find.prune
else
dirs<<path
end
elsif path.symlink?
resolved_path=path.dirname+path.readlink
unless resolved_path.exist?
path.unlink
n+=1
end
end
end
dirs.sort.reverse_each do |d|
if d.children.length == 0
d.rmdir
n+=1
end
end
return n
end
# we actually remove formulae from ARGV so that any other analysis of ARGV
# only includes relevent arguments
# TODO require will throw if no formula, so we should catch no?
def extract_named_args
args=Array.new
ARGV.delete_if do |arg|
if arg[0,1] == '-'
false
else
args<<arg
true
end
end
raise "Expecting the name of a keg or formula, eg:\n==> brew #{PRISTINE_ARGV.join ' '} wget" if args.empty?
return args
end
def extract_kegs
require 'keg'
kegs=extract_named_args.collect {|name| Keg.new name}
return kegs
end
def abv keg=nil
path=keg ? keg.path : HOMEBREW_CELLAR
if path.directory?
`find #{path} -type f | wc -l`.strip+' files, '+`du -hd0 #{path} | cut -d"\t" -f1`.strip
else
''
end
end
def install formula
require 'keg'
beginning = Time.now
formula.brew do
if ARGV.include? '--interactive'
ohai "Entering interactive mode"
puts "Type `exit' to return and finalize the installation"
puts "Install to this prefix: #{formula.prefix}"
pid=fork
if pid.nil?
exec 'bash'
else
Process.wait pid
end
elsif ARGV.include? '--help'
ohai './configure --help'
puts `./configure --help`
exit
else
formula.prefix.mkpath
formula.install
%w[README ChangeLog COPYING LICENSE COPYRIGHT AUTHORS].each do |file|
formula.prefix.install file if File.file? file
end
end
end
raise "Nothing was installed to #{formula.prefix}" unless formula.installed?
ohai 'Finishing up'
keg=Keg.new formula
keg.clean
keg.ln
if formula.caveats
ohai "Caveats"
puts formula.caveats
ohai "Summary"
end
puts "#{keg.path}: "+abv(keg)+", built in #{pretty_duration Time.now-beginning}"
rescue Exception
formula.prefix.rmtree if formula.prefix.directory?
raise
end
def mk url, mode='make'
require 'formula'
path=Pathname.new(url)
/(.*?)[-_.]?#{path.version}/.match path.basename
raise "Couldn't parse name from #{url}" if $1.nil? or $1.empty?
path=Formula.path $1
raise "#{path} already exists!" if File.exist? path
f=File.new path, 'w'
f.puts "require 'brewkit'"
f.puts
f.puts "class #{Formula.class $1} <Formula"
f.puts " @url='#{url}'"
f.puts " @homepage=''" # second because you fill in these two first
f.puts " @md5=''"
f.puts
if mode == "cmake"
f.puts " def deps"
f.puts " BinaryDep.new 'cmake'"
f.puts " end"
f.puts
end
f.puts " def install"
if mode == "make"
f.puts " system \"./configure --disable-debug --prefix='\#{prefix}'\""
f.puts " system \"make install\""
elsif mode == "cmake"
f.puts " system \"cmake -G 'Unix Makefiles' -DCMAKE_INSTALL_PREFIX=\#{prefix}\""
f.puts " system \"make\""
f.puts " system \"make install\""
end
f.puts " end"
f.print "end"
f.close
return path
end
def prefix
Pathname.new(__FILE__).dirname.parent.expand_path
end
def usage
name=File.basename $0
<<-EOS
Usage: #{name} command [formula] ...
Usage: #{name} [--prefix] [--cache] [--version]
Usage: #{name} [--verbose]
Commands:
install formula ... [--debug] [--interactive]
rm formula ...
list formula ...
ln formula ...
info [formula]
mk url
prune
EOS
end
######################################################################## utils
def pretty_duration s
return "#{(s*1000).to_i} milliseconds" if s < 3
return "#{s.to_i} seconds" if s < 10*60
return "#{(s/60).to_i} minutes"
end
######################################################################### impl
begin
case ARGV.shift
when '--prefix' then puts prefix
when '--prefix' then puts HOMEBREW_PREFIX
when '--cache' then puts HOMEBREW_CACHE
when '-h', '--help', '--usage', '-?' then puts usage
when '-h', '--help', '--usage', '-?' then puts ARGV.usage
when '-v', '--version' then puts HOMEBREW_VERSION
when 'macports' then exec "open 'http://www.macports.org/ports.php?by=name&substr=#{ARGV.shift}'"
when 'home', 'homepage'
require 'formula'
homepages=extract_named_args.collect {|name| Formula.create(name).homepage}
exec "open #{homepages.join' '}"
if ARGV.named.empty?
exec "open", HOMEBREW_WWW
else
exec "open", *ARGV.formulae.collect {|f| f.homepage}
end
when 'ls', 'list'
dirs=extract_kegs.collect {|keg| keg.path}
exec "find #{dirs.join' '} -not -type d -print"
exec "find", *ARGV.kegs.concat(%w[-not -type d -print])
when 'edit'
if ARGV.empty?
d=HOMEBREW_PREFIX
exec "mate #{Dir["#{d}/Library/*"].join' '} #{d}/bin/brew #{d}/README"
exec "mate", *Dir["#{HOMEBREW_PREFIX}/Library/*"]<<
"#{HOMEBREW_PREFIX}/bin/brew"<<
"#{HOMEBREW_PREFIX}/README"
else
require 'formula'
paths=extract_named_args.collect {|name| Formula.path(name).to_s.gsub ' ', '\\ '}
exec "mate #{paths.join ' '}"
exec "mate", *ARGV.formulae.collect {|f| f.path}
end
when 'install'
require 'formula'
extract_named_args.each do |name|
f=Formula.create(name)
raise "#{f.name} already installed!\n==> #{f.prefix}" if f.installed? unless ARGV.include? '--force'
install f
require 'keg'
ARGV.formulae.each do |f|
raise "#{f.name} is already installed" if f.installed? unless ARGV.force?
start_time=Time.now
begin
install f
if f.caveats
ohai "Caveats"
puts f.caveats
puts
end
ohai 'Finishing up'
clean f
raise "Nothing was installed to #{f.prefix}" unless f.installed?
Keg.new(f.prefix).link
rescue
f.prefix.rmtree if f.prefix.directory?
raise
end
puts "#{f.prefix}: "+f.prefix.abv+", built in #{pretty_duration Time.now-start_time}"
end
when 'ln', 'link'
n=0
(kegs=extract_kegs).each do |keg|
n+=nn=keg.ln
puts "Created #{nn} links for #{keg.name}" if kegs.length > 1
end
puts "Created #{n} links"
ARGV.kegs.each {|keg| puts "#{keg.link} links created for #{keg}"}
when 'rm', 'uninstall'
extract_kegs.each do |keg|
puts "Removing #{keg.name}..."
keg.rm
when 'unlink'
ARGV.kegs.each {|keg| puts "#{keg.unlink} links removed for #{keg}"}
when 'rm', 'uninstall', 'remove'
ARGV.kegs.each do |keg|
puts "Uninstalling #{keg}..."
keg.uninstall
end
print "Pruning #{prefix}/..."
puts " #{prune} symbolic links pruned"
prune
when 'up', 'update'
puts "Reserved command"
when 'prune'
puts "Pruned #{prune} symbolic links"
prune
when 'mk', 'make'
mode = "make"
if ARGV.length > 0
if ARGV[0] == '--cmake'
ARGV.shift
mode = "cmake"
end
end
paths=ARGV.collect {|arg| mk arg, mode}
if paths.empty?
raise "Invalid URL"
elsif Kernel.system "which mate > /dev/null" and $? == 0
paths=paths.collect {|path| path.to_s.gsub " ", "\\ "}
exec "mate #{paths.join ' '}"
if ARGV.include? '--macports'
exec "open", "http://www.macports.org/ports.php?by=name&substr=#{ARGV.next}"
else
puts paths.join("\n")
exec "mate", *ARGV.named.collect {|name| make name}
end
when 'info', 'abv'
if ARGV.empty?
puts `ls #{HOMEBREW_CELLAR} | wc -l`.strip+" kegs, "+abv
if ARGV.named.empty?
puts `ls #{HOMEBREW_CELLAR} | wc -l`.strip+" kegs, "+HOMEBREW_CELLAR.abv
elsif ARGV[0][0..6] == 'http://'
puts Pathname.new(ARGV.shift).version
else
require 'formula'
#TODO show outdated status and that
frm=Formula.create(extract_named_args[0])
puts "#{frm.name} #{frm.version}"
puts frm.homepage
if frm.installed?
require 'keg'
keg=Keg.new frm
puts "#{abv keg} (installed to #{keg.path})"
end
if frm.caveats
ohai 'Caveats'
puts frm.caveats
end
ARGV.named.each {|name| info name}
end
else
puts usage
puts ARGV.usage
end
rescue StandardError, Interrupt => e
if ARGV.include? '--verbose' or ENV['HOMEBREW_DEBUG']
raise
elsif e.kind_of? Interrupt
puts # seeimgly a newline is typical
exit 130
elsif e.kind_of? StandardError and not e.kind_of? NameError
puts "\033[1;31mError\033[0;0m: #{e}"
exit 1
rescue SystemExit
ohai "Kernel.exit" if ARGV.verbose?
rescue Interrupt => e
puts # seemingly a newline is typical
exit 130
rescue SystemCallError, RuntimeError => e
if ARGV.verbose? or ARGV.debug?
onoe e.inspect
puts e.backtrace
else
raise
onoe e
end
exit 1
rescue Exception => e
onoe "Homebrew has failed you :("
puts "Please report this bug at: #{HOMEBREW_WWW}"
puts "Please include this backtrace:"
ohai e.inspect
puts e.backtrace
end