aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/dev-cmd
diff options
context:
space:
mode:
authorMike McQuaid2016-09-05 21:37:02 +0100
committerMike McQuaid2016-09-08 20:46:37 +0100
commitaf8605ea4ba1d9856c055c8c76b447e030540e3f (patch)
treed093b11340406c21a4b873a80effe3b068fd07d4 /Library/Homebrew/dev-cmd
parent4f6bae46f9c0f7b713cdbb999318460135f423de (diff)
downloadbrew-af8605ea4ba1d9856c055c8c76b447e030540e3f.tar.bz2
Move developer-focused commands to dev-cmd.
Diffstat (limited to 'Library/Homebrew/dev-cmd')
-rw-r--r--Library/Homebrew/dev-cmd/audit.rb1361
-rw-r--r--Library/Homebrew/dev-cmd/bottle.rb458
-rw-r--r--Library/Homebrew/dev-cmd/create.rb218
-rw-r--r--Library/Homebrew/dev-cmd/edit.rb50
-rw-r--r--Library/Homebrew/dev-cmd/man.rb88
-rw-r--r--Library/Homebrew/dev-cmd/pull.rb575
-rw-r--r--Library/Homebrew/dev-cmd/tap-readme.rb36
-rw-r--r--Library/Homebrew/dev-cmd/test.rb89
-rw-r--r--Library/Homebrew/dev-cmd/tests.rb63
9 files changed, 2938 insertions, 0 deletions
diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb
new file mode 100644
index 000000000..6d1fa055f
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/audit.rb
@@ -0,0 +1,1361 @@
+#: * `audit` [`--strict`] [`--online`] [`--new-formula`] [`--display-cop-names`] [`--display-filename`] [<formulae>]:
+#: Check <formulae> for Homebrew coding style violations. This should be
+#: run before submitting a new formula.
+#:
+#: If no <formulae> are provided, all of them are checked.
+#:
+#: If `--strict` is passed, additional checks are run, including RuboCop
+#: style checks.
+#:
+#: If `--online` is passed, additional slower checks that require a network
+#: connection are run.
+#:
+#: If `--new-formula` is passed, various additional checks are run that check
+#: if a new formula is eligable for Homebrew. This should be used when creating
+#: new formulae and implies `--strict` and `--online`.
+#:
+#: If `--display-cop-names` is passed, the RuboCop cop name for each violation
+#: is included in the output.
+#:
+#: If `--display-filename` is passed, every line of output is prefixed with the
+#: name of the file or formula being audited, to make the output easy to grep.
+#:
+#: `audit` exits with a non-zero status if any errors are found. This is useful,
+#: for instance, for implementing pre-commit hooks.
+
+# Undocumented options:
+# -D activates debugging and profiling of the audit methods (not the same as --debug)
+
+require "formula"
+require "formula_versions"
+require "utils"
+require "extend/ENV"
+require "formula_cellar_checks"
+require "official_taps"
+require "cmd/search"
+require "cmd/style"
+require "date"
+
+module Homebrew
+ def audit
+ if ARGV.switch? "D"
+ Homebrew.inject_dump_stats!(FormulaAuditor, /^audit_/)
+ end
+
+ formula_count = 0
+ problem_count = 0
+
+ new_formula = ARGV.include? "--new-formula"
+ strict = new_formula || ARGV.include?("--strict")
+ online = new_formula || ARGV.include?("--online")
+
+ ENV.activate_extensions!
+ ENV.setup_build_environment
+
+ if ARGV.named.empty?
+ ff = Formula
+ files = Tap.map(&:formula_dir)
+ else
+ ff = ARGV.resolved_formulae
+ files = ARGV.resolved_formulae.map(&:path)
+ end
+
+ if strict
+ # Check style in a single batch run up front for performance
+ style_results = check_style_json(files, :realpath => true)
+ end
+
+ ff.each do |f|
+ options = { :new_formula => new_formula, :strict => strict, :online => online }
+ options[:style_offenses] = style_results.file_offenses(f.path) if strict
+ fa = FormulaAuditor.new(f, options)
+ fa.audit
+
+ next if fa.problems.empty?
+ fa.problems
+ formula_count += 1
+ problem_count += fa.problems.size
+ problem_lines = fa.problems.map { |p| "* #{p.chomp.gsub("\n", "\n ")}" }
+ if ARGV.include? "--display-filename"
+ puts problem_lines.map { |s| "#{f.path}: #{s}" }
+ else
+ puts "#{f.full_name}:", problem_lines.map { |s| " #{s}" }
+ end
+ end
+
+ unless problem_count.zero?
+ problems = "problem" + plural(problem_count)
+ formulae = "formula" + plural(formula_count, "e")
+ ofail "#{problem_count} #{problems} in #{formula_count} #{formulae}"
+ end
+ end
+end
+
+class FormulaText
+ def initialize(path)
+ @text = path.open("rb", &:read)
+ @lines = @text.lines.to_a
+ end
+
+ def without_patch
+ @text.split("\n__END__").first
+ end
+
+ def has_DATA?
+ /^[^#]*\bDATA\b/ =~ @text
+ end
+
+ def has_END?
+ /^__END__$/ =~ @text
+ end
+
+ def has_trailing_newline?
+ /\Z\n/ =~ @text
+ end
+
+ def =~(regex)
+ regex =~ @text
+ end
+
+ def include?(s)
+ @text.include? s
+ end
+
+ def line_number(regex, skip = 0)
+ index = @lines.drop(skip).index { |line| line =~ regex }
+ index ? index + 1 : nil
+ end
+
+ def reverse_line_number(regex)
+ index = @lines.reverse.index { |line| line =~ regex }
+ index ? @lines.count - index : nil
+ end
+end
+
+class FormulaAuditor
+ include FormulaCellarChecks
+
+ attr_reader :formula, :text, :problems
+
+ BUILD_TIME_DEPS = %W[
+ autoconf
+ automake
+ boost-build
+ bsdmake
+ cmake
+ godep
+ imake
+ intltool
+ libtool
+ pkg-config
+ scons
+ smake
+ sphinx-doc
+ swig
+ ]
+
+ FILEUTILS_METHODS = FileUtils.singleton_methods(false).map { |m| Regexp.escape(m) }.join "|"
+
+ def initialize(formula, options = {})
+ @formula = formula
+ @new_formula = !!options[:new_formula]
+ @strict = !!options[:strict]
+ @online = !!options[:online]
+ # Accept precomputed style offense results, for efficiency
+ @style_offenses = options[:style_offenses]
+ @problems = []
+ @text = FormulaText.new(formula.path)
+ @specs = %w[stable devel head].map { |s| formula.send(s) }.compact
+ end
+
+ def audit_style
+ return unless @style_offenses
+ display_cop_names = ARGV.include?("--display-cop-names")
+ @style_offenses.each do |offense|
+ problem offense.to_s(:display_cop_name => display_cop_names)
+ end
+ end
+
+ def component_problem(before, after, offset = 0)
+ problem "`#{before[1]}` (line #{before[0] + offset}) should be put before `#{after[1]}` (line #{after[0] + offset})"
+ end
+
+ # scan in the reverse direction for remaining problems but report problems
+ # in the forward direction so that contributors don't reverse the order of
+ # lines in the file simply by following instructions
+ def audit_components(reverse = true, previous_pair = nil)
+ component_list = [
+ [/^ include Language::/, "include directive"],
+ [/^ desc ["'][\S\ ]+["']/, "desc"],
+ [/^ homepage ["'][\S\ ]+["']/, "homepage"],
+ [/^ url ["'][\S\ ]+["']/, "url"],
+ [/^ mirror ["'][\S\ ]+["']/, "mirror"],
+ [/^ version ["'][\S\ ]+["']/, "version"],
+ [/^ (sha1|sha256) ["'][\S\ ]+["']/, "checksum"],
+ [/^ revision/, "revision"],
+ [/^ version_scheme/, "version_scheme"],
+ [/^ head ["'][\S\ ]+["']/, "head"],
+ [/^ stable do/, "stable block"],
+ [/^ bottle do/, "bottle block"],
+ [/^ devel do/, "devel block"],
+ [/^ head do/, "head block"],
+ [/^ bottle (:unneeded|:disable)/, "bottle modifier"],
+ [/^ keg_only/, "keg_only"],
+ [/^ option/, "option"],
+ [/^ depends_on/, "depends_on"],
+ [/^ conflicts_with/, "conflicts_with"],
+ [/^ (go_)?resource/, "resource"],
+ [/^ def install/, "install method"],
+ [/^ def caveats/, "caveats method"],
+ [/^ (plist_options|def plist)/, "plist block"],
+ [/^ test do/, "test block"],
+ ]
+ if previous_pair
+ previous_before = previous_pair[0]
+ previous_after = previous_pair[1]
+ end
+ offset = (previous_after && previous_after[0] && previous_after[0] >= 1) ? previous_after[0] - 1 : 0
+ present = component_list.map do |regex, name|
+ lineno = if reverse
+ text.reverse_line_number regex
+ else
+ text.line_number regex, offset
+ end
+ next unless lineno
+ [lineno, name]
+ end.compact
+ no_problem = true
+ present.each_cons(2) do |c1, c2|
+ if reverse
+ # scan in the forward direction from the offset
+ audit_components(false, [c1, c2]) if c1[0] > c2[0] # at least one more offense
+ elsif c1[0] > c2[0] && (offset == 0 || previous_pair.nil? || (c1[0] + offset) != previous_before[0] || (c2[0] + offset) != previous_after[0])
+ component_problem c1, c2, offset
+ no_problem = false
+ end
+ end
+ if no_problem && previous_pair
+ component_problem previous_before, previous_after
+ end
+ present
+ end
+
+ def audit_file
+ # Under normal circumstances (umask 0022), we expect a file mode of 644. If
+ # the user's umask is more restrictive, respect that by masking out the
+ # corresponding bits. (The also included 0100000 flag means regular file.)
+ wanted_mode = 0100644 & ~File.umask
+ actual_mode = formula.path.stat.mode
+ unless actual_mode == wanted_mode
+ problem format("Incorrect file permissions (%03o): chmod %03o %s",
+ actual_mode & 0777, wanted_mode & 0777, formula.path)
+ end
+
+ if text.has_DATA? && !text.has_END?
+ problem "'DATA' was found, but no '__END__'"
+ end
+
+ if text.has_END? && !text.has_DATA?
+ problem "'__END__' was found, but 'DATA' is not used"
+ end
+
+ if text =~ /inreplace [^\n]* do [^\n]*\n[^\n]*\.gsub![^\n]*\n\ *end/m
+ problem "'inreplace ... do' was used for a single substitution (use the non-block form instead)."
+ end
+
+ unless text.has_trailing_newline?
+ problem "File should end with a newline"
+ end
+
+ return unless @strict
+
+ present = audit_components
+
+ present.map!(&:last)
+ if present.include?("stable block")
+ %w[url checksum mirror].each do |component|
+ if present.include?(component)
+ problem "`#{component}` should be put inside `stable block`"
+ end
+ end
+ end
+ if present.include?("head") && present.include?("head block")
+ problem "Should not have both `head` and `head do`"
+ end
+ if present.include?("bottle modifier") && present.include?("bottle block")
+ problem "Should not have `bottle :unneeded/:disable` and `bottle do`"
+ end
+ end
+
+ def audit_class
+ if @strict
+ unless formula.test_defined?
+ problem "A `test do` test block should be added"
+ end
+ end
+
+ classes = %w[GithubGistFormula ScriptFileFormula AmazonWebServicesFormula]
+ klass = classes.find do |c|
+ Object.const_defined?(c) && formula.class < Object.const_get(c)
+ end
+
+ problem "#{klass} is deprecated, use Formula instead" if klass
+ end
+
+ # core aliases + tap alias names + tap alias full name
+ @@aliases ||= Formula.aliases + Formula.tap_aliases
+
+ def audit_formula_name
+ return unless @strict
+ # skip for non-official taps
+ return if formula.tap.nil? || !formula.tap.official?
+
+ name = formula.name
+ full_name = formula.full_name
+
+ if Formula.aliases.include? name
+ problem "Formula name conflicts with existing aliases."
+ return
+ end
+
+ if oldname = CoreTap.instance.formula_renames[name]
+ problem "'#{name}' is reserved as the old name of #{oldname}"
+ return
+ end
+
+ if !formula.core_formula? && Formula.core_names.include?(name)
+ problem "Formula name conflicts with existing core formula."
+ return
+ end
+
+ @@local_official_taps_name_map ||= Tap.select(&:official?).flat_map(&:formula_names).
+ reduce(Hash.new) do |name_map, tap_formula_full_name|
+ tap_formula_name = tap_formula_full_name.split("/").last
+ name_map[tap_formula_name] ||= []
+ name_map[tap_formula_name] << tap_formula_full_name
+ name_map
+ end
+
+ same_name_tap_formulae = @@local_official_taps_name_map[name] || []
+
+ if @online
+ @@remote_official_taps ||= OFFICIAL_TAPS - Tap.select(&:official?).map(&:repo)
+
+ same_name_tap_formulae += @@remote_official_taps.map do |tap|
+ Thread.new { Homebrew.search_tap "homebrew", tap, name }
+ end.flat_map(&:value)
+ end
+
+ same_name_tap_formulae.delete(full_name)
+
+ unless same_name_tap_formulae.empty?
+ problem "Formula name conflicts with #{same_name_tap_formulae.join ", "}"
+ end
+ end
+
+ def audit_deps
+ @specs.each do |spec|
+ # Check for things we don't like to depend on.
+ # We allow non-Homebrew installs whenever possible.
+ spec.deps.each do |dep|
+ begin
+ dep_f = dep.to_formula
+ rescue TapFormulaUnavailableError
+ # Don't complain about missing cross-tap dependencies
+ next
+ rescue FormulaUnavailableError
+ problem "Can't find dependency #{dep.name.inspect}."
+ next
+ rescue TapFormulaAmbiguityError
+ problem "Ambiguous dependency #{dep.name.inspect}."
+ next
+ rescue TapFormulaWithOldnameAmbiguityError
+ problem "Ambiguous oldname dependency #{dep.name.inspect}."
+ next
+ end
+
+ if dep_f.oldname && dep.name.split("/").last == dep_f.oldname
+ problem "Dependency '#{dep.name}' was renamed; use new name '#{dep_f.name}'."
+ end
+
+ if @@aliases.include?(dep.name)
+ problem "Dependency '#{dep.name}' is an alias; use the canonical name '#{dep.to_formula.full_name}'."
+ end
+
+ dep.options.reject do |opt|
+ next true if dep_f.option_defined?(opt)
+ dep_f.requirements.detect do |r|
+ if r.recommended?
+ opt.name == "with-#{r.name}"
+ elsif r.optional?
+ opt.name == "without-#{r.name}"
+ end
+ end
+ end.each do |opt|
+ problem "Dependency #{dep} does not define option #{opt.name.inspect}"
+ end
+
+ case dep.name
+ when *BUILD_TIME_DEPS
+ next if dep.build? || dep.run?
+ problem <<-EOS.undent
+ #{dep} dependency should be
+ depends_on "#{dep}" => :build
+ Or if it is indeed a runtime dependency
+ depends_on "#{dep}" => :run
+ EOS
+ when "git"
+ problem "Don't use git as a dependency"
+ when "mercurial"
+ problem "Use `depends_on :hg` instead of `depends_on 'mercurial'`"
+ when "gfortran"
+ problem "Use `depends_on :fortran` instead of `depends_on 'gfortran'`"
+ when "ruby"
+ problem <<-EOS.undent
+ Don't use "ruby" as a dependency. If this formula requires a
+ minimum Ruby version not provided by the system you should
+ use the RubyRequirement:
+ depends_on :ruby => "1.8"
+ where "1.8" is the minimum version of Ruby required.
+ EOS
+ when "open-mpi", "mpich"
+ problem <<-EOS.undent
+ There are multiple conflicting ways to install MPI. Use an MPIRequirement:
+ depends_on :mpi => [<lang list>]
+ Where <lang list> is a comma delimited list that can include:
+ :cc, :cxx, :f77, :f90
+ EOS
+ end
+ end
+ end
+ end
+
+ def audit_conflicts
+ formula.conflicts.each do |c|
+ begin
+ Formulary.factory(c.name)
+ rescue TapFormulaUnavailableError
+ # Don't complain about missing cross-tap conflicts.
+ next
+ rescue FormulaUnavailableError
+ problem "Can't find conflicting formula #{c.name.inspect}."
+ rescue TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
+ problem "Ambiguous conflicting formula #{c.name.inspect}."
+ end
+ end
+ end
+
+ def audit_options
+ formula.options.each do |o|
+ next unless @strict
+ if o.name !~ /with(out)?-/ && o.name != "c++11" && o.name != "universal" && o.name != "32-bit"
+ problem "Options should begin with with/without. Migrate '--#{o.name}' with `deprecated_option`."
+ end
+
+ if o.name =~ /^with(out)?-(?:checks?|tests)$/
+ unless formula.deps.any? { |d| d.name == "check" && (d.optional? || d.recommended?) }
+ problem "Use '--with#{$1}-test' instead of '--#{o.name}'. Migrate '--#{o.name}' with `deprecated_option`."
+ end
+ end
+ end
+ end
+
+ def audit_desc
+ # For now, only check the description when using `--strict`
+ return unless @strict
+
+ desc = formula.desc
+
+ unless desc && !desc.empty?
+ problem "Formula should have a desc (Description)."
+ return
+ end
+
+ # Make sure the formula name plus description is no longer than 80 characters
+ # Note full_name includes the name of the tap, while name does not
+ linelength = formula.name.length + ": ".length + desc.length
+ if linelength > 80
+ problem <<-EOS.undent
+ Description is too long. \"name: desc\" should be less than 80 characters.
+ Length is calculated as #{formula.name} + desc. (currently #{linelength})
+ EOS
+ end
+
+ if desc =~ /([Cc]ommand ?line)/
+ problem "Description should use \"command-line\" instead of \"#{$1}\""
+ end
+
+ if desc =~ /^([Aa]n?)\s/
+ problem "Description shouldn't start with an indefinite article (#{$1})"
+ end
+
+ if desc.downcase.start_with? "#{formula.name} "
+ problem "Description shouldn't include the formula name"
+ end
+ end
+
+ def audit_homepage
+ homepage = formula.homepage
+
+ unless homepage =~ %r{^https?://}
+ problem "The homepage should start with http or https (URL is #{homepage})."
+ end
+
+ # Check for http:// GitHub homepage urls, https:// is preferred.
+ # Note: only check homepages that are repo pages, not *.github.com hosts
+ if homepage.start_with? "http://github.com/"
+ problem "Please use https:// for #{homepage}"
+ end
+
+ # Savannah has full SSL/TLS support but no auto-redirect.
+ # Doesn't apply to the download URLs, only the homepage.
+ if homepage.start_with? "http://savannah.nongnu.org/"
+ problem "Please use https:// for #{homepage}"
+ end
+
+ # Freedesktop is complicated to handle - It has SSL/TLS, but only on certain subdomains.
+ # To enable https Freedesktop change the URL from http://project.freedesktop.org/wiki to
+ # https://wiki.freedesktop.org/project_name.
+ # "Software" is redirected to https://wiki.freedesktop.org/www/Software/project_name
+ if homepage =~ %r{^http://((?:www|nice|libopenraw|liboil|telepathy|xorg)\.)?freedesktop\.org/(?:wiki/)?}
+ if homepage =~ /Software/
+ problem "#{homepage} should be styled `https://wiki.freedesktop.org/www/Software/project_name`"
+ else
+ problem "#{homepage} should be styled `https://wiki.freedesktop.org/project_name`"
+ end
+ end
+
+ # Google Code homepages should end in a slash
+ if homepage =~ %r{^https?://code\.google\.com/p/[^/]+[^/]$}
+ problem "#{homepage} should end with a slash"
+ end
+
+ # People will run into mixed content sometimes, but we should enforce and then add
+ # exemptions as they are discovered. Treat mixed content on homepages as a bug.
+ # Justify each exemptions with a code comment so we can keep track here.
+ if homepage =~ %r{^http://[^/]*github\.io/}
+ problem "Please use https:// for #{homepage}"
+ end
+
+ # There's an auto-redirect here, but this mistake is incredibly common too.
+ # Only applies to the homepage and subdomains for now, not the FTP URLs.
+ if homepage =~ %r{^http://((?:build|cloud|developer|download|extensions|git|glade|help|library|live|nagios|news|people|projects|rt|static|wiki|www)\.)?gnome\.org}
+ problem "Please use https:// for #{homepage}"
+ end
+
+ # Compact the above into this list as we're able to remove detailed notations, etc over time.
+ case homepage
+ when %r{^http://[^/]*\.apache\.org},
+ %r{^http://packages\.debian\.org},
+ %r{^http://wiki\.freedesktop\.org/},
+ %r{^http://((?:www)\.)?gnupg\.org/},
+ %r{^http://ietf\.org},
+ %r{^http://[^/.]+\.ietf\.org},
+ %r{^http://[^/.]+\.tools\.ietf\.org},
+ %r{^http://www\.gnu\.org/},
+ %r{^http://code\.google\.com/},
+ %r{^http://bitbucket\.org/},
+ %r{^http://(?:[^/]*\.)?archive\.org}
+ problem "Please use https:// for #{homepage}"
+ end
+
+ return unless @online
+ begin
+ nostdout { curl "--connect-timeout", "15", "-o", "/dev/null", homepage }
+ rescue ErrorDuringExecution
+ problem "The homepage is not reachable (curl exit code #{$?.exitstatus})"
+ end
+ end
+
+ def audit_bottle_spec
+ if formula.bottle_disabled? && !formula.bottle_disable_reason.valid?
+ problem "Unrecognized bottle modifier"
+ end
+ end
+
+ def audit_github_repository
+ return unless @online
+ return unless @new_formula
+
+ regex = %r{https?://github\.com/([^/]+)/([^/]+)/?.*}
+ _, user, repo = *regex.match(formula.stable.url) if formula.stable
+ _, user, repo = *regex.match(formula.homepage) unless user
+ return if !user || !repo
+
+ repo.gsub!(/.git$/, "")
+
+ begin
+ metadata = GitHub.repository(user, repo)
+ rescue GitHub::HTTPNotFoundError
+ return
+ end
+
+ return if metadata.nil?
+
+ problem "GitHub fork (not canonical repository)" if metadata["fork"]
+ if (metadata["forks_count"] < 20) && (metadata["subscribers_count"] < 20) &&
+ (metadata["stargazers_count"] < 50)
+ problem "GitHub repository not notable enough (<20 forks, <20 watchers and <50 stars)"
+ end
+
+ if Date.parse(metadata["created_at"]) > (Date.today - 30)
+ problem "GitHub repository too new (<30 days old)"
+ end
+ end
+
+ def audit_specs
+ if head_only?(formula) && formula.tap.to_s.downcase !~ %r{[-/]head-only$}
+ problem "Head-only (no stable download)"
+ end
+
+ if devel_only?(formula) && formula.tap.to_s.downcase !~ %r{[-/]devel-only$}
+ problem "Devel-only (no stable download)"
+ end
+
+ %w[Stable Devel HEAD].each do |name|
+ next unless spec = formula.send(name.downcase)
+
+ ra = ResourceAuditor.new(spec).audit
+ problems.concat ra.problems.map { |problem| "#{name}: #{problem}" }
+
+ spec.resources.each_value do |resource|
+ ra = ResourceAuditor.new(resource).audit
+ problems.concat ra.problems.map { |problem|
+ "#{name} resource #{resource.name.inspect}: #{problem}"
+ }
+ end
+
+ spec.patches.each { |p| audit_patch(p) if p.external? }
+ end
+
+ %w[Stable Devel].each do |name|
+ next unless spec = formula.send(name.downcase)
+ version = spec.version
+ if version.to_s !~ /\d/
+ problem "#{name}: version (#{version}) is set to a string without a digit"
+ end
+ end
+
+ if formula.stable && formula.devel
+ if formula.devel.version < formula.stable.version
+ problem "devel version #{formula.devel.version} is older than stable version #{formula.stable.version}"
+ elsif formula.devel.version == formula.stable.version
+ problem "stable and devel versions are identical"
+ end
+ end
+
+ stable = formula.stable
+ case stable && stable.url
+ when %r{download\.gnome\.org/sources}, %r{ftp\.gnome\.org/pub/GNOME/sources}i
+ version = Version.parse(stable.url)
+ if version >= Version.create("1.0")
+ minor_version = version.to_s.split(".", 3)[1].to_i
+ if minor_version.odd?
+ problem "#{stable.version} is a development release"
+ end
+ end
+ end
+ end
+
+ def audit_revision_and_version_scheme
+ return unless formula.tap # skip formula not from core or any taps
+ return unless formula.tap.git? # git log is required
+
+ fv = FormulaVersions.new(formula, :max_depth => 10)
+ attributes = [:revision, :version_scheme]
+ attributes_map = fv.version_attributes_map(attributes, "origin/master")
+
+ attributes.each do |attribute|
+ attributes_for_version = attributes_map[attribute][formula.version]
+ if !attributes_for_version.empty?
+ if formula.send(attribute) < attributes_for_version.max
+ problem "#{attribute} should not decrease"
+ end
+ end
+ end
+
+ revision_map = attributes_map[:revision]
+ if formula.revision != 0
+ if formula.stable
+ if revision_map[formula.stable.version].empty? # check stable spec
+ problem "'revision #{formula.revision}' should be removed"
+ end
+ else # head/devel-only formula
+ problem "'revision #{formula.revision}' should be removed"
+ end
+ end
+ end
+
+ def audit_legacy_patches
+ return unless formula.respond_to?(:patches)
+ legacy_patches = Patch.normalize_legacy_patches(formula.patches).grep(LegacyPatch)
+ unless legacy_patches.empty?
+ problem "Use the patch DSL instead of defining a 'patches' method"
+ legacy_patches.each { |p| audit_patch(p) }
+ end
+ end
+
+ def audit_patch(patch)
+ case patch.url
+ when /raw\.github\.com/, %r{gist\.github\.com/raw}, %r{gist\.github\.com/.+/raw},
+ %r{gist\.githubusercontent\.com/.+/raw}
+ unless patch.url =~ /[a-fA-F0-9]{40}/
+ problem "GitHub/Gist patches should specify a revision:\n#{patch.url}"
+ end
+ when %r{https?://patch-diff\.githubusercontent\.com/raw/(.+)/(.+)/pull/(.+)\.(?:diff|patch)}
+ problem <<-EOS.undent
+ use GitHub pull request URLs:
+ https://github.com/#{$1}/#{$2}/pull/#{$3}.patch
+ Rather than patch-diff:
+ #{patch.url}
+ EOS
+ when %r{macports/trunk}
+ problem "MacPorts patches should specify a revision instead of trunk:\n#{patch.url}"
+ when %r{^http://trac\.macports\.org}
+ problem "Patches from MacPorts Trac should be https://, not http:\n#{patch.url}"
+ when %r{^http://bugs\.debian\.org}
+ problem "Patches from Debian should be https://, not http:\n#{patch.url}"
+ end
+ end
+
+ def audit_text
+ if text =~ /system\s+['"]scons/
+ problem "use \"scons *args\" instead of \"system 'scons', *args\""
+ end
+
+ if text =~ /system\s+['"]xcodebuild/
+ problem %(use "xcodebuild *args" instead of "system 'xcodebuild', *args")
+ end
+
+ if text =~ /xcodebuild[ (]["'*]/ && !text.include?("SYMROOT=")
+ problem %(xcodebuild should be passed an explicit "SYMROOT")
+ end
+
+ if text.include? "Formula.factory("
+ problem "\"Formula.factory(name)\" is deprecated in favor of \"Formula[name]\""
+ end
+
+ if text.include?("def plist") && !text.include?("plist_options")
+ problem "Please set plist_options when using a formula-defined plist."
+ end
+
+ if text.include?('require "language/go"') && !text.include?("go_resource")
+ problem "require \"language/go\" is unnecessary unless using `go_resource`s"
+ end
+ end
+
+ def audit_line(line, lineno)
+ if line =~ /<(Formula|AmazonWebServicesFormula|ScriptFileFormula|GithubGistFormula)/
+ problem "Use a space in class inheritance: class Foo < #{$1}"
+ end
+
+ # Commented-out cmake support from default template
+ if line.include?('# system "cmake')
+ problem "Commented cmake call found"
+ end
+
+ # Comments from default template
+ [
+ "# PLEASE REMOVE",
+ "# Documentation:",
+ "# if this fails, try separate make/make install steps",
+ "# The URL of the archive",
+ "## Naming --",
+ "# if your formula requires any X11/XQuartz components",
+ "# if your formula fails when building in parallel",
+ "# Remove unrecognized options if warned by configure",
+ ].each do |comment|
+ if line.include? comment
+ problem "Please remove default template comments"
+ end
+ end
+
+ # FileUtils is included in Formula
+ # encfs modifies a file with this name, so check for some leading characters
+ if line =~ /[^'"\/]FileUtils\.(\w+)/
+ problem "Don't need 'FileUtils.' before #{$1}."
+ end
+
+ # Check for long inreplace block vars
+ if line =~ /inreplace .* do \|(.{2,})\|/
+ problem "\"inreplace <filenames> do |s|\" is preferred over \"|#{$1}|\"."
+ end
+
+ # Check for string interpolation of single values.
+ if line =~ /(system|inreplace|gsub!|change_make_var!).*[ ,]"#\{([\w.]+)\}"/
+ problem "Don't need to interpolate \"#{$2}\" with #{$1}"
+ end
+
+ # Check for string concatenation; prefer interpolation
+ if line =~ /(#\{\w+\s*\+\s*['"][^}]+\})/
+ problem "Try not to concatenate paths in string interpolation:\n #{$1}"
+ end
+
+ # Prefer formula path shortcuts in Pathname+
+ if line =~ %r{\(\s*(prefix\s*\+\s*(['"])(bin|include|libexec|lib|sbin|share|Frameworks)[/'"])}
+ problem "\"(#{$1}...#{$2})\" should be \"(#{$3.downcase}+...)\""
+ end
+
+ if line =~ /((man)\s*\+\s*(['"])(man[1-8])(['"]))/
+ problem "\"#{$1}\" should be \"#{$4}\""
+ end
+
+ # Prefer formula path shortcuts in strings
+ if line =~ %r[(\#\{prefix\}/(bin|include|libexec|lib|sbin|share|Frameworks))]
+ problem "\"#{$1}\" should be \"\#{#{$2.downcase}}\""
+ end
+
+ if line =~ %r[((\#\{prefix\}/share/man/|\#\{man\}/)(man[1-8]))]
+ problem "\"#{$1}\" should be \"\#{#{$3}}\""
+ end
+
+ if line =~ %r[((\#\{share\}/(man)))[/'"]]
+ problem "\"#{$1}\" should be \"\#{#{$3}}\""
+ end
+
+ if line =~ %r[(\#\{prefix\}/share/(info|man))]
+ problem "\"#{$1}\" should be \"\#{#{$2}}\""
+ end
+
+ if line =~ /depends_on :(automake|autoconf|libtool)/
+ problem ":#{$1} is deprecated. Usage should be \"#{$1}\""
+ end
+
+ # Commented-out depends_on
+ if line =~ /#\s*depends_on\s+(.+)\s*$/
+ problem "Commented-out dep #{$1}"
+ end
+
+ # No trailing whitespace, please
+ if line =~ /[\t ]+$/
+ problem "#{lineno}: Trailing whitespace was found"
+ end
+
+ if line =~ /if\s+ARGV\.include\?\s+'--(HEAD|devel)'/
+ problem "Use \"if build.#{$1.downcase}?\" instead"
+ end
+
+ if line.include?("make && make")
+ problem "Use separate make calls"
+ end
+
+ if line =~ /^[ ]*\t/
+ problem "Use spaces instead of tabs for indentation"
+ end
+
+ if line.include?("ENV.x11")
+ problem "Use \"depends_on :x11\" instead of \"ENV.x11\""
+ end
+
+ # Avoid hard-coding compilers
+ if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?(gcc|llvm-gcc|clang)['" ]}
+ problem "Use \"\#{ENV.cc}\" instead of hard-coding \"#{$3}\""
+ end
+
+ if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?((g|llvm-g|clang)\+\+)['" ]}
+ problem "Use \"\#{ENV.cxx}\" instead of hard-coding \"#{$3}\""
+ end
+
+ if line =~ /system\s+['"](env|export)(\s+|['"])/
+ problem "Use ENV instead of invoking '#{$1}' to modify the environment"
+ end
+
+ if line =~ /version == ['"]HEAD['"]/
+ problem "Use 'build.head?' instead of inspecting 'version'"
+ end
+
+ if line =~ /build\.include\?[\s\(]+['"]\-\-(.*)['"]/
+ problem "Reference '#{$1}' without dashes"
+ end
+
+ if line =~ /build\.include\?[\s\(]+['"]with(out)?-(.*)['"]/
+ problem "Use build.with#{$1}? \"#{$2}\" instead of build.include? 'with#{$1}-#{$2}'"
+ end
+
+ if line =~ /build\.with\?[\s\(]+['"]-?-?with-(.*)['"]/
+ problem "Don't duplicate 'with': Use `build.with? \"#{$1}\"` to check for \"--with-#{$1}\""
+ end
+
+ if line =~ /build\.without\?[\s\(]+['"]-?-?without-(.*)['"]/
+ problem "Don't duplicate 'without': Use `build.without? \"#{$1}\"` to check for \"--without-#{$1}\""
+ end
+
+ if line =~ /unless build\.with\?(.*)/
+ problem "Use if build.without?#{$1} instead of unless build.with?#{$1}"
+ end
+
+ if line =~ /unless build\.without\?(.*)/
+ problem "Use if build.with?#{$1} instead of unless build.without?#{$1}"
+ end
+
+ if line =~ /(not\s|!)\s*build\.with?\?/
+ problem "Don't negate 'build.without?': use 'build.with?'"
+ end
+
+ if line =~ /(not\s|!)\s*build\.without?\?/
+ problem "Don't negate 'build.with?': use 'build.without?'"
+ end
+
+ if line =~ /ARGV\.(?!(debug\?|verbose\?|value[\(\s]))/
+ problem "Use build instead of ARGV to check options"
+ end
+
+ if line.include?("def options")
+ problem "Use new-style option definitions"
+ end
+
+ if line.end_with?("def test")
+ problem "Use new-style test definitions (test do)"
+ end
+
+ if line.include?("MACOS_VERSION")
+ problem "Use MacOS.version instead of MACOS_VERSION"
+ end
+
+ if line.include?("MACOS_FULL_VERSION")
+ problem "Use MacOS.full_version instead of MACOS_FULL_VERSION"
+ end
+
+ cats = %w[leopard snow_leopard lion mountain_lion].join("|")
+ if line =~ /MacOS\.(?:#{cats})\?/
+ problem "\"#{$&}\" is deprecated, use a comparison to MacOS.version instead"
+ end
+
+ if line =~ /skip_clean\s+:all/
+ problem "`skip_clean :all` is deprecated; brew no longer strips symbols\n" \
+ "\tPass explicit paths to prevent Homebrew from removing empty folders."
+ end
+
+ if line =~ /depends_on [A-Z][\w:]+\.new$/
+ problem "`depends_on` can take requirement classes instead of instances"
+ end
+
+ if line =~ /^def (\w+).*$/
+ problem "Define method #{$1.inspect} in the class body, not at the top-level"
+ end
+
+ if line.include?("ENV.fortran") && !formula.requirements.map(&:class).include?(FortranRequirement)
+ problem "Use `depends_on :fortran` instead of `ENV.fortran`"
+ end
+
+ if line =~ /JAVA_HOME/i && !formula.requirements.map(&:class).include?(JavaRequirement)
+ problem "Use `depends_on :java` to set JAVA_HOME"
+ end
+
+ if line =~ /depends_on :(.+) (if.+|unless.+)$/
+ audit_conditional_dep($1.to_sym, $2, $&)
+ end
+
+ if line =~ /depends_on ['"](.+)['"] (if.+|unless.+)$/
+ audit_conditional_dep($1, $2, $&)
+ end
+
+ if line =~ /(Dir\[("[^\*{},]+")\])/
+ problem "#{$1} is unnecessary; just use #{$2}"
+ end
+
+ if line =~ /system (["'](#{FILEUTILS_METHODS})["' ])/o
+ system = $1
+ method = $2
+ problem "Use the `#{method}` Ruby method instead of `system #{system}`"
+ end
+
+ if line =~ /assert [^!]+\.include?/
+ problem "Use `assert_match` instead of `assert ...include?`"
+ end
+
+ if line.include?('system "npm", "install"') && !line.include?("Language::Node") && formula.name !~ /^kibana(\d{2})?$/
+ problem "Use Language::Node for npm install args"
+ end
+
+ if @strict
+ if line =~ /system ((["'])[^"' ]*(?:\s[^"' ]*)+\2)/
+ bad_system = $1
+ unless %w[| < > & ; *].any? { |c| bad_system.include? c }
+ good_system = bad_system.gsub(" ", "\", \"")
+ problem "Use `system #{good_system}` instead of `system #{bad_system}` "
+ end
+ end
+
+ if line =~ /(require ["']formula["'])/
+ problem "`#{$1}` is now unnecessary"
+ end
+
+ if line =~ %r{#\{share\}/#{Regexp.escape(formula.name)}[/'"]}
+ problem "Use \#{pkgshare} instead of \#{share}/#{formula.name}"
+ end
+
+ if line =~ %r{share(\s*[/+]\s*)(['"])#{Regexp.escape(formula.name)}(?:\2|/)}
+ problem "Use pkgshare instead of (share#{$1}\"#{formula.name}\")"
+ end
+ end
+ end
+
+ def audit_caveats
+ caveats = formula.caveats.to_s
+
+ if caveats.include?("setuid")
+ problem "Don't recommend setuid in the caveats, suggest sudo instead."
+ end
+ end
+
+ def audit_reverse_migration
+ # Only enforce for new formula being re-added to core and official taps
+ return unless @strict
+ return unless formula.tap && formula.tap.official?
+
+ if formula.tap.tap_migrations.key?(formula.name)
+ problem <<-EOS.undent
+ #{formula.name} seems to be listed in tap_migrations.json!
+ Please remove #{formula.name} from present tap & tap_migrations.json
+ before submitting it to Homebrew/homebrew-#{formula.tap.repo}.
+ EOS
+ end
+ end
+
+ def audit_prefix_has_contents
+ return unless formula.prefix.directory?
+
+ if Keg.new(formula.prefix).empty_installation?
+ problem <<-EOS.undent
+ The installation seems to be empty. Please ensure the prefix
+ is set correctly and expected files are installed.
+ The prefix configure/make argument may be case-sensitive.
+ EOS
+ end
+ end
+
+ def audit_conditional_dep(dep, condition, line)
+ quoted_dep = quote_dep(dep)
+ dep = Regexp.escape(dep.to_s)
+
+ case condition
+ when /if build\.include\? ['"]with-#{dep}['"]$/, /if build\.with\? ['"]#{dep}['"]$/
+ problem %(Replace #{line.inspect} with "depends_on #{quoted_dep} => :optional")
+ when /unless build\.include\? ['"]without-#{dep}['"]$/, /unless build\.without\? ['"]#{dep}['"]$/
+ problem %(Replace #{line.inspect} with "depends_on #{quoted_dep} => :recommended")
+ end
+ end
+
+ def quote_dep(dep)
+ Symbol === dep ? dep.inspect : "'#{dep}'"
+ end
+
+ def audit_check_output(output)
+ problem(output) if output
+ end
+
+ def audit
+ audit_file
+ audit_formula_name
+ audit_class
+ audit_specs
+ audit_revision_and_version_scheme
+ audit_desc
+ audit_homepage
+ audit_bottle_spec
+ audit_github_repository
+ audit_deps
+ audit_conflicts
+ audit_options
+ audit_legacy_patches
+ audit_text
+ audit_caveats
+ text.without_patch.split("\n").each_with_index { |line, lineno| audit_line(line, lineno+1) }
+ audit_installed
+ audit_prefix_has_contents
+ audit_reverse_migration
+ audit_style
+ end
+
+ private
+
+ def problem(p)
+ @problems << p
+ end
+
+ def head_only?(formula)
+ formula.head && formula.devel.nil? && formula.stable.nil?
+ end
+
+ def devel_only?(formula)
+ formula.devel && formula.stable.nil?
+ end
+end
+
+class ResourceAuditor
+ attr_reader :problems
+ attr_reader :version, :checksum, :using, :specs, :url, :mirrors, :name
+
+ def initialize(resource)
+ @name = resource.name
+ @version = resource.version
+ @checksum = resource.checksum
+ @url = resource.url
+ @mirrors = resource.mirrors
+ @using = resource.using
+ @specs = resource.specs
+ @problems = []
+ end
+
+ def audit
+ audit_version
+ audit_checksum
+ audit_download_strategy
+ audit_urls
+ self
+ end
+
+ def audit_version
+ if version.nil?
+ problem "missing version"
+ elsif version.to_s.empty?
+ problem "version is set to an empty string"
+ elsif !version.detected_from_url?
+ version_text = version
+ version_url = Version.detect(url, specs)
+ if version_url.to_s == version_text.to_s && version.instance_of?(Version)
+ problem "version #{version_text} is redundant with version scanned from URL"
+ end
+ end
+
+ if version.to_s.start_with?("v")
+ problem "version #{version} should not have a leading 'v'"
+ end
+
+ if version.to_s =~ /_\d+$/
+ problem "version #{version} should not end with an underline and a number"
+ end
+ end
+
+ def audit_checksum
+ return unless checksum
+
+ case checksum.hash_type
+ when :md5
+ problem "MD5 checksums are deprecated, please use SHA256"
+ return
+ when :sha1
+ problem "SHA1 checksums are deprecated, please use SHA256"
+ return
+ when :sha256 then len = 64
+ end
+
+ if checksum.empty?
+ problem "#{checksum.hash_type} is empty"
+ else
+ problem "#{checksum.hash_type} should be #{len} characters" unless checksum.hexdigest.length == len
+ problem "#{checksum.hash_type} contains invalid characters" unless checksum.hexdigest =~ /^[a-fA-F0-9]+$/
+ problem "#{checksum.hash_type} should be lowercase" unless checksum.hexdigest == checksum.hexdigest.downcase
+ end
+ end
+
+ def audit_download_strategy
+ if url =~ %r{^(cvs|bzr|hg|fossil)://} || url =~ %r{^(svn)\+http://}
+ problem "Use of the #{$&} scheme is deprecated, pass `:using => :#{$1}` instead"
+ end
+
+ url_strategy = DownloadStrategyDetector.detect(url)
+
+ if using == :git || url_strategy == GitDownloadStrategy
+ if specs[:tag] && !specs[:revision]
+ problem "Git should specify :revision when a :tag is specified."
+ end
+ end
+
+ return unless using
+
+ if using == :ssl3 || \
+ (Object.const_defined?("CurlSSL3DownloadStrategy") && using == CurlSSL3DownloadStrategy)
+ problem "The SSL3 download strategy is deprecated, please choose a different URL"
+ elsif (Object.const_defined?("CurlUnsafeDownloadStrategy") && using == CurlUnsafeDownloadStrategy) || \
+ (Object.const_defined?("UnsafeSubversionDownloadStrategy") && using == UnsafeSubversionDownloadStrategy)
+ problem "#{using.name} is deprecated, please choose a different URL"
+ end
+
+ if using == :cvs
+ mod = specs[:module]
+
+ if mod == name
+ problem "Redundant :module value in URL"
+ end
+
+ if url =~ %r{:[^/]+$}
+ mod = url.split(":").last
+
+ if mod == name
+ problem "Redundant CVS module appended to URL"
+ else
+ problem "Specify CVS module as `:module => \"#{mod}\"` instead of appending it to the URL"
+ end
+ end
+ end
+
+ using_strategy = DownloadStrategyDetector.detect("", using)
+
+ if url_strategy == using_strategy
+ problem "Redundant :using value in URL"
+ end
+ end
+
+ def audit_urls
+ # Check GNU urls; doesn't apply to mirrors
+ if url =~ %r{^(?:https?|ftp)://(?!alpha).+/gnu/}
+ problem "Please use \"https://ftpmirror.gnu.org\" instead of #{url}."
+ end
+
+ if mirrors.include?(url)
+ problem "URL should not be duplicated as a mirror: #{url}"
+ end
+
+ urls = [url] + mirrors
+
+ # Check a variety of SSL/TLS URLs that don't consistently auto-redirect
+ # or are overly common errors that need to be reduced & fixed over time.
+ urls.each do |p|
+ case p
+ when %r{^http://ftp\.gnu\.org/},
+ %r{^http://ftpmirror\.gnu\.org/},
+ %r{^http://download\.savannah\.gnu\.org/},
+ %r{^http://download-mirror\.savannah\.gnu\.org/},
+ %r{^http://[^/]*\.apache\.org/},
+ %r{^http://code\.google\.com/},
+ %r{^http://fossies\.org/},
+ %r{^http://mirrors\.kernel\.org/},
+ %r{^http://(?:[^/]*\.)?bintray\.com/},
+ %r{^http://tools\.ietf\.org/},
+ %r{^http://launchpad\.net/},
+ %r{^http://bitbucket\.org/},
+ %r{^http://anonscm\.debian\.org/},
+ %r{^http://cpan\.metacpan\.org/},
+ %r{^http://hackage\.haskell\.org/},
+ %r{^http://(?:[^/]*\.)?archive\.org},
+ %r{^http://(?:[^/]*\.)?freedesktop\.org},
+ %r{^http://(?:[^/]*\.)?mirrorservice\.org/}
+ problem "Please use https:// for #{p}"
+ when %r{^http://search\.mcpan\.org/CPAN/(.*)}i
+ problem "#{p} should be `https://cpan.metacpan.org/#{$1}`"
+ when %r{^(http|ftp)://ftp\.gnome\.org/pub/gnome/(.*)}i
+ problem "#{p} should be `https://download.gnome.org/#{$2}`"
+ when %r{^git://anonscm\.debian\.org/users/(.*)}i
+ problem "#{p} should be `https://anonscm.debian.org/git/users/#{$1}`"
+ end
+ end
+
+ # Prefer HTTP/S when possible over FTP protocol due to possible firewalls.
+ urls.each do |p|
+ case p
+ when %r{^ftp://ftp\.mirrorservice\.org}
+ problem "Please use https:// for #{p}"
+ when %r{^ftp://ftp\.cpan\.org/pub/CPAN(.*)}i
+ problem "#{p} should be `http://search.cpan.org/CPAN#{$1}`"
+ end
+ end
+
+ # Check SourceForge urls
+ urls.each do |p|
+ # Skip if the URL looks like a SVN repo
+ next if p.include? "/svnroot/"
+ next if p.include? "svn.sourceforge"
+
+ # Is it a sourceforge http(s) URL?
+ next unless p =~ %r{^https?://.*\b(sourceforge|sf)\.(com|net)}
+
+ if p =~ /(\?|&)use_mirror=/
+ problem "Don't use #{$1}use_mirror in SourceForge urls (url is #{p})."
+ end
+
+ if p.end_with?("/download")
+ problem "Don't use /download in SourceForge urls (url is #{p})."
+ end
+
+ if p =~ %r{^https?://sourceforge\.}
+ problem "Use https://downloads.sourceforge.net to get geolocation (url is #{p})."
+ end
+
+ if p =~ %r{^https?://prdownloads\.}
+ problem "Don't use prdownloads in SourceForge urls (url is #{p}).\n" \
+ "\tSee: http://librelist.com/browser/homebrew/2011/1/12/prdownloads-is-bad/"
+ end
+
+ if p =~ %r{^http://\w+\.dl\.}
+ problem "Don't use specific dl mirrors in SourceForge urls (url is #{p})."
+ end
+
+ if p.start_with? "http://downloads"
+ problem "Please use https:// for #{p}"
+ end
+ end
+
+ # Debian has an abundance of secure mirrors. Let's not pluck the insecure
+ # one out of the grab bag.
+ urls.each do |u|
+ next unless u =~ %r{^http://http\.debian\.net/debian/(.*)}i
+ problem <<-EOS.undent
+ Please use a secure mirror for Debian URLs.
+ We recommend:
+ https://mirrors.ocf.berkeley.edu/debian/#{$1}
+ EOS
+ end
+
+ # Check for Google Code download urls, https:// is preferred
+ # Intentionally not extending this to SVN repositories due to certificate
+ # issues.
+ urls.grep(%r{^http://.*\.googlecode\.com/files.*}) do |u|
+ problem "Please use https:// for #{u}"
+ end
+
+ # Check for new-url Google Code download urls, https:// is preferred
+ urls.grep(%r{^http://code\.google\.com/}) do |u|
+ problem "Please use https:// for #{u}"
+ end
+
+ # Check for git:// GitHub repo urls, https:// is preferred.
+ urls.grep(%r{^git://[^/]*github\.com/}) do |u|
+ problem "Please use https:// for #{u}"
+ end
+
+ # Check for git:// Gitorious repo urls, https:// is preferred.
+ urls.grep(%r{^git://[^/]*gitorious\.org/}) do |u|
+ problem "Please use https:// for #{u}"
+ end
+
+ # Check for http:// GitHub repo urls, https:// is preferred.
+ urls.grep(%r{^http://github\.com/.*\.git$}) do |u|
+ problem "Please use https:// for #{u}"
+ end
+
+ # Use new-style archive downloads
+ urls.each do |u|
+ next unless u =~ %r{https://.*github.*/(?:tar|zip)ball/} && u !~ /\.git$/
+ problem "Use /archive/ URLs for GitHub tarballs (url is #{u})."
+ end
+
+ # Don't use GitHub .zip files
+ urls.each do |u|
+ next unless u =~ %r{https://.*github.*/(archive|releases)/.*\.zip$} && u !~ %r{releases/download}
+ problem "Use GitHub tarballs rather than zipballs (url is #{u})."
+ end
+
+ # Don't use GitHub codeload URLs
+ urls.each do |u|
+ next unless u =~ %r{https?://codeload\.github\.com/(.+)/(.+)/(?:tar\.gz|zip)/(.+)}
+ problem <<-EOS.undent
+ use GitHub archive URLs:
+ https://github.com/#{$1}/#{$2}/archive/#{$3}.tar.gz
+ Rather than codeload:
+ #{u}
+ EOS
+ end
+
+ # Check for Maven Central urls, prefer HTTPS redirector over specific host
+ urls.each do |u|
+ next unless u =~ %r{https?://(?:central|repo\d+)\.maven\.org/maven2/(.+)$}
+ problem "#{u} should be `https://search.maven.org/remotecontent?filepath=#{$1}`"
+ end
+ end
+
+ def problem(text)
+ @problems << text
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/bottle.rb b/Library/Homebrew/dev-cmd/bottle.rb
new file mode 100644
index 000000000..1980fbe9d
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/bottle.rb
@@ -0,0 +1,458 @@
+#: @hide_from_man_page
+#: * `bottle` [`--verbose`] [`--no-rebuild`] [`--keep-old`] [`--skip-relocation`] [`--root-url=<root_url>`]:
+#: * `bottle` `--merge` [`--no-commit`] [`--keep-old`] [`--write`]:
+#:
+#: Generate a bottle (binary package) from a formula installed with
+#: `--build-bottle`.
+
+require "formula"
+require "utils/bottles"
+require "tab"
+require "keg"
+require "formula_versions"
+require "utils/inreplace"
+require "erb"
+require "extend/pathname"
+
+BOTTLE_ERB = <<-EOS
+ bottle do
+ <% if !root_url.start_with?(BottleSpecification::DEFAULT_DOMAIN) %>
+ root_url "<%= root_url %>"
+ <% end %>
+ <% if prefix != BottleSpecification::DEFAULT_PREFIX %>
+ prefix "<%= prefix %>"
+ <% end %>
+ <% if cellar.is_a? Symbol %>
+ cellar :<%= cellar %>
+ <% elsif cellar != BottleSpecification::DEFAULT_CELLAR %>
+ cellar "<%= cellar %>"
+ <% end %>
+ <% if rebuild > 0 %>
+ rebuild <%= rebuild %>
+ <% end %>
+ <% checksums.each do |checksum_type, checksum_values| %>
+ <% checksum_values.each do |checksum_value| %>
+ <% checksum, osx = checksum_value.shift %>
+ <%= checksum_type %> "<%= checksum %>" => :<%= osx %>
+ <% end %>
+ <% end %>
+ end
+EOS
+
+MAXIMUM_STRING_MATCHES = 100
+
+module Homebrew
+ def keg_contain?(string, keg, ignores)
+ @put_string_exists_header, @put_filenames = nil
+
+ def print_filename(string, filename)
+ unless @put_string_exists_header
+ opoo "String '#{string}' still exists in these files:"
+ @put_string_exists_header = true
+ end
+
+ @put_filenames ||= []
+ unless @put_filenames.include? filename
+ puts "#{Tty.red}#{filename}#{Tty.reset}"
+ @put_filenames << filename
+ end
+ end
+
+ result = false
+
+ keg.each_unique_file_matching(string) do |file|
+ # skip document file.
+ next if Metafiles::EXTENSIONS.include? file.extname
+
+ linked_libraries = Keg.file_linked_libraries(file, string)
+ result ||= !linked_libraries.empty?
+
+ if ARGV.verbose?
+ print_filename(string, file) unless linked_libraries.empty?
+ linked_libraries.each do |lib|
+ puts " #{Tty.gray}-->#{Tty.reset} links to #{lib}"
+ end
+ end
+
+ text_matches = []
+
+ # Use strings to search through the file for each string
+ Utils.popen_read("strings", "-t", "x", "-", file.to_s) do |io|
+ until io.eof?
+ str = io.readline.chomp
+ next if ignores.any? { |i| i =~ str }
+ next unless str.include? string
+ offset, match = str.split(" ", 2)
+ next if linked_libraries.include? match # Don't bother reporting a string if it was found by otool
+
+ result = true
+ text_matches << [match, offset]
+ end
+ end
+
+ if ARGV.verbose? && !text_matches.empty?
+ print_filename string, file
+ text_matches.first(MAXIMUM_STRING_MATCHES).each do |match, offset|
+ puts " #{Tty.gray}-->#{Tty.reset} match '#{match}' at offset #{Tty.em}0x#{offset}#{Tty.reset}"
+ end
+
+ if text_matches.size > MAXIMUM_STRING_MATCHES
+ puts "Only the first #{MAXIMUM_STRING_MATCHES} matches were output"
+ end
+ end
+ end
+
+ keg_contain_absolute_symlink_starting_with?(string, keg) || result
+ end
+
+ def keg_contain_absolute_symlink_starting_with?(string, keg)
+ absolute_symlinks_start_with_string = []
+ keg.find do |pn|
+ if pn.symlink? && (link = pn.readlink).absolute?
+ if link.to_s.start_with?(string)
+ absolute_symlinks_start_with_string << pn
+ end
+ end
+ end
+
+ if ARGV.verbose?
+ unless absolute_symlinks_start_with_string.empty?
+ opoo "Absolute symlink starting with #{string}:"
+ absolute_symlinks_start_with_string.each do |pn|
+ puts " #{pn} -> #{pn.resolved_path}"
+ end
+ end
+ end
+
+ !absolute_symlinks_start_with_string.empty?
+ end
+
+ def bottle_output(bottle)
+ erb = ERB.new BOTTLE_ERB
+ erb.result(bottle.instance_eval { binding }).gsub(/^\s*$\n/, "")
+ end
+
+ def bottle_formula(f)
+ unless f.installed?
+ return ofail "Formula not installed or up-to-date: #{f.full_name}"
+ end
+
+ unless f.tap
+ return ofail "Formula not from core or any taps: #{f.full_name}"
+ end
+
+ if f.bottle_disabled?
+ ofail "Formula has disabled bottle: #{f.full_name}"
+ puts f.bottle_disable_reason
+ return
+ end
+
+ unless Utils::Bottles::built_as? f
+ return ofail "Formula not installed with '--build-bottle': #{f.full_name}"
+ end
+
+ unless f.stable
+ return ofail "Formula has no stable version: #{f.full_name}"
+ end
+
+ if ARGV.include? "--no-rebuild"
+ rebuild = 0
+ elsif ARGV.include? "--keep-old"
+ rebuild = f.bottle_specification.rebuild
+ else
+ ohai "Determining #{f.full_name} bottle rebuild..."
+ versions = FormulaVersions.new(f)
+ rebuilds = versions.bottle_version_map("origin/master")[f.pkg_version]
+ rebuilds.pop if rebuilds.last.to_i > 0
+ rebuild = rebuilds.empty? ? 0 : rebuilds.max.to_i + 1
+ end
+
+ filename = Bottle::Filename.create(f, Utils::Bottles.tag, rebuild)
+ bottle_path = Pathname.pwd/filename
+
+ tar_filename = filename.to_s.sub(/.gz$/, "")
+ tar_path = Pathname.pwd/tar_filename
+
+ prefix = HOMEBREW_PREFIX.to_s
+ cellar = HOMEBREW_CELLAR.to_s
+
+ ohai "Bottling #{filename}..."
+
+ keg = Keg.new(f.prefix)
+ relocatable = false
+ skip_relocation = false
+
+ keg.lock do
+ original_tab = nil
+
+ begin
+ unless ARGV.include? "--skip-relocation"
+ keg.relocate_dynamic_linkage prefix, Keg::PREFIX_PLACEHOLDER,
+ cellar, Keg::CELLAR_PLACEHOLDER
+ keg.relocate_text_files prefix, Keg::PREFIX_PLACEHOLDER,
+ cellar, Keg::CELLAR_PLACEHOLDER
+ end
+
+ keg.delete_pyc_files!
+
+ Tab.clear_cache
+ tab = Tab.for_keg(keg)
+ original_tab = tab.dup
+ tab.poured_from_bottle = false
+ tab.HEAD = nil
+ tab.time = nil
+ tab.write
+
+ keg.find do |file|
+ if file.symlink?
+ # Ruby does not support `File.lutime` yet.
+ # Shellout using `touch` to change modified time of symlink itself.
+ system "/usr/bin/touch", "-h",
+ "-t", tab.source_modified_time.strftime("%Y%m%d%H%M.%S"), file
+ else
+ file.utime(tab.source_modified_time, tab.source_modified_time)
+ end
+ end
+
+ cd cellar do
+ safe_system "tar", "cf", tar_path, "#{f.name}/#{f.pkg_version}"
+ tar_path.utime(tab.source_modified_time, tab.source_modified_time)
+ relocatable_tar_path = "#{f}-bottle.tar"
+ mv tar_path, relocatable_tar_path
+ # Use gzip, faster to compress than bzip2, faster to uncompress than bzip2
+ # or an uncompressed tarball (and more bandwidth friendly).
+ safe_system "gzip", "-f", relocatable_tar_path
+ mv "#{relocatable_tar_path}.gz", bottle_path
+ end
+
+ if bottle_path.size > 1*1024*1024
+ ohai "Detecting if #{filename} is relocatable..."
+ end
+
+ if prefix == "/usr/local"
+ prefix_check = File.join(prefix, "opt")
+ else
+ prefix_check = prefix
+ end
+
+ ignores = []
+ if f.deps.any? { |dep| dep.name == "go" }
+ ignores << %r{#{Regexp.escape(HOMEBREW_CELLAR)}/go/[\d\.]+/libexec}
+ end
+
+ relocatable = true
+ if ARGV.include? "--skip-relocation"
+ skip_relocation = true
+ else
+ relocatable = false if keg_contain?(prefix_check, keg, ignores)
+ relocatable = false if keg_contain?(cellar, keg, ignores)
+ if prefix != prefix_check
+ relocatable = false if keg_contain_absolute_symlink_starting_with?(prefix, keg)
+ end
+ skip_relocation = relocatable && !keg.require_install_name_tool?
+ end
+ puts if !relocatable && ARGV.verbose?
+ rescue Interrupt
+ ignore_interrupts { bottle_path.unlink if bottle_path.exist? }
+ raise
+ ensure
+ ignore_interrupts do
+ original_tab.write if original_tab
+ unless ARGV.include? "--skip-relocation"
+ keg.relocate_dynamic_linkage Keg::PREFIX_PLACEHOLDER, prefix,
+ Keg::CELLAR_PLACEHOLDER, cellar
+ keg.relocate_text_files Keg::PREFIX_PLACEHOLDER, prefix,
+ Keg::CELLAR_PLACEHOLDER, cellar
+ end
+ end
+ end
+ end
+
+ root_url = ARGV.value("root-url")
+ # Use underscored version for legacy reasons. Remove at some point.
+ root_url ||= ARGV.value("root_url")
+
+ bottle = BottleSpecification.new
+ bottle.root_url(root_url) if root_url
+ if relocatable
+ if skip_relocation
+ bottle.cellar :any_skip_relocation
+ else
+ bottle.cellar :any
+ end
+ else
+ bottle.cellar cellar
+ bottle.prefix prefix
+ end
+ bottle.rebuild rebuild
+ sha256 = bottle_path.sha256
+ bottle.sha256 sha256 => Utils::Bottles.tag
+
+ old_spec = f.bottle_specification
+ if ARGV.include?("--keep-old") && !old_spec.checksums.empty?
+ bad_fields = [:root_url, :prefix, :cellar, :rebuild].select do |field|
+ old_spec.send(field) != bottle.send(field)
+ end
+ bad_fields.delete(:cellar) if old_spec.cellar == :any && bottle.cellar == :any_skip_relocation
+ unless bad_fields.empty?
+ bottle_path.unlink if bottle_path.exist?
+ odie "--keep-old is passed but there are changes in: #{bad_fields.join ", "}"
+ end
+ end
+
+ output = bottle_output bottle
+
+ puts "./#{filename}"
+ puts output
+
+ if ARGV.include? "--json"
+ json = {
+ f.full_name => {
+ "formula" => {
+ "pkg_version" => f.pkg_version.to_s,
+ "path" => f.path.to_s.strip_prefix("#{HOMEBREW_REPOSITORY}/"),
+ },
+ "bottle" => {
+ "root_url" => bottle.root_url,
+ "prefix" => bottle.prefix,
+ "cellar" => bottle.cellar.to_s,
+ "rebuild" => bottle.rebuild,
+ "tags" => {
+ Utils::Bottles.tag.to_s => {
+ "filename" => filename.to_s,
+ "sha256" => sha256,
+ },
+ }
+ },
+ "bintray" => {
+ "package" => Utils::Bottles::Bintray.package(f.name),
+ "repository" => Utils::Bottles::Bintray.repository(f.tap),
+ },
+ },
+ }
+ File.open("#{filename.prefix}.bottle.json", "w") do |file|
+ file.write Utils::JSON.dump json
+ end
+ end
+ end
+
+ def merge
+ write = ARGV.include? "--write"
+
+ bottles_hash = ARGV.named.reduce({}) do |hash, json_file|
+ deep_merge_hashes hash, Utils::JSON.load(IO.read(json_file))
+ end
+
+ bottles_hash.each do |formula_name, bottle_hash|
+ ohai formula_name
+
+ bottle = BottleSpecification.new
+ bottle.root_url bottle_hash["bottle"]["root_url"]
+ cellar = bottle_hash["bottle"]["cellar"]
+ if cellar == "any" || cellar == "any_skip_relocation"
+ cellar = cellar.to_sym
+ end
+ bottle.cellar cellar
+ bottle.prefix bottle_hash["bottle"]["prefix"]
+ bottle.rebuild bottle_hash["bottle"]["rebuild"]
+ bottle_hash["bottle"]["tags"].each do |tag, tag_hash|
+ bottle.sha256 tag_hash["sha256"] => tag.to_sym
+ end
+
+ output = bottle_output bottle
+
+ if write
+ path = Pathname.new("#{HOMEBREW_REPOSITORY/bottle_hash["formula"]["path"]}")
+ update_or_add = nil
+
+ Utils::Inreplace.inreplace(path) do |s|
+ if s.include? "bottle do"
+ update_or_add = "update"
+ if ARGV.include? "--keep-old"
+ mismatches = []
+ bottle_block_contents = s[/ bottle do(.+?)end\n/m, 1]
+ bottle_block_contents.lines.each do |line|
+ line = line.strip
+ next if line.empty?
+ key, value, _, tag = line.split " ", 4
+ valid_key = %w[root_url prefix cellar rebuild sha1 sha256].include? key
+ next unless valid_key
+
+ value = value.to_s.delete ":'\""
+ tag = tag.to_s.delete ":"
+
+ if !tag.empty?
+ if !bottle_hash["bottle"]["tags"][tag].to_s.empty?
+ mismatches << "#{key} => #{tag}"
+ else
+ bottle.send(key, value => tag.to_sym)
+ end
+ next
+ end
+
+ old_value = bottle_hash["bottle"][key].to_s
+ next if key == "cellar" && old_value == "any" && value == "any_skip_relocation"
+ mismatches << key if old_value.empty? || value != old_value
+ end
+ unless mismatches.empty?
+ odie "--keep-old was passed but there were changes in #{mismatches.join(", ")}!"
+ end
+ output = bottle_output bottle
+ end
+ puts output
+ string = s.sub!(/ bottle do.+?end\n/m, output)
+ odie "Bottle block update failed!" unless string
+ else
+ if ARGV.include? "--keep-old"
+ odie "--keep-old was passed but there was no existing bottle block!"
+ end
+ puts output
+ update_or_add = "add"
+ if s.include? "stable do"
+ indent = s.slice(/^ +stable do/).length - "stable do".length
+ string = s.sub!(/^ {#{indent}}stable do(.|\n)+?^ {#{indent}}end\n/m, '\0' + output + "\n")
+ else
+ string = s.sub!(
+ /(
+ \ {2}( # two spaces at the beginning
+ (url|head)\ ['"][\S\ ]+['"] # url or head with a string
+ (
+ ,[\S\ ]*$ # url may have options
+ (\n^\ {3}[\S\ ]+$)* # options can be in multiple lines
+ )?|
+ (homepage|desc|sha1|sha256|version|mirror)\ ['"][\S\ ]+['"]| # specs with a string
+ rebuild\ \d+ # rebuild with a number
+ )\n+ # multiple empty lines
+ )+
+ /mx, '\0' + output + "\n")
+ end
+ odie "Bottle block addition failed!" unless string
+ end
+ end
+
+ unless ARGV.include? "--no-commit"
+ short_name = formula_name.split("/", -1).last
+ pkg_version = bottle_hash["formula"]["pkg_version"]
+
+ path.parent.cd do
+ safe_system "git", "commit", "--no-edit", "--verbose",
+ "--message=#{short_name}: #{update_or_add} #{pkg_version} bottle.",
+ "--", path
+ end
+ end
+ else
+ puts output
+ end
+ end
+ end
+
+ def bottle
+ if ARGV.include? "--merge"
+ merge
+ else
+ ARGV.resolved_formulae.each do |f|
+ bottle_formula f
+ end
+ end
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/create.rb b/Library/Homebrew/dev-cmd/create.rb
new file mode 100644
index 000000000..9be990318
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/create.rb
@@ -0,0 +1,218 @@
+#: * `create` <URL> [`--autotools`|`--cmake`] [`--no-fetch`] [`--set-name` <name>] [`--set-version` <version>] [`--tap` <user>`/`<repo>]:
+#: Generate a formula for the downloadable file at <URL> and open it in the editor.
+#: Homebrew will attempt to automatically derive the formula name
+#: and version, but if it fails, you'll have to make your own template. The `wget`
+#: formula serves as a simple example. For the complete API have a look at
+#:
+#: <http://www.rubydoc.info/github/Homebrew/brew/master/Formula>
+#:
+#: If `--autotools` is passed, create a basic template for an Autotools-style build.
+#: If `--cmake` is passed, create a basic template for a CMake-style build.
+#:
+#: If `--no-fetch` is passed, Homebrew will not download <URL> to the cache and
+#: will thus not add the SHA256 to the formula for you.
+#:
+#: The options `--set-name` and `--set-version` each take an argument and allow
+#: you to explicitly set the name and version of the package you are creating.
+#:
+#: The option `--tap` takes a tap as its argument and generates the formula in
+#: the specified tap.
+
+require "formula"
+require "blacklist"
+require "digest"
+require "erb"
+
+module Homebrew
+ # Create a formula from a tarball URL
+ def create
+ # Allow searching MacPorts or Fink.
+ if ARGV.include? "--macports"
+ opoo "`brew create --macports` is deprecated; use `brew search --macports` instead"
+ exec_browser "https://www.macports.org/ports.php?by=name&substr=#{ARGV.next}"
+ elsif ARGV.include? "--fink"
+ opoo "`brew create --fink` is deprecated; use `brew search --fink` instead"
+ exec_browser "http://pdb.finkproject.org/pdb/browse.php?summary=#{ARGV.next}"
+ end
+
+ raise UsageError if ARGV.named.empty?
+
+ # Ensure that the cache exists so we can fetch the tarball
+ HOMEBREW_CACHE.mkpath
+
+ url = ARGV.named.first # Pull the first (and only) url from ARGV
+
+ version = ARGV.next if ARGV.include? "--set-version"
+ name = ARGV.next if ARGV.include? "--set-name"
+ tap = ARGV.next if ARGV.include? "--tap"
+
+ fc = FormulaCreator.new
+ fc.name = name
+ fc.version = version
+ fc.tap = Tap.fetch(tap || "homebrew/core")
+ raise TapUnavailableError, tap unless fc.tap.installed?
+ fc.url = url
+
+ fc.mode = if ARGV.include? "--cmake"
+ :cmake
+ elsif ARGV.include? "--autotools"
+ :autotools
+ end
+
+ if fc.name.nil? || fc.name.strip.empty?
+ stem = Pathname.new(url).stem
+ print "Formula name [#{stem}]: "
+ fc.name = __gets || stem
+ fc.update_path
+ end
+
+ # Don't allow blacklisted formula, or names that shadow aliases,
+ # unless --force is specified.
+ unless ARGV.force?
+ if msg = blacklisted?(fc.name)
+ raise "#{fc.name} is blacklisted for creation.\n#{msg}\nIf you really want to create this formula use --force."
+ end
+
+ if Formula.aliases.include? fc.name
+ realname = Formulary.canonical_name(fc.name)
+ raise <<-EOS.undent
+ The formula #{realname} is already aliased to #{fc.name}
+ Please check that you are not creating a duplicate.
+ To force creation use --force.
+ EOS
+ end
+ end
+
+ fc.generate!
+
+ puts "Please `brew audit --new-formula #{fc.name}` before submitting, thanks."
+ exec_editor fc.path
+ end
+
+ def __gets
+ gots = $stdin.gets.chomp
+ if gots.empty? then nil else gots end
+ end
+end
+
+class FormulaCreator
+ attr_reader :url, :sha256
+ attr_accessor :name, :version, :tap, :path, :mode
+
+ def url=(url)
+ @url = url
+ path = Pathname.new(url)
+ if @name.nil?
+ case url
+ when %r{github\.com/\S+/(\S+)\.git}
+ @name = $1
+ @head = true
+ when %r{github\.com/\S+/(\S+)/archive/}
+ @name = $1
+ else
+ @name = path.basename.to_s[/(.*?)[-_.]?#{Regexp.escape(path.version.to_s)}/, 1]
+ end
+ end
+ update_path
+ if @version
+ @version = Version.create(@version)
+ else
+ @version = Pathname.new(url).version
+ end
+ end
+
+ def update_path
+ return if @name.nil? || @tap.nil?
+ @path = Formulary.path "#{@tap}/#{@name}"
+ end
+
+ def fetch?
+ !head? && !ARGV.include?("--no-fetch")
+ end
+
+ def head?
+ @head || ARGV.build_head?
+ end
+
+ def generate!
+ raise "#{path} already exists" if path.exist?
+
+ if version.nil?
+ opoo "Version cannot be determined from URL."
+ puts "You'll need to add an explicit 'version' to the formula."
+ end
+
+ if fetch? && version
+ r = Resource.new
+ r.url(url)
+ r.version(version)
+ r.owner = self
+ @sha256 = r.fetch.sha256 if r.download_strategy == CurlDownloadStrategy
+ end
+
+ path.write ERB.new(template, nil, ">").result(binding)
+ end
+
+ def template; <<-EOS.undent
+ # Documentation: https://github.com/Homebrew/brew/blob/master/share/doc/homebrew/Formula-Cookbook.md
+ # http://www.rubydoc.info/github/Homebrew/brew/master/Formula
+ # PLEASE REMOVE ALL GENERATED COMMENTS BEFORE SUBMITTING YOUR PULL REQUEST!
+
+ class #{Formulary.class_s(name)} < Formula
+ desc ""
+ homepage ""
+ <% if head? %>
+ head "#{url}"
+ <% else %>
+ url "#{url}"
+ <% unless version.nil? or version.detected_from_url? %>
+ version "#{version}"
+ <% end %>
+ sha256 "#{sha256}"
+ <% end %>
+
+ <% if mode == :cmake %>
+ depends_on "cmake" => :build
+ <% elsif mode.nil? %>
+ # depends_on "cmake" => :build
+ <% end %>
+ depends_on :x11 # if your formula requires any X11/XQuartz components
+
+ def install
+ # ENV.deparallelize # if your formula fails when building in parallel
+
+ <% if mode == :cmake %>
+ system "cmake", ".", *std_cmake_args
+ <% elsif mode == :autotools %>
+ # Remove unrecognized options if warned by configure
+ system "./configure", "--disable-debug",
+ "--disable-dependency-tracking",
+ "--disable-silent-rules",
+ "--prefix=\#{prefix}"
+ <% else %>
+ # Remove unrecognized options if warned by configure
+ system "./configure", "--disable-debug",
+ "--disable-dependency-tracking",
+ "--disable-silent-rules",
+ "--prefix=\#{prefix}"
+ # system "cmake", ".", *std_cmake_args
+ <% end %>
+ system "make", "install" # if this fails, try separate make/make install steps
+ end
+
+ test do
+ # `test do` will create, run in and delete a temporary directory.
+ #
+ # This test will fail and we won't accept that! It's enough to just replace
+ # "false" with the main program this formula installs, but it'd be nice if you
+ # were more thorough. Run the test with `brew test #{name}`. Options passed
+ # to `brew install` such as `--HEAD` also need to be provided to `brew test`.
+ #
+ # The installed folder is not in the path, so use the entire path to any
+ # executables being tested: `system "\#{bin}/program", "do", "something"`.
+ system "false"
+ end
+ end
+ EOS
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/edit.rb b/Library/Homebrew/dev-cmd/edit.rb
new file mode 100644
index 000000000..ef325b8b6
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/edit.rb
@@ -0,0 +1,50 @@
+#: * `edit`:
+#: Open all of Homebrew for editing.
+#:
+#: * `edit` <formula>:
+#: Open <formula> in the editor.
+
+require "formula"
+
+module Homebrew
+ def edit
+ unless (HOMEBREW_REPOSITORY/".git").directory?
+ raise <<-EOS.undent
+ Changes will be lost!
+ The first time you `brew update', all local changes will be lost, you should
+ thus `brew update' before you `brew edit'!
+ EOS
+ end
+
+ # If no brews are listed, open the project root in an editor.
+ if ARGV.named.empty?
+ editor = File.basename which_editor
+ if editor == "mate" || editor == "subl"
+ # If the user is using TextMate or Sublime Text,
+ # give a nice project view instead.
+ exec_editor HOMEBREW_REPOSITORY+"bin/brew",
+ HOMEBREW_REPOSITORY+"README.md",
+ HOMEBREW_REPOSITORY+".gitignore",
+ *library_folders
+ else
+ exec_editor HOMEBREW_REPOSITORY
+ end
+ else
+ # Don't use ARGV.formulae as that will throw if the file doesn't parse
+ paths = ARGV.named.map do |name|
+ path = Formulary.path(name)
+ unless path.file? || ARGV.force?
+ raise FormulaUnavailableError, name
+ end
+ path
+ end
+ exec_editor(*paths)
+ end
+ end
+
+ def library_folders
+ Dir["#{HOMEBREW_LIBRARY}/*"].reject do |d|
+ case File.basename(d) when "LinkedKegs", "Aliases" then true end
+ end
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/man.rb b/Library/Homebrew/dev-cmd/man.rb
new file mode 100644
index 000000000..6754a15f2
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/man.rb
@@ -0,0 +1,88 @@
+#: @hide_from_man_page
+#: * `man`:
+#: Generate Homebrew's manpages.
+
+require "formula"
+require "erb"
+require "ostruct"
+
+module Homebrew
+ SOURCE_PATH = HOMEBREW_LIBRARY_PATH/"manpages"
+ TARGET_MAN_PATH = HOMEBREW_REPOSITORY/"share/man/man1"
+ TARGET_DOC_PATH = HOMEBREW_REPOSITORY/"share/doc/homebrew"
+
+ def man
+ raise UsageError unless ARGV.named.empty?
+
+ if ARGV.flag? "--link"
+ odie "`brew man --link` is now done automatically by `brew update`."
+ else
+ regenerate_man_pages
+ end
+ end
+
+ private
+
+ def regenerate_man_pages
+ Homebrew.install_gem_setup_path! "ronn"
+
+ markup = build_man_page
+ convert_man_page(markup, TARGET_DOC_PATH/"brew.1.html")
+ convert_man_page(markup, TARGET_MAN_PATH/"brew.1")
+
+ cask_markup = (HOMEBREW_LIBRARY/"Homebrew/manpages/brew-cask.1.md").read
+ convert_man_page(cask_markup, TARGET_MAN_PATH/"brew-cask.1")
+ end
+
+ def build_man_page
+ template = (SOURCE_PATH/"brew.1.md.erb").read
+ variables = OpenStruct.new
+
+ variables[:commands] = Pathname.glob("#{HOMEBREW_LIBRARY_PATH}/cmd/*.{rb,sh}").
+ sort_by { |source_file| sort_key_for_path(source_file) }.
+ map { |source_file|
+ source_file.read.lines.
+ grep(/^#:/).
+ map { |line| line.slice(2..-1) }.
+ join
+ }.
+ reject { |s| s.strip.empty? || s.include?("@hide_from_man_page") }
+
+ variables[:maintainers] = (HOMEBREW_REPOSITORY/"README.md").
+ read[/Homebrew's current maintainers are (.*)\./, 1].
+ scan(/\[([^\]]*)\]/).flatten
+
+ ERB.new(template, nil, ">").result(variables.instance_eval{ binding })
+ end
+
+ def sort_key_for_path(path)
+ # Options after regular commands (`~` comes after `z` in ASCII table).
+ path.basename.to_s.sub(/\.(rb|sh)$/, "").sub(/^--/, "~~")
+ end
+
+ def convert_man_page(markup, target)
+ shared_args = %W[
+ --pipe
+ --organization=Homebrew
+ --manual=#{target.basename(".1")}
+ ]
+
+ format_flag, format_desc = target_path_to_format(target)
+
+ puts "Writing #{format_desc} to #{target}"
+ Utils.popen(["ronn", format_flag] + shared_args, "rb+") do |ronn|
+ ronn.write markup
+ ronn.close_write
+ target.atomic_write ronn.read
+ end
+ end
+
+ def target_path_to_format(target)
+ case target.basename
+ when /\.html?$/ then ["--fragment", "HTML fragment"]
+ when /\.\d$/ then ["--roff", "man page"]
+ else
+ odie "Failed to infer output format from '#{target.basename}'."
+ end
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/pull.rb b/Library/Homebrew/dev-cmd/pull.rb
new file mode 100644
index 000000000..341eed34a
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/pull.rb
@@ -0,0 +1,575 @@
+#: @hide_from_man_page
+#: `pull` [`--bottle`] [`--bump`] [`--clean`] [`--ignore-whitespace`] [`--resolve`] [`--branch-okay`] [`--no-pbcopy`] [`--no-publish`] <patch-source> [<patch-source>]
+#:
+#: Gets a patch from a GitHub commit or pull request and applies it to Homebrew.
+#: Optionally, installs the formulae changed by the patch.
+#:
+#:
+#: Each <patch-source> may be one of:
+#: * The ID number of a PR (Pull Request) in the homebrew/core GitHub
+#: repository
+#: * The URL of a PR on GitHub, using either the web page or API URL
+#: formats. In this form, the PR may be on Homebrew/brew,
+#: Homebrew/homebrew-core or any tap.
+#: * The URL of a commit on GitHub
+#: * A "http://bot.brew.sh/job/..." string specifying a testing job ID
+#:
+#: If `--bottle` was passed, handle bottles, pulling the bottle-update
+#: commit and publishing files on Bintray.
+#: If `--bump` was passed, for one-formula PRs, automatically reword
+#: commit message to our preferred format.
+#: If `--clean` was passed, do not rewrite or otherwise modify the
+#: commits found in the pulled PR.
+#: If `--ignore-whitespace` was passed, silently ignore whitespace
+#: discrepancies when applying diffs.
+#: If `--resolve` was passed, when a patch fails to apply, leave in
+#: progress and allow user to
+#: resolve, instead of aborting.
+#: If `--branch-okay` was passed, do not warn if pulling to a branch
+#: besides master (useful for testing).
+#: If `--no-pbcopy` was passed, do not copy anything to the system
+# clipboard.
+#: If `--no-publish` was passed, do not publish bottles to Bintray.
+
+require "net/http"
+require "net/https"
+require "utils"
+require "utils/json"
+require "formula"
+require "formulary"
+require "tap"
+require "version"
+require "pkg_version"
+
+module Homebrew
+ def pull
+ if ARGV[0] == "--rebase"
+ odie "You meant `git pull --rebase`."
+ end
+ if ARGV.named.empty?
+ odie "This command requires at least one argument containing a URL or pull request number"
+ end
+ do_bump = ARGV.include?("--bump") && !ARGV.include?("--clean")
+
+ # Formulae with affected bottles that were published
+ bintray_published_formulae = []
+ tap = nil
+
+ ARGV.named.each do |arg|
+ if arg.to_i > 0
+ issue = arg
+ url = "https://github.com/Homebrew/homebrew-core/pull/#{arg}"
+ tap = CoreTap.instance
+ elsif (testing_match = arg.match %r{/job/Homebrew.*Testing/(\d+)/})
+ tap = ARGV.value("tap")
+ tap = if tap && tap.start_with?("homebrew/")
+ Tap.fetch("homebrew", tap.strip_prefix("homebrew/"))
+ elsif tap
+ odie "Tap option did not start with \"homebrew/\": #{tap}"
+ else
+ CoreTap.instance
+ end
+ _, testing_job = *testing_match
+ url = "https://github.com/Homebrew/homebrew-#{tap.repo}/compare/master...BrewTestBot:testing-#{testing_job}"
+ odie "Testing URLs require `--bottle`!" unless ARGV.include?("--bottle")
+ elsif (api_match = arg.match HOMEBREW_PULL_API_REGEX)
+ _, user, repo, issue = *api_match
+ url = "https://github.com/#{user}/#{repo}/pull/#{issue}"
+ tap = Tap.fetch(user, repo) if repo.start_with?("homebrew-")
+ elsif (url_match = arg.match HOMEBREW_PULL_OR_COMMIT_URL_REGEX)
+ url, user, repo, issue = *url_match
+ tap = Tap.fetch(user, repo) if repo.start_with?("homebrew-")
+ else
+ odie "Not a GitHub pull request or commit: #{arg}"
+ end
+
+ if !testing_job && ARGV.include?("--bottle") && issue.nil?
+ odie "No pull request detected!"
+ end
+
+ if tap
+ tap.install unless tap.installed?
+ Dir.chdir tap.path
+ else
+ Dir.chdir HOMEBREW_REPOSITORY
+ end
+
+ # The cache directory seems like a good place to put patches.
+ HOMEBREW_CACHE.mkpath
+
+ # Store current revision and branch
+ orig_revision = `git rev-parse --short HEAD`.strip
+ branch = `git symbolic-ref --short HEAD`.strip
+
+ unless branch == "master" || ARGV.include?("--clean") || ARGV.include?("--branch-okay")
+ opoo "Current branch is #{branch}: do you need to pull inside master?"
+ end
+
+ patch_puller = PatchPuller.new(url)
+ patch_puller.fetch_patch
+ patch_changes = files_changed_in_patch(patch_puller.patchpath, tap)
+
+ is_bumpable = patch_changes[:formulae].length == 1 && patch_changes[:others].empty?
+ if do_bump
+ odie "No changed formulae found to bump" if patch_changes[:formulae].empty?
+ if patch_changes[:formulae].length > 1
+ odie "Can only bump one changed formula; bumped #{patch_changes[:formulae]}"
+ end
+ odie "Can not bump if non-formula files are changed" unless patch_changes[:others].empty?
+ end
+ if is_bumpable
+ old_versions = current_versions_from_info_external(patch_changes[:formulae].first)
+ end
+ patch_puller.apply_patch
+
+ changed_formulae_names = []
+
+ if tap
+ Utils.popen_read(
+ "git", "diff-tree", "-r", "--name-only",
+ "--diff-filter=AM", orig_revision, "HEAD", "--", tap.formula_dir.to_s
+ ).each_line do |line|
+ next unless line.end_with? ".rb\n"
+ name = "#{tap.name}/#{File.basename(line.chomp, ".rb")}"
+ changed_formulae_names << name
+ end
+ end
+
+ fetch_bottles = false
+ changed_formulae_names.each do |name|
+ next if ENV["HOMEBREW_DISABLE_LOAD_FORMULA"]
+
+ begin
+ f = Formula[name]
+ # Make sure we catch syntax errors.
+ rescue Exception
+ next
+ end
+
+ if ARGV.include? "--bottle"
+ if f.bottle_unneeded?
+ ohai "#{f}: skipping unneeded bottle."
+ elsif f.bottle_disabled?
+ ohai "#{f}: skipping disabled bottle: #{f.bottle_disable_reason}"
+ else
+ fetch_bottles = true
+ end
+ else
+ next unless f.bottle_defined?
+ opoo "#{f.full_name} has a bottle: do you need to update it with --bottle?"
+ end
+ end
+
+ orig_message = message = `git log HEAD^.. --format=%B`
+ if issue && !ARGV.include?("--clean")
+ ohai "Patch closes issue ##{issue}"
+ close_message = "Closes ##{issue}."
+ # If this is a pull request, append a close message.
+ message += "\n#{close_message}" unless message.include? close_message
+ end
+
+ if changed_formulae_names.empty?
+ odie "cannot bump: no changed formulae found after applying patch" if do_bump
+ is_bumpable = false
+ end
+
+ is_bumpable = false if ARGV.include?("--clean")
+ is_bumpable = false if ENV["HOMEBREW_DISABLE_LOAD_FORMULA"]
+
+ if is_bumpable
+ formula = Formula[changed_formulae_names.first]
+ new_versions = current_versions_from_info_external(patch_changes[:formulae].first)
+ orig_subject = message.empty? ? "" : message.lines.first.chomp
+ bump_subject = subject_for_bump(formula, old_versions, new_versions)
+ if do_bump
+ odie "No version changes found for #{formula.name}" if bump_subject.nil?
+ unless orig_subject == bump_subject
+ ohai "New bump commit subject: #{bump_subject}"
+ pbcopy bump_subject unless ARGV.include? "--no-pbcopy"
+ message = "#{bump_subject}\n\n#{message}"
+ end
+ elsif bump_subject != orig_subject && !bump_subject.nil?
+ opoo "Nonstandard bump subject: #{orig_subject}"
+ opoo "Subject should be: #{bump_subject}"
+ end
+ end
+
+ if message != orig_message && !ARGV.include?("--clean")
+ safe_system "git", "commit", "--amend", "--signoff", "--allow-empty", "-q", "-m", message
+ end
+
+ # Bottles: Pull bottle block commit and publish bottle files on Bintray
+ if fetch_bottles
+ bottle_commit_url = if testing_job
+ bottle_branch = "testing-bottle-#{testing_job}"
+ url
+ else
+ bottle_branch = "pull-bottle-#{issue}"
+ "https://github.com/BrewTestBot/homebrew-#{tap.repo}/compare/homebrew:master...pr-#{issue}"
+ end
+
+ curl "--silent", "--fail", "-o", "/dev/null", "-I", bottle_commit_url
+
+ safe_system "git", "checkout", "--quiet", "-B", bottle_branch, orig_revision
+ pull_patch bottle_commit_url, "bottle commit"
+ safe_system "git", "rebase", "--quiet", branch
+ safe_system "git", "checkout", "--quiet", branch
+ safe_system "git", "merge", "--quiet", "--ff-only", "--no-edit", bottle_branch
+ safe_system "git", "branch", "--quiet", "-D", bottle_branch
+
+ # Publish bottles on Bintray
+ unless ARGV.include? "--no-publish"
+ published = publish_changed_formula_bottles(tap, changed_formulae_names)
+ bintray_published_formulae.concat(published)
+ end
+ end
+
+ ohai "Patch changed:"
+ safe_system "git", "diff-tree", "-r", "--stat", orig_revision, "HEAD"
+ end
+
+ # Verify bintray publishing after all patches have been applied
+ bintray_published_formulae.uniq!
+ verify_bintray_published(bintray_published_formulae)
+ end
+
+ def force_utf8!(str)
+ str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
+ end
+
+ private
+
+ def publish_changed_formula_bottles(tap, changed_formulae_names)
+ if ENV["HOMEBREW_DISABLE_LOAD_FORMULA"]
+ raise "Need to load formulae to publish them!"
+ end
+
+ published = []
+ bintray_creds = { :user => ENV["BINTRAY_USER"], :key => ENV["BINTRAY_KEY"] }
+ if bintray_creds[:user] && bintray_creds[:key]
+ changed_formulae_names.each do |name|
+ f = Formula[name]
+ next if f.bottle_unneeded? || f.bottle_disabled?
+ ohai "Publishing on Bintray: #{f.name} #{f.pkg_version}"
+ publish_bottle_file_on_bintray(f, bintray_creds)
+ published << f.full_name
+ end
+ else
+ opoo "You must set BINTRAY_USER and BINTRAY_KEY to add or update bottles on Bintray!"
+ end
+ published
+ end
+
+ def pull_patch(url, description = nil)
+ PatchPuller.new(url, description).pull_patch
+ end
+
+ class PatchPuller
+ attr_reader :base_url
+ attr_reader :patch_url
+ attr_reader :patchpath
+
+ def initialize(url, description = nil)
+ @base_url = url
+ # GitHub provides commits/pull-requests raw patches using this URL.
+ @patch_url = url + ".patch"
+ @patchpath = HOMEBREW_CACHE + File.basename(patch_url)
+ @description = description
+ end
+
+ def pull_patch
+ fetch_patch
+ apply_patch
+ end
+
+ def fetch_patch
+ extra_msg = @description ? "(#{@description})" : nil
+ ohai "Fetching patch #{extra_msg}"
+ puts "Patch: #{patch_url}"
+ curl patch_url, "-s", "-o", patchpath
+ end
+
+ def apply_patch
+ # Applies a patch previously downloaded with fetch_patch()
+ # Deletes the patch file as a side effect, regardless of success
+
+ ohai "Applying patch"
+ patch_args = []
+ # Normally we don't want whitespace errors, but squashing them can break
+ # patches so an option is provided to skip this step.
+ if ARGV.include?("--ignore-whitespace") || ARGV.include?("--clean")
+ patch_args << "--whitespace=nowarn"
+ else
+ patch_args << "--whitespace=fix"
+ end
+
+ # Fall back to three-way merge if patch does not apply cleanly
+ patch_args << "-3"
+ patch_args << patchpath
+
+ begin
+ safe_system "git", "am", *patch_args
+ rescue ErrorDuringExecution
+ if ARGV.include? "--resolve"
+ odie "Patch failed to apply: try to resolve it."
+ else
+ system "git", "am", "--abort"
+ odie "Patch failed to apply: aborted."
+ end
+ ensure
+ patchpath.unlink
+ end
+ end
+ end
+
+ # List files changed by a patch, partitioned in to those that are (probably)
+ # formula definitions, and those which aren't. Only applies to patches on
+ # Homebrew core or taps, based simply on relative pathnames of affected files.
+ def files_changed_in_patch(patchfile, tap)
+ files = []
+ formulae = []
+ others = []
+ File.foreach(patchfile) do |line|
+ files << $1 if line =~ %r{^\+\+\+ b/(.*)}
+ end
+ files.each do |file|
+ if tap && tap.formula_file?(file)
+ formula_name = File.basename(file, ".rb")
+ formulae << formula_name unless formulae.include?(formula_name)
+ else
+ others << file
+ end
+ end
+ { :files => files, :formulae => formulae, :others => others }
+ end
+
+ # Get current formula versions without loading formula definition in this process
+ # Returns info as a hash (type => version), for pull.rb's internal use
+ # Uses special key :nonexistent => true for nonexistent formulae
+ def current_versions_from_info_external(formula_name)
+ info = FormulaInfoFromJson.lookup(formula_name)
+ versions = {}
+ if info
+ [:stable, :devel, :head].each do |spec_type|
+ versions[spec_type] = info.version(spec_type)
+ end
+ else
+ versions[:nonexistent] = true
+ end
+ versions
+ end
+
+ def subject_for_bump(formula, old, new)
+ if old[:nonexistent]
+ # New formula
+ headline_ver = new[:stable] ? new[:stable] : new[:devel] ? new[:devel] : new[:head]
+ subject = "#{formula.name} #{headline_ver} (new formula)"
+ else
+ # Update to existing formula
+ subject_strs = []
+ formula_name_str = formula.name
+ if old[:stable] != new[:stable]
+ if new[:stable].nil?
+ subject_strs << "remove stable"
+ formula_name_str += ":" # just for cosmetics
+ else
+ subject_strs << formula.version.to_s
+ end
+ end
+ if old[:devel] != new[:devel]
+ if new[:devel].nil?
+ # Only bother mentioning if there's no accompanying stable change
+ if !new[:stable].nil? && old[:stable] == new[:stable]
+ subject_strs << "remove devel"
+ formula_name_str += ":" # just for cosmetics
+ end
+ else
+ subject_strs << "#{formula.devel.version} (devel)"
+ end
+ end
+ subject = subject_strs.empty? ? nil : "#{formula_name_str} #{subject_strs.join(", ")}"
+ end
+ subject
+ end
+
+ def pbcopy(text)
+ Utils.popen_write("pbcopy") { |io| io.write text }
+ end
+
+ # Publishes the current bottle files for a given formula to Bintray
+ def publish_bottle_file_on_bintray(f, creds)
+ repo = Utils::Bottles::Bintray.repository(f.tap)
+ package = Utils::Bottles::Bintray.package(f.name)
+ info = FormulaInfoFromJson.lookup(f.name)
+ if info.nil?
+ raise "Failed publishing bottle: failed reading formula info for #{f.full_name}"
+ end
+ version = info.pkg_version
+ curl "-w", '\n', "--silent", "--fail",
+ "-u#{creds[:user]}:#{creds[:key]}", "-X", "POST",
+ "-H", "Content-Type: application/json",
+ "-d", '{"publish_wait_for_secs": 0}',
+ "https://api.bintray.com/content/homebrew/#{repo}/#{package}/#{version}/publish"
+ end
+
+ # Formula info drawn from an external "brew info --json" call
+ class FormulaInfoFromJson
+ # The whole info structure parsed from the JSON
+ attr_accessor :info
+
+ def initialize(info)
+ @info = info
+ end
+
+ # Looks up formula on disk and reads its info
+ # Returns nil if formula is absent or if there was an error reading it
+ def self.lookup(name)
+ json = Utils.popen_read(HOMEBREW_BREW_FILE, "info", "--json=v1", name)
+ unless $?.success?
+ return nil
+ end
+ Homebrew.force_utf8!(json)
+ FormulaInfoFromJson.new(Utils::JSON.load(json)[0])
+ end
+
+ def bottle_tags()
+ return [] unless info["bottle"]["stable"]
+ info["bottle"]["stable"]["files"].keys
+ end
+
+ def bottle_info(my_bottle_tag = Utils::Bottles.tag)
+ tag_s = my_bottle_tag.to_s
+ return nil unless info["bottle"]["stable"]
+ btl_info = info["bottle"]["stable"]["files"][tag_s]
+ return nil unless btl_info
+ BottleInfo.new(btl_info["url"], btl_info["sha256"])
+ end
+
+ def bottle_info_any
+ bottle_info(any_bottle_tag)
+ end
+
+ def any_bottle_tag
+ tag = Utils::Bottles.tag
+ # Prefer native bottles as a convenience for download caching
+ bottle_tags.include?(tag) ? tag : bottle_tags.first
+ end
+
+ def version(spec_type)
+ version_str = info["versions"][spec_type.to_s]
+ version_str && Version.create(version_str)
+ end
+
+ def pkg_version(spec_type = :stable)
+ PkgVersion.new(version(spec_type), revision)
+ end
+
+ def revision
+ info["revision"]
+ end
+ end
+
+
+ # Bottle info as used internally by pull, with alternate platform support
+ class BottleInfo
+ # URL of bottle as string
+ attr_accessor :url
+ # Expected SHA256 as string
+ attr_accessor :sha256
+
+ def initialize(url, sha256)
+ @url = url
+ @sha256 = sha256
+ end
+ end
+
+ # Verifies that formulae have been published on Bintray by downloading a bottle file
+ # for each one. Blocks until the published files are available.
+ # Raises an error if the verification fails.
+ # This does not currently work for `brew pull`, because it may have cached the old
+ # version of a formula.
+ def verify_bintray_published(formulae_names)
+ return if formulae_names.empty?
+
+ if ENV["HOMEBREW_DISABLE_LOAD_FORMULA"]
+ raise "Need to load formulae to verify their publication!"
+ end
+
+ ohai "Verifying bottles published on Bintray"
+ formulae = formulae_names.map { |n| Formula[n] }
+ max_retries = 300 # shared among all bottles
+ poll_retry_delay_seconds = 2
+
+ HOMEBREW_CACHE.cd do
+ formulae.each do |f|
+ retry_count = 0
+ wrote_dots = false
+ # Choose arbitrary bottle just to get the host/port for Bintray right
+ jinfo = FormulaInfoFromJson.lookup(f.full_name)
+ unless jinfo
+ opoo "Cannot publish bottle: Failed reading info for formula #{f.full_name}"
+ next
+ end
+ bottle_info = jinfo.bottle_info(jinfo.bottle_tags.first)
+ unless bottle_info
+ opoo "No bottle defined in formula #{f.full_name}"
+ next
+ end
+
+ # Poll for publication completion using a quick partial HEAD, to avoid spurious error messages
+ # 401 error is normal while file is still in async publishing process
+ url = URI(bottle_info.url)
+ puts "Verifying bottle: #{File.basename(url.path)}"
+ http = Net::HTTP.new(url.host, url.port)
+ http.use_ssl = true
+ retry_count = 0
+ http.start do
+ while true do
+ req = Net::HTTP::Head.new bottle_info.url
+ req.initialize_http_header "User-Agent" => HOMEBREW_USER_AGENT_RUBY
+ res = http.request req
+ if res.is_a?(Net::HTTPSuccess)
+ break
+ elsif res.is_a?(Net::HTTPClientError)
+ if retry_count >= max_retries
+ raise "Failed to find published #{f} bottle at #{url}!"
+ end
+ print(wrote_dots ? "." : "Waiting on Bintray.")
+ wrote_dots = true
+ sleep poll_retry_delay_seconds
+ retry_count += 1
+ else
+ raise "Failed to find published #{f} bottle at #{url} (#{res.code} #{res.message})!"
+ end
+ end
+ end
+
+ # Actual download and verification
+ # We do a retry on this, too, because sometimes the external curl will fail even
+ # when the prior HEAD has succeeded.
+ puts "\n" if wrote_dots
+ filename = File.basename(url.path)
+ curl_retry_delay_seconds = 4
+ max_curl_retries = 1
+ retry_count = 0
+ # We're in the cache; make sure to force re-download
+ while true do
+ begin
+ curl url, "-o", filename
+ break
+ rescue
+ if retry_count >= max_curl_retries
+ raise "Failed to download #{f} bottle from #{url}!"
+ end
+ puts "curl download failed; retrying in #{curl_retry_delay_seconds} sec"
+ sleep curl_retry_delay_seconds
+ curl_retry_delay_seconds *= 2
+ retry_count += 1
+ end
+ end
+ checksum = Checksum.new(:sha256, bottle_info.sha256)
+ Pathname.new(filename).verify_checksum(checksum)
+ end
+ end
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/tap-readme.rb b/Library/Homebrew/dev-cmd/tap-readme.rb
new file mode 100644
index 000000000..ad115a53e
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/tap-readme.rb
@@ -0,0 +1,36 @@
+#: @hide_from_man_page
+#: * `tap_readme` [`-v`] <name>:
+#: Generate the README.md file for a new tap.
+
+module Homebrew
+ def tap_readme
+ name = ARGV.first
+ raise "A name is required" if name.nil?
+
+ titleized_name = name.dup
+ titleized_name[0..0] = titleized_name[0..0].upcase
+
+ template = <<-EOS.undent
+ # Homebrew #{titleized_name}
+
+ ## How do I install these formulae?
+ `brew install homebrew/#{name}/<formula>`
+
+ Or `brew tap homebrew/#{name}` and then `brew install <formula>`.
+
+ Or install via URL (which will not receive updates):
+
+ ```
+ brew install https://raw.githubusercontent.com/Homebrew/homebrew-#{name}/master/<formula>.rb
+ ```
+
+ ## Documentation
+ `brew help`, `man brew` or check [Homebrew's documentation](https://github.com/Homebrew/brew/tree/master/share/doc/homebrew#readme).
+ EOS
+
+ puts template if ARGV.verbose?
+ path = HOMEBREW_LIBRARY/"Taps/homebrew/homebrew-#{name}/README.md"
+ raise "#{path} already exists" if path.exist?
+ path.write template
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/test.rb b/Library/Homebrew/dev-cmd/test.rb
new file mode 100644
index 000000000..a80fa5e4f
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/test.rb
@@ -0,0 +1,89 @@
+#: * `test` [`--devel`|`--HEAD`] [`--debug`] [`--keep-tmp`] <formula>:
+#: A few formulae provide a test method. `brew test` <formula> runs this
+#: test method. There is no standard output or return code, but it should
+#: generally indicate to the user if something is wrong with the installed
+#: formula.
+#:
+#: To test the development or head version of a formula, use `--devel` or
+#: `--HEAD`.
+#:
+#: If `--debug` is passed and the test fails, an interactive debugger will be
+#: launched with access to IRB or a shell inside the temporary test directory.
+#:
+#: If `--keep-tmp` is passed, the temporary files created for the test are
+#: not deleted.
+#:
+#: Example: `brew install jruby && brew test jruby`
+
+require "extend/ENV"
+require "formula_assertions"
+require "sandbox"
+require "timeout"
+
+module Homebrew
+ def test
+ raise FormulaUnspecifiedError if ARGV.named.empty?
+
+ ARGV.resolved_formulae.each do |f|
+ # Cannot test uninstalled formulae
+ unless f.installed?
+ ofail "Testing requires the latest version of #{f.full_name}"
+ next
+ end
+
+ # Cannot test formulae without a test method
+ unless f.test_defined?
+ ofail "#{f.full_name} defines no test"
+ next
+ end
+
+ puts "Testing #{f.full_name}"
+
+ env = ENV.to_hash
+
+ begin
+ args = %W[
+ #{RUBY_PATH}
+ -W0
+ -I #{HOMEBREW_LOAD_PATH}
+ --
+ #{HOMEBREW_LIBRARY_PATH}/test.rb
+ #{f.path}
+ ].concat(ARGV.options_only)
+
+ if f.head?
+ args << "--HEAD"
+ elsif f.devel?
+ args << "--devel"
+ end
+
+ Sandbox.print_sandbox_message if Sandbox.test?
+
+ Utils.safe_fork do
+ if Sandbox.test?
+ sandbox = Sandbox.new
+ f.logs.mkpath
+ sandbox.record_log(f.logs/"test.sandbox.log")
+ sandbox.allow_write_temp_and_cache
+ sandbox.allow_write_log(f)
+ sandbox.allow_write_xcode
+ sandbox.allow_write_path(HOMEBREW_PREFIX/"var/cache")
+ sandbox.allow_write_path(HOMEBREW_PREFIX/"var/log")
+ sandbox.allow_write_path(HOMEBREW_PREFIX/"var/run")
+ sandbox.exec(*args)
+ else
+ exec(*args)
+ end
+ end
+ rescue Assertions::FailedAssertion => e
+ ofail "#{f.full_name}: failed"
+ puts e.message
+ rescue Exception => e
+ ofail "#{f.full_name}: failed"
+ puts e, e.backtrace
+ ensure
+ ENV.replace(env)
+ end
+ end
+ end
+end
diff --git a/Library/Homebrew/dev-cmd/tests.rb b/Library/Homebrew/dev-cmd/tests.rb
new file mode 100644
index 000000000..be8f72ace
--- /dev/null
+++ b/Library/Homebrew/dev-cmd/tests.rb
@@ -0,0 +1,63 @@
+#: @hide_from_man_page
+#: * `tests` [`-v`] [`--coverage`] [`--generic`] [`--no-compat`] [`--only=`<test_script/test_method>] [`--seed` <seed>] [`--trace`] [`--online`] [`--official-cmd-taps`]:
+#: Run Homebrew's unit and integration tests.
+
+require "fileutils"
+require "tap"
+
+module Homebrew
+ def tests
+ (HOMEBREW_LIBRARY/"Homebrew/test").cd do
+ ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "1"
+ ENV["TESTOPTS"] = "-v" if ARGV.verbose?
+ ENV["HOMEBREW_NO_COMPAT"] = "1" if ARGV.include? "--no-compat"
+ ENV["HOMEBREW_TEST_GENERIC_OS"] = "1" if ARGV.include? "--generic"
+ ENV["HOMEBREW_NO_GITHUB_API"] = "1" unless ARGV.include? "--online"
+ if ARGV.include? "--official-cmd-taps"
+ ENV["HOMEBREW_TEST_OFFICIAL_CMD_TAPS"] = "1"
+ end
+
+ if ARGV.include? "--coverage"
+ ENV["HOMEBREW_TESTS_COVERAGE"] = "1"
+ FileUtils.rm_f "coverage/.resultset.json"
+ end
+
+ # Override author/committer as global settings might be invalid and thus
+ # will cause silent failure during the setup of dummy Git repositories.
+ %w[AUTHOR COMMITTER].each do |role|
+ ENV["GIT_#{role}_NAME"] = "brew tests"
+ ENV["GIT_#{role}_EMAIL"] = "brew-tests@localhost"
+ end
+
+ Homebrew.install_gem_setup_path! "bundler"
+ unless quiet_system("bundle", "check")
+ system "bundle", "install", "--path", "vendor/bundle"
+ end
+
+ # Make it easier to reproduce test runs.
+ ENV["SEED"] = ARGV.next if ARGV.include? "--seed"
+
+ args = []
+ args << "--trace" if ARGV.include? "--trace"
+ if ARGV.value("only")
+ ENV["HOMEBREW_TESTS_ONLY"] = "1"
+ test_name, test_method = ARGV.value("only").split("/", 2)
+ args << "TEST=test_#{test_name}.rb"
+ args << "TESTOPTS=--name=test_#{test_method}" if test_method
+ end
+ args += ARGV.named.select { |v| v[/^TEST(OPTS)?=/] }
+ system "bundle", "exec", "rake", "test", *args
+
+ Homebrew.failed = !$?.success?
+
+ if (fs_leak_log = HOMEBREW_LIBRARY/"Homebrew/test/fs_leak_log").file?
+ fs_leak_log_content = fs_leak_log.read
+ unless fs_leak_log_content.empty?
+ opoo "File leak is detected"
+ puts fs_leak_log_content
+ Homebrew.failed = true
+ end
+ end
+ end
+ end
+end