diff options
| author | Mike McQuaid | 2016-09-05 21:37:02 +0100 | 
|---|---|---|
| committer | Mike McQuaid | 2016-09-08 20:46:37 +0100 | 
| commit | af8605ea4ba1d9856c055c8c76b447e030540e3f (patch) | |
| tree | d093b11340406c21a4b873a80effe3b068fd07d4 /Library/Homebrew/dev-cmd | |
| parent | 4f6bae46f9c0f7b713cdbb999318460135f423de (diff) | |
| download | brew-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.rb | 1361 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/bottle.rb | 458 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/create.rb | 218 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/edit.rb | 50 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/man.rb | 88 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/pull.rb | 575 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/tap-readme.rb | 36 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/test.rb | 89 | ||||
| -rw-r--r-- | Library/Homebrew/dev-cmd/tests.rb | 63 | 
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  | 
