272 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| #  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
 | |
| 
 | |
| 
 | |
| # Derive and define at least @url, see Library/Formula for examples
 | |
| class Formula
 | |
|   # Homebrew determines the name
 | |
|   def initialize name='__UNKNOWN__'
 | |
|     @url=self.class.url unless @url
 | |
|     raise if @url.nil?
 | |
|     @name=name
 | |
|     validate_variable :name
 | |
|     @version=self.class.version unless @version
 | |
|     @version=Pathname.new(@url).version unless @version
 | |
|     validate_variable :version if @version
 | |
|     @homepage=self.class.homepage unless @homepage
 | |
|     @md5=self.class.md5 unless @md5
 | |
|     @sha1=self.class.sha1 unless @sha1
 | |
|   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
 | |
|     validate_variable :name
 | |
|     validate_variable :version
 | |
|     HOMEBREW_CELLAR+@name+@version
 | |
|   end
 | |
| 
 | |
|   def path
 | |
|     self.class.path name
 | |
|   end
 | |
| 
 | |
|   attr_reader :url, :version, :homepage, :name
 | |
| 
 | |
|   def bin; prefix+'bin' end
 | |
|   def sbin; prefix+'sbin' 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
 | |
| 
 | |
|   # reimplement if we don't autodetect the download strategy you require
 | |
|   def download_strategy
 | |
|     case url
 | |
|       when %r[^svn://] then SubversionDownloadStrategy
 | |
|       when %r[^git://] then GitDownloadStrategy
 | |
|       when %r[^http://(.+?\.)?googlecode\.com/svn] then SubversionDownloadStrategy
 | |
|       else HttpDownloadStrategy
 | |
|     end
 | |
|   end
 | |
|   # tell the user about any caveats regarding this package
 | |
|   def caveats; nil end
 | |
|   # patches are automatically applied after extracting the tarball
 | |
|   # return an array of strings, or if you need a patch level other than -p0
 | |
|   # 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']
 | |
|   #   }
 | |
|   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
 | |
|     validate_variable :name
 | |
|     validate_variable :version
 | |
|     
 | |
|     stage do
 | |
|       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
 | |
|         raise "Non-zero exit status, installation aborted" if $? != 0
 | |
|       end
 | |
|     end
 | |
|   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
 | |
| 
 | |
|   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
 | |
| 
 | |
| 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
 | |
|   # creates a temporary directory then yields, when the block returns it
 | |
|   # recursively deletes the temporary directory
 | |
|   def mktemp
 | |
|     tmp=Pathname.new `mktemp -dt #{name}-#{version}`.strip
 | |
|     raise if not tmp.directory? or $? != 0
 | |
|     begin
 | |
|       wd=Dir.pwd
 | |
|       Dir.chdir tmp
 | |
|       yield
 | |
|     ensure
 | |
|       Dir.chdir wd
 | |
|       tmp.rmtree
 | |
|     end
 | |
|   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 stage
 | |
|     ds=download_strategy.new url, name, version
 | |
|     HOMEBREW_CACHE.mkpath
 | |
|     dl=ds.fetch
 | |
|     verify_download_integrity dl if dl.kind_of? Pathname
 | |
|     mktemp do
 | |
|       ds.stage
 | |
|       yield
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def patch
 | |
|     return if patches.empty?
 | |
|     ohai "Patching"
 | |
|     if patches.kind_of? Hash
 | |
|       patch_args=[]
 | |
|       curl_args=[]
 | |
|       n=0
 | |
|       patches.each do |arg, urls|
 | |
|         urls.each do |url|
 | |
|           dst='%03d-homebrew.patch' % n+=1
 | |
|           curl_args<<url<<'-o'<<dst
 | |
|           patch_args<<["-#{arg}",'-i',dst]
 | |
|         end
 | |
|       end
 | |
|       # downloading all at once is much more efficient, espeically for FTP
 | |
|       curl *curl_args
 | |
|       patch_args.each do |args|
 | |
|         # -f means it doesn't prompt the user if there are errors, if just
 | |
|         # exits with non-zero status
 | |
|         safe_system 'patch', '-f', *args
 | |
|       end
 | |
|     else
 | |
|       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
 | |
| 
 | |
|   def validate_variable name
 | |
|     v=eval("@#{name}")
 | |
|     raise "Invalid @#{name}" if v.nil? or v.empty? or v =~ /\s/
 | |
|   end
 | |
| 
 | |
|   def method_added method
 | |
|     raise 'You cannot override Formula.brew' if method == 'brew'
 | |
|   end
 | |
| 
 | |
|   class <<self
 | |
|     attr_reader :url, :svnurl, :version, :homepage, :md5, :sha1
 | |
|   end
 | |
| end
 | |
| 
 | |
| # see ack.rb for an example usage
 | |
| class ScriptFileFormula <Formula
 | |
|   def install
 | |
|     bin.install Dir['*']
 | |
|   end
 | |
| end
 | |
| 
 | |
| # see flac.rb for example usage
 | |
| class GithubGistFormula <ScriptFileFormula
 | |
|   def initialize name=nil
 | |
|     @version=File.basename(File.dirname(url))[0,6]
 | |
|     super name
 | |
|   end
 | |
| end
 | 
