require 'download_strategy' require 'fileutils' require 'formula_support' require 'hardware' # Derive and define at least @url, see Library/Formula for examples class Formula include FileUtils attr_reader :name, :path, :url, :version, :homepage, :specs, :downloader attr_reader :standard, :unstable attr_reader :bottle_url, :bottle_sha1, :head # The build folder, usually in /tmp. # Will only be non-nil during the stage method. attr_reader :buildpath # Homebrew determines the name def initialize name='__UNKNOWN__', path=nil set_instance_variable 'homepage' set_instance_variable 'url' set_instance_variable 'bottle_url' set_instance_variable 'bottle_sha1' set_instance_variable 'head' set_instance_variable 'specs' set_instance_variable 'standard' set_instance_variable 'unstable' if @head and (not @url or ARGV.build_head?) @url = @head @version = 'HEAD' @spec_to_use = @unstable else if @standard.nil? @spec_to_use = SoftwareSpecification.new(@url, @specs) else @spec_to_use = @standard end end raise "No url provided for formula #{name}" if @url.nil? @name=name validate_variable :name @path = path.nil? ? nil : Pathname.new(path) set_instance_variable 'version' @version ||= @spec_to_use.detect_version validate_variable :version if @version CHECKSUM_TYPES.each { |type| set_instance_variable type } @downloader=download_strategy.new @spec_to_use.url, name, version, @spec_to_use.specs end # if the dir is there, but it's empty we consider it not installed def installed? return installed_prefix.children.length > 0 rescue return false end def bottle_up_to_date? !bottle_url.nil? && Pathname.new(bottle_url).version == version end def explicitly_requested? # `ARGV.formulae` will throw an exception if it comes up with an empty list. # FIXME: `ARGV.formulae` shouldn't be throwing exceptions, see issue #8823 return false if ARGV.named.empty? ARGV.formulae.include? self end def linked_keg HOMEBREW_REPOSITORY/'Library/LinkedKegs'/@name end def installed_prefix head_prefix = HOMEBREW_CELLAR+@name+'HEAD' if @version == 'HEAD' || head_prefix.directory? head_prefix else prefix end end def path if @path.nil? return self.class.path(name) else return @path end 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; prefix+'share/doc'+name end def include; prefix+'include' end def info; prefix+'share/info' end def lib; prefix+'lib' end def libexec; prefix+'libexec' end def man; prefix+'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 # 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 # A version of mkdir that also changes to that folder in a block def mkdir name, &block FileUtils.mkdir name if block_given? FileUtils.chdir name do yield end end end # Use the @spec_to_use to detect the download strategy. # Can be overriden to force a custom download strategy def download_strategy @spec_to_use.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? self.class.keg_only_reason || false end def fails_with_llvm? llvm = self.class.fails_with_llvm_reason if llvm if llvm.build and MacOS.llvm_build_version.to_i > llvm.build.to_i false else llvm end 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? 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 handle_llvm_failure(fails_with_llvm?) if fails_with_llvm? stage do begin patch # we allow formulas to do anything they want to the Ruby process # so load any deps before this point! And exit asap afterwards yield self rescue Interrupt, RuntimeError, SystemCallError => e unless ARGV.debug? logs = File.expand_path '~/Library/Logs/Homebrew/' if File.exist? 'config.log' mkdir_p logs mv 'config.log', logs end if File.exist? 'CMakeCache.txt' mkdir_p logs mv 'CMakeCache.txt', logs end raise end onoe e.inspect puts e.backtrace ohai "Rescuing build..." if (e.was_running_configure? rescue false) and File.exist? 'config.log' puts "It looks like an autotools configure failed." puts "Gist 'config.log' and any error output when reporting an issue." puts end puts "When you exit this shell Homebrew will attempt to finalise the installation." puts "If nothing is installed or the shell exits with a non-zero error code," puts "Homebrew will abort. The installation prefix is:" puts prefix interactive_shell self end 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. # 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_parameters "-DCMAKE_INSTALL_PREFIX='#{prefix}' -DCMAKE_BUILD_TYPE=None -Wno-dev" end def handle_llvm_failure llvm if ENV.compiler == :llvm # version 2336 is the latest version as of Xcode 4.2, so it is the # latest version we have tested against so we will switch to GCC and # bump this integer when Xcode 4.3 is released. TODO do that! if llvm.build.to_i >= 2336 if MacOS.xcode_version < "4.2" opoo "Formula will not build with LLVM, using GCC" ENV.gcc else opoo "Formula will not build with LLVM, trying Clang" ENV.clang end return end opoo "Building with LLVM, but this formula is reported to not work with LLVM:" puts puts llvm.reason puts puts <<-EOS.undent We are continuing anyway so if the build succeeds, please open a ticket with the following information: #{MacOS.llvm_build_version}-#{MACOS_VERSION}. So that we can update the formula accordingly. Thanks! EOS puts if MacOS.xcode_version < "4.2" puts "If it doesn't work you can: brew install --use-gcc" else puts "If it doesn't work you can try: brew install --use-clang" end puts end 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 # an array of all Formula, instantiated def self.all map{ |f| f } end def self.map rv = [] each{ |f| rv << yield(f) } rv end def self.each names.each do |n| begin yield Formula.factory(n) rescue # Don't let one broken formula break commands. But do complain. onoe "Formula #{n} will not import." end end 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 # Cast pathnames to strings. 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? "/" # 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 target_file = HOMEBREW_CACHE_FORMULA+name name = name.basename(".rb").to_s HOMEBREW_CACHE_FORMULA.mkpath FileUtils.rm target_file, :force => true curl url, '-o', target_file require target_file install_type = :from_url else name = Formula.canonical_name(name) # If name was a path or mapped to a cached formula if name.include? "/" require name path = Pathname.new(name) name = path.stem install_type = :from_path target_file = path.to_s else # For names, map to the path and then require require Formula.path(name) install_type = :from_name end end begin klass_name = self.class_s(name) 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 return klass.new(name) if install_type == :from_name return klass.new(name, target_file) rescue LoadError raise FormulaUnavailableError.new(name) end def self.path name HOMEBREW_REPOSITORY+"Library/Formula/#{name.downcase}.rb" end def mirrors self.class.mirrors or [] end def deps self.class.deps or [] end def external_deps self.class.external_deps or {} end # deps are in an installable order # which means if a depends on b then b will be ordered before a in this list def recursive_deps Formula.expand_deps(self).flatten.uniq end def self.expand_deps f f.deps.map do |dep| dep = Formula.factory dep expand_deps(dep) << dep end 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 pretty_args.delete "--disable-dependency-tracking" if cmd == "./configure" and not ARGV.verbose? 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 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 exit! 1 # never gets here unless exec threw or failed end wr.close out = '' out << rd.read until rd.eof? Process.wait unless $?.success? puts out raise end end removed_ENV_variables.each do |key, value| ENV[key] = value # ENV.kind_of? Hash # => false end if removed_ENV_variables rescue raise BuildError.new(self, cmd, args, $?) end private # Create a temporary directory then yield. When the block returns, # recursively delete the temporary directory. def mktemp # I used /tmp rather than `mktemp -td` because that generates a directory # name with exotic characters like + in it, and these break badly written # scripts that don't escape strings before trying to regexp them :( # If the user has FileVault enabled, then we can't mv symlinks from the # /tmp volume to the other volume. So we let the user override the tmp # prefix if they need to. tmp_prefix = ENV['HOMEBREW_TEMP'] || '/tmp' tmp=Pathname.new `/usr/bin/mktemp -d #{tmp_prefix}/homebrew-#{name}-#{version}-XXXX`.strip raise "Couldn't create build sandbox" if not tmp.directory? or $? != 0 begin wd=Dir.pwd Dir.chdir tmp yield ensure Dir.chdir wd tmp.rmtree end end CHECKSUM_TYPES=[:md5, :sha1, :sha256].freeze public # For brew-fetch and others. def fetch downloader = @downloader # Don't attempt mirrors if this install is not pointed at a "stable" URL. # This can happen when options like `--HEAD` are invoked. mirror_list = @spec_to_use == @standard ? mirrors : [] # Ensure the cache exists HOMEBREW_CACHE.mkpath begin fetched = downloader.fetch rescue CurlDownloadStrategyError => e raise e if mirror_list.empty? puts "Trying a mirror..." url, specs = mirror_list.shift.values_at :url, :specs downloader = download_strategy.new url, name, version, specs retry end return fetched, downloader end # Detect which type of checksum is being used, or nil if none def checksum_type CHECKSUM_TYPES.detect { |type| instance_variable_defined?("@#{type}") } end # For FormulaInstaller. def verify_download_integrity fn, *args require 'digest' if args.length != 2 type = checksum_type || :md5 supplied = instance_variable_get("@#{type}") # Convert symbol to readable string type = type.to_s.upcase else supplied, type = args end hasher = Digest.const_get(type) hash = fn.incremental_hash(hasher) if supplied and not supplied.empty? message = <<-EOF #{type} mismatch Expected: #{supplied} Got: #{hash} Archive: #{fn} (To retry an incomplete download, remove the file above.) EOF raise message 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 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 # Only call `patches` once. # If there is code in `patches`, which is not recommended, we only # want to run that code once. the_patches = patches return if the_patches.nil? if not the_patches.kind_of? Hash # We assume -p1 patch_defns = { :p1 => the_patches } else patch_defns = the_patches end patch_list=[] n=0 patch_defns.each do |arg, urls| # DATA.each does each line, which doesn't work so great urls = [urls] unless urls.kind_of? Array urls.each do |url| p = {:filename => '%03d-homebrew.diff' % n+=1, :compression => false} if defined? DATA and url == DATA pn = Pathname.new p[:filename] pn.write(DATA.read.to_s.gsub("HOMEBREW_PREFIX", HOMEBREW_PREFIX)) elsif url =~ %r[^\w+\://] out_fn = p[:filename] case url when /\.gz$/ p[:compression] = :gzip out_fn += '.gz' when /\.bz2$/ p[:compression] = :bzip2 out_fn += '.bz2' end p[:curl_args] = [url, '-o', out_fn] else # it's a file on the local filesystem p[:filename] = url end p[:args] = ["-#{arg}", '-i', p[:filename]] patch_list << p end end return if patch_list.empty? external_patches = patch_list.collect{|p| p[:curl_args]}.select{|p| p}.flatten unless external_patches.empty? ohai "Downloading patches" # downloading all at once is much more efficient, especially for FTP curl(*external_patches) end ohai "Patching" patch_list.each do |p| case p[:compression] when :gzip then safe_system "/usr/bin/gunzip", p[:filename]+'.gz' when :bzip2 then safe_system "/usr/bin/bunzip2", p[:filename]+'.bz2' end # -f means it doesn't prompt the user if there are errors, if just # exits with non-zero status safe_system '/usr/bin/patch', '-f', *(p[:args]) end end def validate_variable name v = instance_variable_get("@#{name}") raise "Invalid @#{name}" if v.to_s.empty? or v =~ /\s/ end def set_instance_variable(type) unless instance_variable_defined? "@#{type}" class_value = self.class.send(type) instance_variable_set("@#{type}", class_value) if class_value end end def method_added method raise 'You cannot override Formula.brew' if method == 'brew' end class << self # The methods below define the formula DSL. attr_reader :standard, :unstable 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 :version, :homepage, :mirrors, :specs, :deps, :external_deps attr_rw :keg_only_reason, :fails_with_llvm_reason, :skip_clean_all attr_rw :bottle_url, :bottle_sha1 attr_rw(*CHECKSUM_TYPES) def head val=nil, specs=nil return @head if val.nil? @unstable = SoftwareSpecification.new(val, specs) @head = val @specs = specs end def url val=nil, specs=nil return @url if val.nil? @standard = SoftwareSpecification.new(val, specs) @url = val @specs = specs end def stable &block raise "url and md5 must be specified in a block" unless block_given? instance_eval &block unless ARGV.build_devel? or ARGV.build_head? end def devel &block raise "url and md5 must be specified in a block" unless block_given? if ARGV.build_devel? # clear out mirrors from the stable release @mirrors = nil instance_eval &block end end def bottle url=nil, &block if block_given? eval <<-EOCLASS module BottleData def self.url url; @url = url; end def self.sha1 sha1; @sha1 = sha1; end def self.return_data; [@url,@sha1]; end end EOCLASS BottleData.instance_eval &block @bottle_url, @bottle_sha1 = BottleData.return_data end end def mirror val, specs=nil @mirrors ||= [] @mirrors << {:url => val, :specs => specs} # Added the uniq after some inspection with Pry---seems `mirror` gets # called three times. The first two times only one copy of the input is # left in `@mirrors`. On the final call, two copies are present. This # happens with `@deps` as well. Odd. @mirrors.uniq! end def depends_on name @deps ||= [] @external_deps ||= {:python => [], :perl => [], :ruby => [], :jruby => [], :chicken => [], :rbx => [], :node => [], :lua => []} case name when String, Formula @deps << name when Hash key, value = name.shift case value when :python, :perl, :ruby, :jruby, :chicken, :rbx, :node, :lua @external_deps[value] << key when :optional, :recommended, :build @deps << key else raise "Unsupported dependency type #{value}" end when Symbol opoo "#{self.name} -- #{name}: Using symbols for deps is deprecated; use a string instead" @deps << name.to_s else raise "Unsupported type #{name.class}" end end def skip_clean paths if paths == :all @skip_clean_all = true return end @skip_clean_paths ||= [] [paths].flatten.each do |p| @skip_clean_paths << p.to_s unless @skip_clean_paths.include? p.to_s 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_llvm msg=nil, data=nil @fails_with_llvm_reason = FailsWithLLVM.new(msg, data) end end end # See youtube-dl.rb for an example class ScriptFileFormula < Formula def install bin.install Dir['*'] end end # See flac.rb for an example class GithubGistFormula < ScriptFileFormula def initialize name='__UNKNOWN__', path=nil super name, path @version=File.basename(File.dirname(url))[0,6] end end