1604 lines
54 KiB
Ruby
Raw Normal View History

2017-01-18 15:55:32 +05:30
#: * `audit` [`--strict`] [`--fix`] [`--online`] [`--new-formula`] [`--display-cop-names`] [`--display-filename`] [<formulae>]:
2016-04-08 16:28:43 +02:00
#: 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.
2016-04-08 16:28:43 +02:00
#:
#: If `--fix` is passed, style violations will be
2017-01-18 15:55:32 +05:30
#: automatically fixed using RuboCop's `--auto-correct` feature.
2016-04-08 16:28:43 +02:00
#:
#: 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 eligible 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.
#:
2016-04-08 16:28:43 +02:00
#: `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"
2015-07-09 15:28:27 +01:00
require "date"
require "missing_formula"
require "digest"
2012-03-17 19:49:49 -07:00
module Homebrew
2016-09-26 01:44:51 +02:00
module_function
2012-08-07 01:37:46 -05:00
def audit
2016-09-25 01:51:37 +02:00
Homebrew.inject_dump_stats!(FormulaAuditor, /^audit_/) if ARGV.switch? "D"
2012-08-07 01:37:46 -05:00
formula_count = 0
problem_count = 0
new_formula = ARGV.include? "--new-formula"
strict = new_formula || ARGV.include?("--strict")
online = new_formula || ARGV.include?("--online")
2013-08-19 13:03:41 -05:00
ENV.activate_extensions!
ENV.setup_build_environment
if ARGV.named.empty?
ff = Formula
files = Tap.map(&:formula_dir)
2012-08-07 01:37:46 -05:00
else
ff = ARGV.resolved_formulae
files = ARGV.resolved_formulae.map(&:path)
end
2016-08-16 17:00:31 +01:00
if strict
2017-01-18 15:55:32 +05:30
options = { fix: ARGV.flag?("--fix"), realpath: true }
# Check style in a single batch run up front for performance
2017-01-18 15:55:32 +05:30
style_results = check_style_json(files, options)
2012-08-07 01:37:46 -05:00
end
2014-12-27 12:38:04 +00:00
2012-08-07 01:37:46 -05:00
ff.each do |f|
options = { new_formula: new_formula, strict: strict, online: online }
2016-08-16 17:00:31 +01:00
options[:style_offenses] = style_results.file_offenses(f.path) if strict
fa = FormulaAuditor.new(f, options)
2012-08-07 01:37:46 -05:00
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
2012-08-07 01:37:46 -05:00
end
2016-09-22 20:12:28 +02:00
return if problem_count.zero?
2017-03-11 11:33:12 +01:00
ofail "#{Formatter.pluralize(problem_count, "problem")} in #{Formatter.pluralize(formula_count, "formula")}"
2012-08-07 01:37:46 -05:00
end
end
2012-03-17 19:49:49 -07:00
2012-08-07 01:37:46 -05:00
class FormulaText
def initialize(path)
2014-06-04 15:37:36 -05:00
@text = path.open("rb", &:read)
@lines = @text.lines.to_a
end
2012-08-07 01:37:46 -05:00
def without_patch
@text.split("\n__END__").first
end
2016-09-11 17:41:51 +01:00
def data?
/^[^#]*\bDATA\b/ =~ @text
2010-08-15 15:19:19 -07:00
end
2016-09-11 17:41:51 +01:00
def end?
2012-08-07 01:37:46 -05:00
/^__END__$/ =~ @text
end
2016-09-11 17:41:51 +01:00
def trailing_newline?
/\Z\n/ =~ @text
end
2016-09-11 17:41:51 +01:00
def =~(other)
other =~ @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
2012-08-07 01:37:46 -05:00
end
2012-08-07 01:37:46 -05:00
class FormulaAuditor
include FormulaCellarChecks
attr_reader :formula, :text, :problems
2012-08-07 01:37:46 -05:00
BUILD_TIME_DEPS = %w[
2012-08-07 01:37:46 -05:00
autoconf
automake
boost-build
bsdmake
cmake
godep
2012-08-07 01:37:46 -05:00
imake
intltool
2012-08-07 01:37:46 -05:00
libtool
pkg-config
scons
smake
sphinx-doc
2012-09-05 21:12:08 -07:00
swig
2016-09-11 17:41:51 +01:00
].freeze
2012-08-07 01:37:46 -05:00
2016-07-13 15:58:04 +08:00
FILEUTILS_METHODS = FileUtils.singleton_methods(false).map { |m| Regexp.escape(m) }.join "|"
def initialize(formula, options = {})
@formula = formula
2016-09-11 17:41:51 +01:00
@new_formula = options[:new_formula]
@strict = options[:strict]
@online = options[:online]
# Accept precomputed style offense results, for efficiency
@style_offenses = options[:style_offenses]
2012-08-07 01:37:46 -05:00
@problems = []
@text = FormulaText.new(formula.path)
@specs = %w[stable devel head].map { |s| formula.send(s) }.compact
2012-08-07 01:37:46 -05:00
end
def self.check_http_content(url, user_agents: [:default])
return unless url.start_with? "http"
details = nil
user_agent = nil
hash_needed = url.start_with?("http:")
user_agents.each do |ua|
details = http_content_headers_and_checksum(url, hash_needed: hash_needed, user_agent: ua)
user_agent = ua
break if details[:status].to_s.start_with?("2")
end
return "The URL #{url} is not reachable" unless details[:status]
unless details[:status].start_with? "2"
return "The URL #{url} is not reachable (HTTP status code #{details[:status]})"
end
return unless hash_needed
secure_url = url.sub "http", "https"
secure_details =
http_content_headers_and_checksum(secure_url, hash_needed: true, user_agent: user_agent)
if !details[:status].to_s.start_with?("2") ||
!secure_details[:status].to_s.start_with?("2")
return
end
etag_match = details[:etag] &&
details[:etag] == secure_details[:etag]
content_length_match =
details[:content_length] &&
details[:content_length] == secure_details[:content_length]
file_match = details[:file_hash] == secure_details[:file_hash]
return if !etag_match && !content_length_match && !file_match
"The URL #{url} could use HTTPS rather than HTTP"
end
def self.http_content_headers_and_checksum(url, hash_needed: false, user_agent: :default)
max_time = hash_needed ? "600" : "25"
args = curl_args(
extra_args: ["--connect-timeout", "15", "--include", "--max-time", max_time, url],
show_output: true,
user_agent: user_agent,
)
output = Open3.popen3(*args) { |_, stdout, _, _| stdout.read }
status_code = :unknown
while status_code == :unknown || status_code.to_s.start_with?("3")
headers, _, output = output.partition("\r\n\r\n")
status_code = headers[%r{HTTP\/.* (\d+)}, 1]
end
output_hash = Digest::SHA256.digest(output) if hash_needed
{
status: status_code,
etag: headers[%r{ETag: ([wW]\/)?"(([^"]|\\")*)"}, 2],
content_length: headers[/Content-Length: (\d+)/, 1],
file_hash: output_hash,
}
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"],
2016-01-28 13:37:45 +00:00
[/^ (go_)?resource/, "resource"],
[/^ def install/, "install method"],
[/^ def caveats/, "caveats method"],
2016-04-04 17:16:06 +01:00
[/^ (plist_options|def plist)/, "plist block"],
[/^ test do/, "test block"],
]
if previous_pair
previous_before = previous_pair[0]
previous_after = previous_pair[1]
end
2016-09-11 17:41:51 +01:00
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
2016-09-11 17:41:51 +01:00
elsif c1[0] > c2[0] && (offset.zero? || 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
2016-09-25 01:51:37 +02:00
problem "'DATA' was found, but no '__END__'" if text.data? && !text.end?
2016-09-11 17:41:51 +01:00
if text.end? && !text.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
2016-09-25 01:51:37 +02:00
problem "File should end with a newline" unless text.trailing_newline?
versioned_formulae = Dir[formula.path.to_s.gsub(/\.rb$/, "@*.rb")]
needs_versioned_alias = !versioned_formulae.empty? &&
formula.tap &&
formula.aliases.grep(/.@\d/).empty?
if needs_versioned_alias
_, last_alias_version = File.basename(versioned_formulae.sort.reverse.first)
.gsub(/\.rb$/, "")
.split("@")
major, minor, = formula.version.to_s.split(".")
alias_name = if last_alias_version.split(".").length == 1
"#{formula.name}@#{major}"
else
"#{formula.name}@#{major}.#{minor}"
end
problem <<-EOS.undent
Formula has other versions so create an alias:
cd #{formula.tap.alias_dir}
ln -s #{formula.path.to_s.gsub(formula.tap.path, "..")} #{alias_name}
EOS
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
2016-09-22 20:12:28 +02:00
if present.include?("head") && present.include?("head block")
problem "Should not have both `head` and `head do`"
end
2016-09-22 20:12:28 +02:00
2016-09-23 22:02:23 +02:00
return unless present.include?("bottle modifier") && present.include?("bottle block")
problem "Should not have `bottle :unneeded/:disable` and `bottle do`"
2014-12-27 20:46:01 +00:00
end
2014-12-27 20:46:01 +00:00
def audit_class
if @strict
2014-12-27 20:46:01 +00:00
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
2015-09-13 17:33:35 +08:00
# 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 Homebrew::MissingFormula.blacklisted_reason(name)
problem "'#{name}' is blacklisted."
end
2015-09-13 17:33:35 +08:00
if Formula.aliases.include? name
problem "Formula name conflicts with existing aliases."
return
end
if oldname = CoreTap.instance.formula_renames[name]
2015-12-09 12:09:55 +08:00
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
2016-09-11 17:41:51 +01:00
@@local_official_taps_name_map ||= Tap.select(&:official?).flat_map(&:formula_names)
.each_with_object({}) do |tap_formula_full_name, name_map|
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
2015-09-07 18:32:38 +08:00
@@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 }
2015-08-06 17:12:35 +08:00
end.flat_map(&:value)
end
same_name_tap_formulae.delete(full_name)
2016-09-23 22:02:23 +02:00
return if same_name_tap_formulae.empty?
problem "Formula name conflicts with #{same_name_tap_formulae.join ", "}"
end
2014-10-17 00:11:46 -05:00
def audit_deps
2014-10-17 00:07:35 -05:00
@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
2015-05-17 19:59:18 +08:00
rescue TapFormulaAmbiguityError
problem "Ambiguous dependency #{dep.name.inspect}."
next
rescue TapFormulaWithOldnameAmbiguityError
problem "Ambiguous oldname dependency #{dep.name.inspect}."
next
2014-10-17 00:07:35 -05:00
end
2014-10-17 00:11:46 -05:00
if dep_f.oldname && dep.name.split("/").last == dep_f.oldname
2016-01-06 17:58:16 +01:00
problem "Dependency '#{dep.name}' was renamed; use new name '#{dep_f.name}'."
end
if @@aliases.include?(dep.name) &&
(core_formula? || !dep_f.versioned_formula?)
2015-05-27 20:42:20 +08:00
problem "Dependency '#{dep.name}' is an alias; use the canonical name '#{dep.to_formula.full_name}'."
2014-10-17 00:11:46 -05:00
end
2014-10-17 00:07:35 -05:00
if @new_formula && dep_f.keg_only_reason &&
!["openssl", "apr", "apr-util"].include?(dep.name) &&
[:provided_by_macos, :provided_by_osx].include?(dep_f.keg_only_reason.reason)
problem "Dependency '#{dep.name}' may be unnecessary as it is provided by macOS; try to build this formula without it."
end
2014-10-17 00:07:35 -05:00
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
2014-10-17 00:07:35 -05:00
end.each do |opt|
problem "Dependency #{dep} does not define option #{opt.name.inspect}"
end
2014-10-17 00:07:35 -05:00
case dep.name
when "git"
problem "Don't use git as a dependency"
when "mercurial"
problem "Use `depends_on :hg` instead of `depends_on 'mercurial'`"
when "gfortran"
2014-10-17 00:07:35 -05:00
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"
2014-10-17 00:07:35 -05:00
problem <<-EOS.undent
There are multiple conflicting ways to install MPI. Use an MPIRequirement:
2014-10-17 00:07:35 -05:00
depends_on :mpi => [<lang list>]
Where <lang list> is a comma delimited list that can include:
:cc, :cxx, :f77, :f90
EOS
2016-10-15 18:31:06 +02:00
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
2014-10-17 00:07:35 -05:00
end
2012-08-07 01:37:46 -05:00
end
end
end
2013-01-03 11:22:31 -08:00
def audit_conflicts
formula.conflicts.each do |c|
2013-01-03 11:22:31 -08:00
begin
2014-02-24 20:23:21 -08:00
Formulary.factory(c.name)
rescue TapFormulaUnavailableError
# Don't complain about missing cross-tap conflicts.
next
2013-02-17 22:54:27 -06:00
rescue FormulaUnavailableError
problem "Can't find conflicting formula #{c.name.inspect}."
rescue TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
2015-05-17 19:59:18 +08:00
problem "Ambiguous conflicting formula #{c.name.inspect}."
2013-01-03 11:22:31 -08:00
end
end
versioned_conflicts_whitelist = %w[node@ bash-completion@].freeze
return unless formula.conflicts.any? && formula.versioned_formula?
return if formula.name.start_with?(*versioned_conflicts_whitelist)
problem <<-EOS
Versioned formulae should not use `conflicts_with`.
Use `keg_only :versioned_formula` instead.
EOS
2013-01-03 11:22:31 -08:00
end
def audit_options
formula.options.each do |o|
2016-12-20 14:26:53 +00:00
if o.name == "32-bit"
problem "macOS has been 64-bit only since 10.6 so 32-bit options are deprecated."
end
next unless @strict
if o.name == "universal"
problem "macOS has been 64-bit only since 10.6 so universal options are deprecated."
end
if o.name !~ /with(out)?-/ && o.name != "c++11" && o.name != "universal"
problem "Options should begin with with/without. Migrate '--#{o.name}' with `deprecated_option`."
end
2016-09-11 17:41:51 +01:00
next unless 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
return unless @new_formula
return if formula.deprecated_options.empty?
return if formula.versioned_formula?
problem "New formulae should not use `deprecated_option`."
end
2015-05-19 13:05:22 -04:00
def audit_desc
# For now, only check the description when using `--strict`
return unless @strict
desc = formula.desc
unless desc && !desc.empty?
2015-05-19 13:05:22 -04:00
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
2016-09-21 08:49:04 +02:00
linelength = "#{formula.name}: #{desc}".length
2015-05-19 13:05:22 -04:00
if linelength > 80
2015-07-14 17:15:51 +01:00
problem <<-EOS.undent
Description is too long. \"name: desc\" should be less than 80 characters.
Length is calculated as #{formula.name} + desc. (currently #{linelength})
2015-07-14 17:15:51 +01:00
EOS
2015-05-19 13:05:22 -04:00
end
if desc =~ /([Cc]ommand ?line)/
problem "Description should use \"command-line\" instead of \"#{$1}\""
2015-05-19 13:05:22 -04:00
end
2015-08-22 13:15:33 +08:00
if desc =~ /^([Aa]n?)\s/
problem "Description shouldn't start with an indefinite article (#{$1})"
end
2016-09-23 22:02:23 +02:00
return unless desc.downcase.start_with? "#{formula.name} "
problem "Description shouldn't include the formula name"
2015-05-19 13:05:22 -04:00
end
2015-05-07 23:18:01 -04:00
def audit_homepage
homepage = formula.homepage
if homepage.nil? || homepage.empty?
problem "Formula should have a homepage."
return
end
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.
2015-05-14 00:15:41 -04:00
# 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.
case homepage
when %r{^http://[^/]*\.github\.io/},
%r{^http://[^/]*\.sourceforge\.io/}
problem "Please use https:// for #{homepage}"
end
if homepage =~ %r{^http://([^/]*)\.(sf|sourceforge)\.net(/|$)}
problem "#{homepage} should be `https://#{$1}.sourceforge.io/`"
end
# There's an auto-redirect here, but this mistake is incredibly common too.
2015-05-14 00:15:41 -04:00
# 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/},
2016-07-13 16:19:51 +08:00
%r{^http://((?:www)\.)?gnupg\.org/},
%r{^http://ietf\.org},
%r{^http://[^/.]+\.ietf\.org},
%r{^http://[^/.]+\.tools\.ietf\.org},
%r{^http://www\.gnu\.org/},
2015-08-10 19:11:58 +01:00
%r{^http://code\.google\.com/},
%r{^http://bitbucket\.org/},
%r{^http://(?:[^/]*\.)?archive\.org}
problem "Please use https:// for #{homepage}"
end
return unless @online
return unless DevelopmentTools.curl_handles_most_https_homepages?
if http_content_problem = FormulaAuditor.check_http_content(homepage,
user_agents: [:browser, :default])
problem http_content_problem
end
end
def audit_bottle_spec
2016-09-23 22:02:23 +02:00
return unless formula.bottle_disabled?
return if formula.bottle_disable_reason.valid?
problem "Unrecognized bottle modifier"
end
def audit_github_repository
return unless @online
return unless @new_formula
2016-07-13 16:19:51 +08:00
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
2016-09-23 22:02:23 +02:00
return if Date.parse(metadata["created_at"]) <= (Date.today - 30)
problem "GitHub repository too new (<30 days old)"
end
2012-08-07 01:37:46 -05:00
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
2013-09-18 18:50:23 -05:00
%w[Stable Devel HEAD].each do |name|
next unless spec = formula.send(name.downcase)
2013-09-18 18:50:23 -05:00
ra = ResourceAuditor.new(spec, online: @online, strict: @strict).audit
2013-09-18 18:50:23 -05:00
problems.concat ra.problems.map { |problem| "#{name}: #{problem}" }
2013-09-18 18:22:00 -05:00
spec.resources.each_value do |resource|
ra = ResourceAuditor.new(resource, online: @online, strict: @strict).audit
2013-09-18 18:50:23 -05:00
problems.concat ra.problems.map { |problem|
"#{name} resource #{resource.name.inspect}: #{problem}"
}
2013-09-18 18:22:00 -05:00
end
spec.patches.each { |p| audit_patch(p) if p.external? }
end
2014-09-23 13:04:55 -05:00
%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
if version.to_s.start_with?("HEAD")
problem "#{name}: non-HEAD version name (#{version}) should not begin with HEAD"
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
2014-09-23 13:04:55 -05:00
problem "stable and devel versions are identical"
end
end
unstable_whitelist = %w[
aalib 1.4rc5
angolmois 2.0.0alpha2
automysqlbackup 3.0-rc6
aview 1.3.0rc1
distcc 3.2rc1
elm-format 0.5.2-alpha
ftgl 2.1.3-rc5
hidapi 0.8.0-rc1
libcaca 0.99b19
nethack4 4.3.0-beta2
opensyobon 1.0rc2
premake 4.4-beta5
pwnat 0.3-beta
pxz 4.999.9
recode 3.7-beta2
speexdsp 1.2rc3
sqoop 1.4.6
tcptraceroute 1.5beta7
testssl 2.8rc3
tiny-fugue 5.0b8
vbindiff 3.0_beta4
].each_slice(2).to_a.map do |formula, version|
[formula, version.sub(/\d+$/, "")]
end
gnome_devel_whitelist = %w[
gtk-doc 1.25
libart 2.3.21
pygtkglext 1.1.0
].each_slice(2).to_a.map do |formula, version|
[formula, version.split(".")[0..1].join(".")]
end
stable = formula.stable
case stable && stable.url
when /[\d\._-](alpha|beta|rc\d)/
matched = $1
version_prefix = stable.version.to_s.sub(/\d+$/, "")
return if unstable_whitelist.include?([formula.name, version_prefix])
problem "Stable version URLs should not contain #{matched}"
when %r{download\.gnome\.org/sources}, %r{ftp\.gnome\.org/pub/GNOME/sources}i
version_prefix = stable.version.to_s.split(".")[0..1].join(".")
return if gnome_devel_whitelist.include?([formula.name, version_prefix])
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
return if @new_formula
fv = FormulaVersions.new(formula, max_depth: 1)
attributes = [:revision, :version_scheme]
attributes_map = fv.version_attributes_map(attributes, "origin/master")
attributes.each do |attribute|
stable_attribute_map = attributes_map[attribute][:stable]
next if stable_attribute_map.nil? || stable_attribute_map.empty?
attributes_for_version = stable_attribute_map[formula.version]
next if attributes_for_version.nil? || attributes_for_version.empty?
old_attribute = formula.send(attribute)
max_attribute = attributes_for_version.max
if max_attribute && old_attribute < max_attribute
problem "#{attribute} should not decrease (from #{max_attribute} to #{old_attribute})"
end
end
[:stable, :devel].each do |spec|
spec_version_scheme_map = attributes_map[:version_scheme][spec]
next if spec_version_scheme_map.nil? || spec_version_scheme_map.empty?
max_version_scheme = spec_version_scheme_map.values.flatten.max
max_version = spec_version_scheme_map.select do |_, version_scheme|
version_scheme.first == max_version_scheme
end.keys.max
formula_spec = formula.send(spec)
next if formula_spec.nil?
if max_version && formula_spec.version < max_version
problem "#{spec} version should not decrease (from #{max_version} to #{formula_spec.version})"
end
end
2016-09-22 20:12:28 +02:00
return if formula.revision.zero?
if formula.stable
revision_map = attributes_map[:revision][:stable]
2016-12-18 15:46:02 -08:00
stable_revisions = revision_map[formula.stable.version] if revision_map
if !stable_revisions || stable_revisions.empty?
problem "'revision #{formula.revision}' should be removed"
end
2016-09-22 20:12:28 +02:00
else # head/devel-only formula
problem "'revision #{formula.revision}' should be removed"
end
end
def audit_legacy_patches
2015-07-22 15:27:58 +08:00
return unless formula.respond_to?(:patches)
legacy_patches = Patch.normalize_legacy_patches(formula.patches).grep(LegacyPatch)
2016-09-22 20:12:28 +02:00
return if legacy_patches.empty?
problem "Use the patch DSL instead of defining a 'patches' method"
legacy_patches.each { |p| audit_patch(p) }
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}"
2012-08-07 01:37:46 -05:00
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}"
2010-09-09 14:16:05 -07:00
end
2012-08-07 01:37:46 -05:00
end
2010-09-09 14:16:05 -07:00
2013-07-16 23:15:22 -05:00
def audit_text
2014-02-25 07:36:47 -08:00
if text =~ /system\s+['"]scons/
2014-02-25 20:51:16 -08:00
problem "use \"scons *args\" instead of \"system 'scons', *args\""
2014-02-25 07:36:47 -08:00
end
if text =~ /system\s+['"]xcodebuild/
2016-10-24 17:27:20 +02:00
problem %q(use "xcodebuild *args" instead of "system 'xcodebuild', *args")
end
bin_names = Set.new
bin_names << formula.name
bin_names += formula.aliases
[formula.bin, formula.sbin].each do |dir|
next unless dir.exist?
bin_names += dir.children.map(&:basename).map(&:to_s)
end
bin_names.each do |name|
["system", "shell_output", "pipe_output"].each do |cmd|
if text =~ %r{(def test|test do).*(#{Regexp.escape HOMEBREW_PREFIX}/bin/)?#{cmd}[\(\s]+['"]#{Regexp.escape name}[\s'"]}m
2016-10-24 18:57:57 +02:00
problem %Q(fully scope test #{cmd} calls e.g. #{cmd} "\#{bin}/#{name}")
end
end
end
if text =~ /xcodebuild[ (]*["'*]*/ && !text.include?("SYMROOT=")
2016-10-24 17:27:20 +02:00
problem 'xcodebuild should be passed an explicit "SYMROOT"'
2013-07-16 23:15:22 -05:00
end
2014-02-24 20:23:21 -08:00
if text.include? "Formula.factory("
2014-02-24 20:23:21 -08:00
problem "\"Formula.factory(name)\" is deprecated in favor of \"Formula[name]\""
end
2015-06-09 15:52:26 +01:00
if text.include?("def plist") && !text.include?("plist_options")
problem "Please set plist_options when using a formula-defined plist."
end
if text =~ /depends_on\s+['"]openssl['"]/ && text =~ /depends_on\s+['"]libressl['"]/
problem "Formulae should not depend on both OpenSSL and LibreSSL (even optionally)."
end
if text =~ /virtualenv_(create|install_with_resources)/ &&
text =~ /resource\s+['"]setuptools['"]\s+do/
problem "Formulae using virtualenvs do not need a `setuptools` resource."
end
2016-09-23 22:02:23 +02:00
return unless text.include?('require "language/go"') && !text.include?("go_resource")
problem "require \"language/go\" is unnecessary unless using `go_resource`s"
2013-07-16 23:15:22 -05:00
end
def audit_line(line, _lineno)
2013-07-16 21:25:02 -05:00
if line =~ /<(Formula|AmazonWebServicesFormula|ScriptFileFormula|GithubGistFormula)/
2012-08-07 01:37:46 -05:00
problem "Use a space in class inheritance: class Foo < #{$1}"
2010-09-09 14:16:05 -07:00
end
2012-08-07 01:37:46 -05:00
# Commented-out cmake support from default template
2016-09-25 01:51:37 +02:00
problem "Commented cmake call found" if line.include?('# system "cmake')
2010-09-09 14:16:05 -07:00
2013-07-03 09:20:41 -07:00
# 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|
2016-09-25 01:51:37 +02:00
next unless line.include?(comment)
problem "Please remove default template comments"
2013-07-03 09:20:41 -07:00
end
2012-08-07 01:37:46 -05:00
# FileUtils is included in Formula
2013-12-04 20:07:27 -08:00
# encfs modifies a file with this name, so check for some leading characters
2016-09-11 17:41:51 +01:00
if line =~ %r{[^'"/]FileUtils\.(\w+)}
2012-08-07 01:37:46 -05:00
problem "Don't need 'FileUtils.' before #{$1}."
end
2010-09-09 14:16:05 -07:00
2012-08-07 01:37:46 -05:00
# Check for long inreplace block vars
2013-07-16 21:25:02 -05:00
if line =~ /inreplace .* do \|(.{2,})\|/
2012-08-07 01:37:46 -05:00
problem "\"inreplace <filenames> do |s|\" is preferred over \"|#{$1}|\"."
end
2012-08-07 01:37:46 -05:00
# Check for string interpolation of single values.
2013-07-16 21:25:02 -05:00
if line =~ /(system|inreplace|gsub!|change_make_var!).*[ ,]"#\{([\w.]+)\}"/
2012-08-07 01:37:46 -05:00
problem "Don't need to interpolate \"#{$2}\" with #{$1}"
end
2012-08-07 01:37:46 -05:00
# Check for string concatenation; prefer interpolation
2013-07-16 21:25:02 -05:00
if line =~ /(#\{\w+\s*\+\s*['"][^}]+\})/
2012-08-07 01:37:46 -05:00
problem "Try not to concatenate paths in string interpolation:\n #{$1}"
2012-04-05 21:12:02 -05:00
end
2012-08-07 01:37:46 -05:00
# 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}+...)\""
2012-08-07 01:37:46 -05:00
end
if line =~ /((man)\s*\+\s*(['"])(man[1-8])(['"]))/
2012-08-07 01:37:46 -05:00
problem "\"#{$1}\" should be \"#{$4}\""
end
2012-04-05 21:12:02 -05:00
2012-08-07 01:37:46 -05:00
# Prefer formula path shortcuts in strings
if line =~ %r[(\#\{prefix\}/(bin|include|libexec|lib|sbin|share|Frameworks))]
problem "\"#{$1}\" should be \"\#{#{$2.downcase}}\""
2012-04-05 21:12:02 -05:00
end
2013-07-16 21:25:02 -05:00
if line =~ %r[((\#\{prefix\}/share/man/|\#\{man\}/)(man[1-8]))]
2012-08-07 01:37:46 -05:00
problem "\"#{$1}\" should be \"\#{#{$3}}\""
end
2013-07-16 21:25:02 -05:00
if line =~ %r[((\#\{share\}/(man)))[/'"]]
2012-08-07 01:37:46 -05:00
problem "\"#{$1}\" should be \"\#{#{$3}}\""
end
2013-07-16 21:25:02 -05:00
if line =~ %r[(\#\{prefix\}/share/(info|man))]
2012-08-07 01:37:46 -05:00
problem "\"#{$1}\" should be \"\#{#{$2}}\""
end
if line =~ /depends_on :(automake|autoconf|libtool)/
problem ":#{$1} is deprecated. Usage should be \"#{$1}\""
end
if line =~ /depends_on :apr/
problem ":apr is deprecated. Usage should be \"apr-util\""
end
if line =~ /depends_on :tex/
problem ":tex is deprecated."
end
2012-08-07 01:37:46 -05:00
# Commented-out depends_on
2016-09-25 01:51:37 +02:00
problem "Commented-out dep #{$1}" if line =~ /#\s*depends_on\s+(.+)\s*$/
2013-07-16 21:25:02 -05:00
if line =~ /if\s+ARGV\.include\?\s+'--(HEAD|devel)'/
2014-06-15 23:26:07 -05:00
problem "Use \"if build.#{$1.downcase}?\" instead"
2012-08-07 01:37:46 -05:00
end
2012-03-17 19:49:49 -07:00
2016-09-25 01:51:37 +02:00
problem "Use separate make calls" if line.include?("make && make")
2012-03-17 19:49:49 -07:00
2016-09-25 01:51:37 +02:00
problem "Use spaces instead of tabs for indentation" if line =~ /^[ ]*\t/
if line.include?("ENV.x11")
2012-09-03 19:18:58 -07:00
problem "Use \"depends_on :x11\" instead of \"ENV.x11\""
end
2012-08-07 01:37:46 -05:00
# 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}\""
2012-08-07 01:37:46 -05:00
end
2013-07-16 21:25:02 -05:00
if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?((g|llvm-g|clang)\+\+)['" ]}
2012-08-07 01:37:46 -05:00
problem "Use \"\#{ENV.cxx}\" instead of hard-coding \"#{$3}\""
end
2011-02-20 15:03:15 -08:00
2014-03-05 07:53:53 -08:00
if line =~ /system\s+['"](env|export)(\s+|['"])/
2012-08-07 01:37:46 -05:00
problem "Use ENV instead of invoking '#{$1}' to modify the environment"
end
2013-07-16 21:25:02 -05:00
if line =~ /version == ['"]HEAD['"]/
problem "Use 'build.head?' instead of inspecting 'version'"
end
if line =~ /build\.include\?[\s\(]+['"]\-\-(.*)['"]/
2012-09-13 07:14:45 -07:00
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
2016-09-25 01:51:37 +02:00
problem "Use new-style option definitions" if line.include?("def options")
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("|")
2013-07-16 21:25:02 -05:00
if line =~ /MacOS\.(?:#{cats})\?/
2013-04-06 22:11:26 -05:00
problem "\"#{$&}\" is deprecated, use a comparison to MacOS.version instead"
end
2012-09-13 07:14:45 -07:00
2013-07-16 21:25:02 -05:00
if line =~ /skip_clean\s+:all/
problem "`skip_clean :all` is deprecated; brew no longer strips symbols\n" \
2014-02-23 12:09:28 -08:00
"\tPass explicit paths to prevent Homebrew from removing empty folders."
2012-09-13 07:14:45 -07:00
end
2013-01-27 14:27:32 -08:00
2013-07-16 21:25:02 -05:00
if line =~ /depends_on [A-Z][\w:]+\.new$/
2013-04-06 22:11:26 -05:00
problem "`depends_on` can take requirement classes instead of instances"
2013-01-27 14:27:32 -08:00
end
2013-04-22 15:06:42 -05:00
2013-07-16 21:25:02 -05:00
if line =~ /^def (\w+).*$/
2013-04-22 15:06:42 -05:00
problem "Define method #{$1.inspect} in the class body, not at the top-level"
end
2013-06-23 20:40:00 -07:00
if line.include?("ENV.fortran") && !formula.requirements.map(&:class).include?(FortranRequirement)
2013-06-23 20:40:00 -07:00
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
2013-07-16 21:25:02 -05:00
if line =~ /depends_on :(.+) (if.+|unless.+)$/
audit_conditional_dep($1.to_sym, $2, $&)
end
2013-07-16 21:25:02 -05:00
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
2017-03-26 12:54:25 -04:00
if line.include?('system "npm", "install"') && !line.include?("Language::Node") &&
2017-03-26 14:29:38 -04:00
formula.name !~ /^kibana(\@\d+(\.\d+)?)?$/
problem "Use Language::Node for npm install args"
end
if line.include?("fails_with :llvm")
problem "'fails_with :llvm' is now a no-op so should be removed"
end
if formula.tap.to_s == "homebrew/core"
["OS.mac?", "OS.linux?"].each do |check|
next unless line.include?(check)
problem "Don't use #{check}; Homebrew/core only supports macOS"
end
end
2016-09-22 20:12:28 +02:00
return unless @strict
problem "`#{$1}` in formulae is deprecated" if line =~ /(env :(std|userpaths))/
2016-09-22 20:12:28 +02:00
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
2016-09-22 20:12:28 +02:00
end
2016-09-25 01:51:37 +02:00
problem "`#{$1}` is now unnecessary" if line =~ /(require ["']formula["'])/
2016-09-22 20:12:28 +02:00
if line =~ %r{#\{share\}/#{Regexp.escape(formula.name)}[/'"]}
problem "Use \#{pkgshare} instead of \#{share}/#{formula.name}"
end
2016-09-23 22:02:23 +02:00
return unless line =~ %r{share(\s*[/+]\s*)(['"])#{Regexp.escape(formula.name)}(?:\2|/)}
problem "Use pkgshare instead of (share#{$1}\"#{formula.name}\")"
end
2015-02-19 09:29:17 +00:00
def audit_caveats
2016-09-22 20:12:28 +02:00
return unless formula.caveats.to_s.include?("setuid")
problem "Don't recommend setuid in the caveats, suggest sudo instead."
2015-02-19 09:29:17 +00:00
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?
2016-09-22 20:12:28 +02:00
return unless formula.tap.tap_migrations.key?(formula.name)
2016-09-22 20:12:28 +02:00
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
def audit_prefix_has_contents
return unless formula.prefix.directory?
2016-09-22 20:12:28 +02:00
return unless Keg.new(formula.prefix).empty_installation?
2016-09-22 20:12:28 +02:00
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
def audit_conditional_dep(dep, condition, line)
2013-07-23 11:21:37 -05:00
quoted_dep = quote_dep(dep)
dep = Regexp.escape(dep.to_s)
case condition
when /if build\.include\? ['"]with-#{dep}['"]$/, /if build\.with\? ['"]#{dep}['"]$/
2016-10-24 17:27:20 +02:00
problem %Q(Replace #{line.inspect} with "depends_on #{quoted_dep} => :optional")
when /unless build\.include\? ['"]without-#{dep}['"]$/, /unless build\.without\? ['"]#{dep}['"]$/
2016-10-24 17:27:20 +02:00
problem %Q(Replace #{line.inspect} with "depends_on #{quoted_dep} => :recommended")
end
end
def quote_dep(dep)
2016-09-11 17:41:51 +01:00
dep.is_a?(Symbol) ? dep.inspect : "'#{dep}'"
2012-08-07 01:37:46 -05:00
end
2014-10-13 23:13:00 -05:00
def audit_check_output(output)
problem(output) if output
end
2012-08-07 01:37:46 -05:00
def audit
audit_file
audit_formula_name
2014-12-27 20:46:01 +00:00
audit_class
2012-08-07 01:37:46 -05:00
audit_specs
audit_revision_and_version_scheme
2015-05-19 13:05:22 -04:00
audit_desc
2015-05-07 23:18:01 -04:00
audit_homepage
audit_bottle_spec
audit_github_repository
2012-08-07 01:37:46 -05:00
audit_deps
2013-01-03 11:22:31 -08:00
audit_conflicts
audit_options
audit_legacy_patches
2013-07-16 23:15:22 -05:00
audit_text
2015-02-19 09:29:17 +00:00
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
2012-08-07 01:37:46 -05:00
end
2010-11-09 13:00:33 +00:00
2012-08-07 01:37:46 -05:00
private
def problem(p)
2012-08-07 01:37:46 -05:00
@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
2016-12-23 11:29:31 +00:00
def initialize(resource, options = {})
@name = resource.name
@version = resource.version
@checksum = resource.checksum
@url = resource.url
@mirrors = resource.mirrors
@using = resource.using
@specs = resource.specs
2016-12-23 11:29:31 +00:00
@online = options[:online]
@strict = options[:strict]
@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
2016-09-23 22:02:23 +02:00
return unless version.to_s =~ /_\d+$/
problem "version #{version} should not end with an underline and a number"
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
2015-07-22 16:41:10 +08:00
if using == :ssl3 || \
(Object.const_defined?("CurlSSL3DownloadStrategy") && using == CurlSSL3DownloadStrategy)
2014-10-18 17:39:53 -05:00
problem "The SSL3 download strategy is deprecated, please choose a different URL"
2015-07-22 16:41:10 +08:00
elsif (Object.const_defined?("CurlUnsafeDownloadStrategy") && using == CurlUnsafeDownloadStrategy) || \
(Object.const_defined?("UnsafeSubversionDownloadStrategy") && using == UnsafeSubversionDownloadStrategy)
2014-10-18 17:39:53 -05:00
problem "#{using.name} is deprecated, please choose a different URL"
end
if using == :cvs
mod = specs[:module]
2016-09-25 01:51:37 +02:00
problem "Redundant :module value in URL" if mod == name
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
2016-09-23 22:02:23 +02:00
return unless url_strategy == DownloadStrategyDetector.detect("", using)
problem "Redundant :using value in URL"
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}."
2015-06-14 20:17:08 +01:00
end
if mirrors.include?(url)
problem "URL should not be duplicated as a mirror: #{url}"
end
urls = [url] + mirrors
2015-05-14 00:15:41 -04:00
# 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/},
2015-08-05 19:45:24 +01:00
%r{^http://tools\.ietf\.org/},
2015-08-10 19:11:58 +01:00
%r{^http://launchpad\.net/},
%r{^http://github\.com/},
%r{^http://bitbucket\.org/},
%r{^http://anonscm\.debian\.org/},
%r{^http://cpan\.metacpan\.org/},
%r{^http://hackage\.haskell\.org/},
2016-05-20 10:54:29 +01:00
%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
2016-09-25 01:51:37 +02:00
problem "Please use https:// for #{p}" if p.start_with? "http://downloads"
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
# Check for master branch GitHub archives.
urls.grep(%r{^https://github\.com/.*archive/master\.(tar\.gz|zip)$}) do
problem "Use versioned rather than branch tarballs for stable checksums."
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
2016-12-23 11:29:31 +00:00
return unless @online
urls.each do |url|
2017-02-24 08:45:39 +00:00
next if !@strict && mirrors.include?(url)
strategy = DownloadStrategyDetector.detect(url, using)
2017-01-30 18:30:57 +00:00
if strategy <= CurlDownloadStrategy && !url.start_with?("file")
# A `brew mirror`'ed URL is usually not yet reachable at the time of
# pull request.
next if url =~ %r{^https://dl.bintray.com/homebrew/mirror/}
if http_content_problem = FormulaAuditor.check_http_content(url)
problem http_content_problem
end
elsif strategy <= GitDownloadStrategy
unless Utils.git_remote_exists url
problem "The URL #{url} is not a valid git URL"
end
elsif strategy <= SubversionDownloadStrategy
next unless DevelopmentTools.subversion_handles_most_https_certificates?
unless Utils.svn_remote_exists url
2016-12-11 21:36:58 +00:00
problem "The URL #{url} is not a valid svn URL"
end
end
end
end
def problem(text)
@problems << text
end
end