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 |
