1069 lines
36 KiB
Ruby
Raw Normal View History

#: * `audit` [`--strict`] [`--fix`] [`--online`] [`--new-formula`] [`--display-cop-names`] [`--display-filename`] [`--only=`<method>|`--except=`<method>] [`--only-cops=`<cops>|`--except-cops=`<cops>] [<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
#: 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.
#:
#: Passing `--only=`<method> will run only the methods named `audit_<method>`,
#: while `--except=`<method> will skip the methods named `audit_<method>`.
#: For either option <method> should be a comma-separated list.
#:
#: Passing `--only-cops=`<cops> will check for violations of only the listed
#: RuboCop <cops>, while `--except-cops=`<cops> will skip checking the listed
#: <cops>. For either option <cops> should be a comma-separated list of cop names.
#:
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/curl"
require "extend/ENV"
require "formula_cellar_checks"
require "cmd/search"
require "style"
2015-07-09 15:28:27 +01:00
require "date"
require "missing_formula"
require "digest"
require "cli_parser"
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
Homebrew::CLI::Parser.parse do
switch "--strict"
switch "--online"
switch "--new-formula"
switch "--fix"
switch "--display-cop-names"
switch "--display-filename"
switch "-D", "--audit-debug", description: "Activates debugging and profiling"
switch :verbose
switch :debug
comma_array "--only"
comma_array "--except"
comma_array "--only-cops"
comma_array "--except-cops"
end
Homebrew.auditing = true
inject_dump_stats!(FormulaAuditor, /^audit_/) if args.audit_debug?
2012-08-07 01:37:46 -05:00
formula_count = 0
problem_count = 0
new_formula_problem_count = 0
new_formula = args.new_formula?
strict = new_formula || args.strict?
online = new_formula || args.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
only_cops = args.only_cops
except_cops = args.except_cops
if only_cops && except_cops
odie "--only-cops and --except-cops cannot be used simultaneously!"
elsif (only_cops || except_cops) && (strict || args.only)
odie "--only-cops/--except-cops and --strict/--only cannot be used simultaneously"
end
options = { fix: args.fix?, realpath: true }
if only_cops
options[:only_cops] = only_cops
args.only = ["style"]
elsif args.new_formula?
nil
elsif strict
options[:except_cops] = [:NewFormulaAudit]
elsif except_cops
options[:except_cops] = except_cops
elsif !strict
options[:only_cops] = [:FormulaAudit]
2012-08-07 01:37:46 -05:00
end
2014-12-27 12:38:04 +00:00
options[:display_cop_names] = args.display_cop_names?
# Check style in a single batch run up front for performance
style_results = Style.check_style_json(files, options)
2017-03-29 02:07:53 +05:30
new_formula_problem_lines = []
ff.sort.each do |f|
options = { new_formula: new_formula, strict: strict, online: online, only: args.only, except: args.except }
2017-03-29 02:07:53 +05:30
options[:style_offenses] = style_results.file_offenses(f.path)
fa = FormulaAuditor.new(f, options)
2012-08-07 01:37:46 -05:00
fa.audit
next if fa.problems.empty? && fa.new_formula_problems.empty?
fa.problems
formula_count += 1
problem_count += fa.problems.size
problem_lines = format_problem_lines(fa.problems)
new_formula_problem_lines = format_problem_lines(fa.new_formula_problems)
if args.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
created_pr_comment = false
if new_formula && !new_formula_problem_lines.empty?
begin
if GitHub.create_issue_comment(new_formula_problem_lines.join("\n"))
created_pr_comment = true
end
rescue *GitHub.api_errors
nil
end
end
unless created_pr_comment
new_formula_problem_count += new_formula_problem_lines.size
puts new_formula_problem_lines.map { |s| " #{s}" }
end
total_problems_count = problem_count + new_formula_problem_count
2018-05-22 14:52:02 +01:00
problem_plural = Formatter.pluralize(total_problems_count, "problem")
formula_plural = Formatter.pluralize(formula_count, "formula")
errors_summary = "#{problem_plural} in #{formula_plural}"
2018-05-22 14:52:02 +01:00
if problem_count.positive? ||
(new_formula_problem_count.positive? && !created_pr_comment)
ofail errors_summary
end
end
2016-09-22 20:12:28 +02:00
def format_problem_lines(problems)
problems.map { |p| "* #{p.chomp.gsub("\n", "\n ")}" }
2012-08-07 01:37:46 -05:00
end
2012-03-17 19:49:49 -07:00
2018-04-22 17:27:44 +02:00
class FormulaText
def initialize(path)
@text = path.open("rb", &:read)
@lines = @text.lines.to_a
end
2018-04-22 17:27:44 +02:00
def without_patch
@text.split("\n__END__").first
end
2018-04-22 17:27:44 +02:00
def data?
/^[^#]*\bDATA\b/ =~ @text
end
2010-08-15 15:19:19 -07:00
2018-04-22 17:27:44 +02:00
def end?
/^__END__$/ =~ @text
end
2018-04-22 17:27:44 +02:00
def trailing_newline?
/\Z\n/ =~ @text
end
2018-04-22 17:27:44 +02:00
def =~(other)
other =~ @text
end
2018-04-22 17:27:44 +02:00
def include?(s)
@text.include? s
end
2018-04-22 17:27:44 +02:00
def line_number(regex, skip = 0)
index = @lines.drop(skip).index { |line| line =~ regex }
index ? index + 1 : nil
end
2018-04-22 17:27:44 +02:00
def reverse_line_number(regex)
index = @lines.reverse.index { |line| line =~ regex }
index ? @lines.count - index : nil
end
end
2018-04-22 17:27:44 +02:00
class FormulaAuditor
include FormulaCellarChecks
attr_reader :formula, :text, :problems, :new_formula_problems
2012-08-07 01:37:46 -05:00
2018-04-22 17:27:44 +02:00
def initialize(formula, options = {})
@formula = formula
@new_formula = options[:new_formula] && !formula.versioned_formula?
2018-04-22 17:27:44 +02:00
@strict = options[:strict]
@online = options[:online]
@display_cop_names = options[:display_cop_names]
@only = options[:only]
@except = options[:except]
# Accept precomputed style offense results, for efficiency
@style_offenses = options[:style_offenses]
# Allow the actual official-ness of a formula to be overridden, for testing purposes
@official_tap = formula.tap&.official? || options[:official_tap]
@problems = []
@new_formula_problems = []
2018-04-22 17:27:44 +02:00
@text = FormulaText.new(formula.path)
@specs = %w[stable devel head].map { |s| formula.send(s) }.compact
end
2018-04-22 17:27:44 +02:00
def audit_style
return unless @style_offenses
@style_offenses.each do |offense|
if offense.cop_name.start_with?("NewFormulaAudit")
next if formula.versioned_formula?
new_formula_problem offense.to_s(display_cop_name: @display_cop_names)
next
end
2018-04-22 17:27:44 +02:00
problem offense.to_s(display_cop_name: @display_cop_names)
end
end
2018-04-22 17:27:44 +02:00
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
2018-04-22 17:27:44 +02:00
problem "'DATA' was found, but no '__END__'" if text.data? && !text.end?
2018-04-22 17:27:44 +02:00
if text.end? && !text.data?
problem "'__END__' was found, but 'DATA' is not used"
end
2018-04-22 17:27:44 +02:00
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
problem "File should end with a newline" unless text.trailing_newline?
2018-04-22 17:27:44 +02:00
if formula.versioned_formula?
unversioned_formula = begin
# build this ourselves as we want e.g. homebrew/core to be present
full_name = if formula.tap
"#{formula.tap}/#{formula.name}"
else
formula.name
end
Formulary.factory(full_name.gsub(/@.*$/, "")).path
rescue FormulaUnavailableError, TapFormulaAmbiguityError,
TapFormulaWithOldnameAmbiguityError
Pathname.new formula.path.to_s.gsub(/@.*\.rb$/, ".rb")
end
unless unversioned_formula.exist?
unversioned_name = unversioned_formula.basename(".rb")
problem "#{formula} is versioned but no #{unversioned_name} formula exists"
end
elsif ARGV.build_stable? && formula.stable? &&
!(versioned_formulae = formula.versioned_formulae).empty?
2018-04-22 17:27:44 +02:00
versioned_aliases = formula.aliases.grep(/.@\d/)
_, last_alias_version = versioned_formulae.map(&:name).last.split("@")
2018-04-22 17:27:44 +02:00
major, minor, = formula.version.to_s.split(".")
alias_name_major = "#{formula.name}@#{major}"
alias_name_major_minor = "#{alias_name_major}.#{minor}"
alias_name = if last_alias_version.split(".").length == 1
alias_name_major
else
2018-04-22 17:27:44 +02:00
alias_name_major_minor
end
2018-04-22 17:27:44 +02:00
valid_alias_names = [alias_name_major, alias_name_major_minor]
2018-04-22 17:27:44 +02:00
unless formula.tap&.core_tap?
versioned_aliases.map! { |a| "#{formula.tap}/#{a}" }
valid_alias_names.map! { |a| "#{formula.tap}/#{a}" }
end
2018-04-22 17:27:44 +02:00
valid_versioned_aliases = versioned_aliases & valid_alias_names
invalid_versioned_aliases = versioned_aliases - valid_alias_names
if valid_versioned_aliases.empty?
if formula.tap
problem <<~EOS
Formula has other versions so create a versioned alias:
cd #{formula.tap.alias_dir}
ln -s #{formula.path.to_s.gsub(formula.tap.path, "..")} #{alias_name}
EOS
else
problem "Formula has other versions so create an alias named #{alias_name}."
end
end
2018-04-22 17:27:44 +02:00
unless invalid_versioned_aliases.empty?
2017-10-15 02:28:32 +02:00
problem <<~EOS
2018-04-22 17:27:44 +02:00
Formula has invalid versioned aliases:
#{invalid_versioned_aliases.join("\n ")}
EOS
end
end
end
2018-04-22 17:27:44 +02:00
def self.aliases
# core aliases + tap alias names + tap alias full name
@aliases ||= Formula.aliases + Formula.tap_aliases
end
2018-04-22 17:27:44 +02:00
def audit_formula_name
return unless @strict
# skip for non-official taps
return unless @official_tap
2018-04-22 17:27:44 +02:00
name = formula.name
2018-04-22 17:27:44 +02:00
if MissingFormula.blacklisted_reason(name)
problem "'#{name}' is blacklisted."
end
2018-04-22 17:27:44 +02:00
if Formula.aliases.include? name
problem "Formula name conflicts with existing aliases."
return
end
2018-04-22 17:27:44 +02:00
if oldname = CoreTap.instance.formula_renames[name]
problem "'#{name}' is reserved as the old name of #{oldname}"
return
end
2014-10-17 00:11:46 -05:00
2018-04-22 17:27:44 +02:00
return if formula.core_formula?
return unless Formula.core_names.include?(name)
problem "Formula name conflicts with existing core formula."
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.
options_message = "Formulae should not have optional or recommended dependencies"
2018-04-22 17:27:44 +02:00
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
2018-04-22 17:27:44 +02:00
if dep_f.oldname && dep.name.split("/").last == dep_f.oldname
problem "Dependency '#{dep.name}' was renamed; use new name '#{dep_f.name}'."
end
2014-10-17 00:07:35 -05:00
2018-04-22 17:27:44 +02:00
if self.class.aliases.include?(dep.name) &&
(dep_f.core_formula? || !dep_f.versioned_formula?)
problem "Dependency '#{dep.name}' is an alias; use the canonical name '#{dep.to_formula.full_name}'."
end
2018-04-22 17:27:44 +02: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)
new_formula_problem "Dependency '#{dep.name}' may be unnecessary as it is provided by macOS; try to build this formula without it."
2018-04-22 17:27:44 +02:00
end
dep.options.each do |opt|
next if dep_f.option_defined?(opt)
next if dep_f.requirements.detect do |r|
if r.recommended?
opt.name == "with-#{r.name}"
elsif r.optional?
opt.name == "without-#{r.name}"
end
2014-10-17 00:07:35 -05:00
end
2018-04-22 17:27:44 +02:00
problem "Dependency #{dep} does not define option #{opt.name.inspect}"
end
2018-04-22 17:27:44 +02:00
if dep.name == "git"
problem "Don't use git as a dependency (it's always available)"
end
2018-04-22 17:27:44 +02:00
if dep.tags.include?(:run)
problem "Dependency '#{dep.name}' is marked as :run. Remove :run; it is a no-op."
end
next unless @new_formula
next unless @official_tap
if dep.tags.include?(:recommended) || dep.tags.include?(:optional)
new_formula_problem options_message
end
end
next unless @new_formula
next unless @official_tap
if spec.requirements.map(&:recommended?).any? || spec.requirements.map(&:optional?).any?
new_formula_problem options_message
end
2018-04-22 17:27:44 +02:00
end
end
2018-04-22 17:27:44 +02:00
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}."
2014-10-17 00:07:35 -05:00
end
2012-08-07 01:37:46 -05:00
end
end
2018-04-22 17:27:44 +02:00
def audit_keg_only_style
return unless @strict
return unless formula.keg_only?
whitelist = %w[
Apple
macOS
OS
Homebrew
Xcode
GPG
GNOME
BSD
Firefox
].freeze
reason = formula.keg_only_reason.to_s
# Formulae names can legitimately be uppercase/lowercase/both.
name = Regexp.new(formula.name, Regexp::IGNORECASE)
reason.sub!(name, "")
first_word = reason.split[0]
if reason =~ /\A[A-Z]/ && !reason.start_with?(*whitelist)
problem <<~EOS
'#{first_word}' from the keg_only reason should be '#{first_word.downcase}'.
EOS
2013-01-03 11:22:31 -08:00
end
2018-04-22 17:27:44 +02:00
return unless reason.end_with?(".")
problem "keg_only reason should not end with a period."
2017-04-28 04:53:52 +01:00
end
2018-04-22 17:27:44 +02:00
def audit_homepage
homepage = formula.homepage
2017-04-28 04:53:52 +01:00
2018-04-22 17:27:44 +02:00
return if homepage.nil? || homepage.empty?
2018-04-22 17:27:44 +02:00
return unless @online
2018-04-22 17:27:44 +02:00
return unless DevelopmentTools.curl_handles_most_https_certificates?
if http_content_problem = curl_check_http_content(homepage,
user_agents: [:browser, :default],
check_content: true,
strict: @strict)
problem http_content_problem
end
end
2018-07-04 13:19:16 +10:00
def audit_bottle_disabled
2018-04-22 17:27:44 +02:00
return unless formula.bottle_disabled?
2018-07-04 13:19:16 +10:00
return if formula.bottle_unneeded?
if !formula.bottle_disable_reason.valid?
problem "Unrecognized bottle modifier"
else
bottle_disabled_whitelist = %w[
cryptopp
leafnode
]
return if bottle_disabled_whitelist.include?(formula.name)
problem "Formulae should not use `bottle :disabled`" if @official_tap
end
end
def audit_bottle_spec
return unless @official_tap
return if @new_formula
return unless @online
return if formula.bottle_defined? || formula.bottle_disabled?
return if formula.name == "testbottest"
problem "`bottle` is not defined"
end
2018-04-22 17:27:44 +02:00
def audit_github_repository
return unless @online
return unless @new_formula
2018-04-22 17:27:44 +02: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
2018-04-22 17:27:44 +02:00
repo.gsub!(/.git$/, "")
2018-04-22 17:27:44 +02:00
begin
metadata = GitHub.repository(user, repo)
rescue GitHub::HTTPNotFoundError
return
end
2018-04-22 17:27:44 +02:00
return if metadata.nil?
new_formula_problem "GitHub fork (not canonical repository)" if metadata["fork"]
2018-04-22 17:27:44 +02:00
if formula&.tap&.core_tap? &&
2018-06-01 08:30:03 +10:00
(metadata["forks_count"] < 30) && (metadata["subscribers_count"] < 30) &&
(metadata["stargazers_count"] < 75)
new_formula_problem "GitHub repository not notable enough (<30 forks, <30 watchers and <75 stars)"
2018-04-22 17:27:44 +02:00
end
2018-04-22 17:27:44 +02:00
return if Date.parse(metadata["created_at"]) <= (Date.today - 30)
new_formula_problem "GitHub repository too new (<30 days old)"
end
2018-04-22 17:27:44 +02:00
def audit_specs
if head_only?(formula) && formula.tap.to_s.downcase !~ %r{[-/]head-only$}
problem "Head-only (no stable download)"
end
2018-04-22 17:27:44 +02:00
if devel_only?(formula) && formula.tap.to_s.downcase !~ %r{[-/]devel-only$}
problem "Devel-only (no stable download)"
end
2018-04-22 17:27:44 +02:00
%w[Stable Devel HEAD].each do |name|
spec_name = name.downcase.to_sym
next unless spec = formula.send(spec_name)
2018-04-22 17:27:44 +02:00
ra = ResourceAuditor.new(spec, spec_name, online: @online, strict: @strict).audit
problems.concat ra.problems.map { |problem| "#{name}: #{problem}" }
2018-04-22 17:27:44 +02:00
spec.resources.each_value do |resource|
ra = ResourceAuditor.new(resource, spec_name, online: @online, strict: @strict).audit
problems.concat ra.problems.map { |problem|
"#{name} resource #{resource.name.inspect}: #{problem}"
}
end
2018-04-22 17:27:44 +02:00
next if spec.patches.empty?
next unless @new_formula
new_formula_problem "Formulae should not require patches to build. Patches should be submitted and accepted upstream first."
2018-04-22 17:27:44 +02:00
end
2018-04-22 17:27:44 +02: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
2018-04-22 17:27:44 +02:00
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
if formula.head || formula.devel
unstable_spec_message = "Formulae should not have an unstable spec"
if @new_formula
new_formula_problem unstable_spec_message
elsif formula.versioned_formula?
versioned_unstable_spec = %w[
bash-completion@2
imagemagick@6
openssl@1.1
python@2
]
problem unstable_spec_message unless versioned_unstable_spec.include?(formula.name)
end
end
2018-06-08 16:53:49 +10:00
throttled = %w[
2018-06-07 16:34:39 +10:00
aws-sdk-cpp 10
awscli 10
heroku 10
quicktype 10
vim 50
]
2018-06-08 16:53:49 +10:00
throttled.each_slice(2).to_a.map do |a, b|
next if formula.stable.nil?
2018-06-07 16:34:39 +10:00
version = formula.stable.version.to_s.split(".").last.to_i
if @strict && a == formula.name && version.modulo(b.to_i).nonzero?
2018-06-07 16:34:39 +10:00
problem "should only be updated every #{b} releases on multiples of #{b}"
end
end
2018-04-22 17:27:44 +02:00
unstable_whitelist = %w[
aalib 1.4rc5
angolmois 2.0.0alpha2
automysqlbackup 3.0-rc6
aview 1.3.0rc1
distcc 3.2rc1
elm-format 0.6.0-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
2018-04-22 17:27:44 +02:00
gnome_devel_whitelist = %w[
gtk-doc 1.25
libart 2.3.21
pygtkglext 1.1.0
libepoxy 1.5.0
gtk-mac-integration 2.1.2
2018-04-22 17:27:44 +02:00
].each_slice(2).to_a.map do |formula, version|
[formula, version.split(".")[0..1].join(".")]
end
2018-04-22 17:27:44 +02:00
stable = formula.stable
case stable&.url
when /[\d\._-](alpha|beta|rc\d)/
matched = Regexp.last_match(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
2016-09-22 20:12:28 +02:00
2018-04-22 17:27:44 +02:00
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)
2018-04-22 17:27:44 +02:00
previous_version_and_checksum = fv.previous_version_and_checksum("origin/master")
[:stable, :devel].each do |spec_sym|
next unless spec = formula.send(spec_sym)
next unless previous_version_and_checksum[spec_sym][:version] == spec.version
next if previous_version_and_checksum[spec_sym][:checksum] == spec.checksum
problem "#{spec_sym}: sha256 changed without the version also changing; please create an issue upstream to rule out malicious circumstances and to find out why the file changed."
end
2018-04-22 17:27:44 +02:00
attributes = [:revision, :version_scheme]
attributes_map = fv.version_attributes_map(attributes, "origin/master")
current_version_scheme = formula.version_scheme
[:stable, :devel].each do |spec|
spec_version_scheme_map = attributes_map[:version_scheme][spec]
next if spec_version_scheme_map.empty?
version_schemes = spec_version_scheme_map.values.flatten
max_version_scheme = version_schemes.max
max_version = spec_version_scheme_map.select do |_, version_scheme|
version_scheme.first == max_version_scheme
end.keys.max
if max_version_scheme && current_version_scheme < max_version_scheme
problem "version_scheme should not decrease (from #{max_version_scheme} to #{current_version_scheme})"
end
if max_version_scheme && current_version_scheme >= max_version_scheme &&
current_version_scheme > 1 &&
!version_schemes.include?(current_version_scheme - 1)
problem "version_schemes should only increment by 1"
end
formula_spec = formula.send(spec)
next unless formula_spec
spec_version = formula_spec.version
next unless max_version
next if spec_version >= max_version
above_max_version_scheme = current_version_scheme > max_version_scheme
2018-06-06 23:34:19 -04:00
map_includes_version = spec_version_scheme_map.key?(spec_version)
2018-04-22 17:27:44 +02:00
next if !current_version_scheme.zero? &&
(above_max_version_scheme || map_includes_version)
problem "#{spec} version should not decrease (from #{max_version} to #{spec_version})"
end
2018-04-22 17:27:44 +02:00
current_revision = formula.revision
revision_map = attributes_map[:revision][:stable]
if formula.stable && !revision_map.empty?
stable_revisions = revision_map[formula.stable.version]
stable_revisions ||= []
max_revision = stable_revisions.max || 0
if current_revision < max_revision
problem "revision should not decrease (from #{max_revision} to #{current_revision})"
end
2018-04-22 17:27:44 +02:00
stable_revisions -= [formula.revision]
if !current_revision.zero? && stable_revisions.empty? &&
revision_map.keys.length > 1
problem "'revision #{formula.revision}' should be removed"
elsif current_revision > 1 &&
current_revision != max_revision &&
!stable_revisions.include?(current_revision - 1)
problem "revisions should only increment by 1"
end
elsif !current_revision.zero? # head/devel-only formula
problem "'revision #{current_revision}' should be removed"
end
end
2013-07-16 23:15:22 -05:00
2018-04-22 17:27:44 +02:00
def audit_text
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
problem %Q(fully scope test #{cmd} calls e.g. #{cmd} "\#{bin}/#{name}")
end
end
end
end
2018-04-22 17:27:44 +02:00
def audit_lines
text.without_patch.split("\n").each_with_index do |line, lineno|
line_problems(line, lineno + 1)
end
2012-08-07 01:37:46 -05:00
end
2018-04-22 17:27:44 +02:00
def line_problems(line, _lineno)
# Check for string interpolation of single values.
if line =~ /(system|inreplace|gsub!|change_make_var!).*[ ,]"#\{([\w.]+)\}"/
problem "Don't need to interpolate \"#{Regexp.last_match(2)}\" with #{Regexp.last_match(1)}"
end
2018-04-22 17:27:44 +02:00
# Check for string concatenation; prefer interpolation
if line =~ /(#\{\w+\s*\+\s*['"][^}]+\})/
problem "Try not to concatenate paths in string interpolation:\n #{Regexp.last_match(1)}"
end
2018-04-22 17:27:44 +02:00
# Prefer formula path shortcuts in Pathname+
if line =~ %r{\(\s*(prefix\s*\+\s*(['"])(bin|include|libexec|lib|sbin|share|Frameworks)[/'"])}
problem "\"(#{Regexp.last_match(1)}...#{Regexp.last_match(2)})\" should be \"(#{Regexp.last_match(3).downcase}+...)\""
end
2012-03-17 19:49:49 -07:00
2018-04-22 17:27:44 +02:00
problem "Use separate make calls" if line.include?("make && make")
2018-04-22 17:27:44 +02:00
if line =~ /JAVA_HOME/i && !formula.requirements.map(&:class).include?(JavaRequirement)
problem "Use `depends_on :java` to set JAVA_HOME"
end
2018-04-22 17:27:44 +02:00
return unless @strict
2018-04-22 17:27:44 +02:00
if @official_tap && line.include?("env :std")
problem "`env :std` in official tap formulae is deprecated"
end
2018-04-22 17:27:44 +02:00
if line.include?("env :userpaths")
problem "`env :userpaths` in formulae is deprecated"
end
2018-04-22 17:27:44 +02:00
if line =~ /system ((["'])[^"' ]*(?:\s[^"' ]*)+\2)/
bad_system = Regexp.last_match(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
2018-04-22 17:27:44 +02:00
problem "`#{Regexp.last_match(1)}` is now unnecessary" if line =~ /(require ["']formula["'])/
if line =~ %r{#\{share\}/#{Regexp.escape(formula.name)}[/'"]}
problem "Use \#{pkgshare} instead of \#{share}/#{formula.name}"
end
if line =~ /depends_on .+ if build\.with(out)?\?\(?["']\w+["']\)?/
problem "`Use :optional` or `:recommended` instead of `#{Regexp.last_match(0)}`"
end
2016-09-22 20:12:28 +02:00
2018-04-22 17:27:44 +02:00
return unless line =~ %r{share(\s*[/+]\s*)(['"])#{Regexp.escape(formula.name)}(?:\2|/)}
problem "Use pkgshare instead of (share#{Regexp.last_match(1)}\"#{formula.name}\")"
end
2018-04-22 17:27:44 +02:00
def audit_reverse_migration
# Only enforce for new formula being re-added to core and official taps
return unless @strict
return unless @official_tap
return unless formula.tap.tap_migrations.key?(formula.name)
2018-04-22 17:27:44 +02:00
problem <<~EOS
#{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
2018-04-22 17:27:44 +02:00
def audit_prefix_has_contents
return unless formula.prefix.directory?
return unless Keg.new(formula.prefix).empty_installation?
2018-04-22 17:27:44 +02:00
problem <<~EOS
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
2018-04-22 17:27:44 +02:00
def audit_url_is_not_binary
return unless @official_tap
2018-04-22 17:27:44 +02:00
urls = @specs.map(&:url)
2018-04-22 17:27:44 +02:00
urls.each do |url|
if url =~ /darwin/i && (url =~ /x86_64/i || url =~ /amd64/i)
problem "#{url} looks like a binary package, not a source archive. Official taps are source-only."
end
end
end
2018-04-22 17:27:44 +02:00
def quote_dep(dep)
dep.is_a?(Symbol) ? dep.inspect : "'#{dep}'"
end
2018-04-22 17:27:44 +02:00
def problem_if_output(output)
problem(output) if output
end
2018-04-22 17:27:44 +02:00
def audit
only_audits = @only
except_audits = @except
if only_audits && except_audits
odie "--only and --except cannot be used simultaneously!"
end
2010-11-09 13:00:33 +00:00
2018-04-22 17:27:44 +02:00
methods.map(&:to_s).grep(/^audit_/).each do |audit_method_name|
name = audit_method_name.gsub(/^audit_/, "")
if only_audits
next unless only_audits.include?(name)
elsif except_audits
next if except_audits.include?(name)
end
send(audit_method_name)
end
end
2018-04-22 17:27:44 +02:00
private
2018-04-22 17:27:44 +02:00
def problem(p)
@problems << p
end
def new_formula_problem(p)
@new_formula_problems << p
2018-04-22 17:27:44 +02:00
end
2018-04-22 17:27:44 +02:00
def head_only?(formula)
formula.head && formula.devel.nil? && formula.stable.nil?
end
2018-04-22 17:27:44 +02:00
def devel_only?(formula)
formula.devel && formula.stable.nil?
end
end
2018-04-22 17:27:44 +02:00
class ResourceAuditor
attr_reader :name, :version, :checksum, :url, :mirrors, :using, :specs, :owner
attr_reader :spec_name, :problems
2018-04-22 17:27:44 +02:00
def initialize(resource, spec_name, options = {})
@name = resource.name
@version = resource.version
@checksum = resource.checksum
@url = resource.url
@mirrors = resource.mirrors
@using = resource.using
@specs = resource.specs
@owner = resource.owner
@spec_name = spec_name
@online = options[:online]
@strict = options[:strict]
@problems = []
end
2018-04-22 17:27:44 +02:00
def audit
audit_version
audit_download_strategy
audit_urls
self
end
2018-04-22 17:27:44 +02:00
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
2018-04-22 17:27:44 +02:00
return unless version.to_s =~ /_\d+$/
problem "version #{version} should not end with an underline and a number"
end
2018-04-22 17:27:44 +02:00
def audit_download_strategy
if url =~ %r{^(cvs|bzr|hg|fossil)://} || url =~ %r{^(svn)\+http://}
problem "Use of the #{$&} scheme is deprecated, pass `:using => :#{Regexp.last_match(1)}` instead"
end
2018-04-22 17:27:44 +02:00
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
2018-04-22 17:27:44 +02:00
return unless using
2018-04-22 17:27:44 +02:00
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
2014-10-18 17:39:53 -05:00
2018-04-22 17:27:44 +02:00
if using == :cvs
mod = specs[:module]
2018-04-22 17:27:44 +02:00
problem "Redundant :module value in URL" if mod == name
2018-04-22 17:27:44 +02:00
if url =~ %r{:[^/]+$}
mod = url.split(":").last
2018-04-22 17:27:44 +02:00
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
2018-04-22 17:27:44 +02:00
return unless url_strategy == DownloadStrategyDetector.detect("", using)
problem "Redundant :using value in URL"
end
2018-04-22 17:27:44 +02:00
def self.curl_openssl_and_deps
@curl_openssl_and_deps ||= begin
formulae_names = ["curl", "openssl"]
formulae_names += formulae_names.flat_map do |f|
Formula[f].recursive_dependencies.map(&:name)
end
formulae_names.uniq
rescue FormulaUnavailableError
[]
end
end
2018-04-22 17:27:44 +02:00
def audit_urls
urls = [url] + mirrors
2016-12-23 11:29:31 +00:00
2018-04-22 17:27:44 +02:00
curl_openssl_or_deps = ResourceAuditor.curl_openssl_and_deps.include?(owner.name)
2018-04-22 17:27:44 +02:00
if spec_name == :stable && curl_openssl_or_deps
problem "should not use xz tarballs" if url.end_with?(".xz")
2018-04-22 17:27:44 +02:00
unless urls.find { |u| u.start_with?("http://") }
problem "should always include at least one HTTP mirror"
end
end
2017-02-24 08:45:39 +00:00
2018-04-22 17:27:44 +02:00
return unless @online
urls.each do |url|
next if !@strict && mirrors.include?(url)
strategy = DownloadStrategyDetector.detect(url, using)
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 = curl_check_http_content(url, require_http: curl_openssl_or_deps)
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?
next unless Utils.svn_available?
unless Utils.svn_remote_exists? url
problem "The URL #{url} is not a valid svn URL"
end
end
end
end
2018-04-22 17:27:44 +02:00
def problem(text)
@problems << text
end
end
end