Newer
Older
require 'formula'
require 'utils'
strict = ARGV.include? "--strict"
if strict && ARGV.formulae.any? && MacOS.version >= :mavericks
require "cmd/style"
ohai "brew style #{ARGV.formulae.join " "}"
style
end
ENV.setup_build_environment
fa = FormulaAuditor.new(f, :strict => strict)
unless output_header
puts
ohai "audit problems"
output_header = true
end
formula_count += 1
problem_count += fa.problems.size
puts "#{f.name}:", fa.problems.map { |p| " * #{p}" }, ""
end
end
unless problem_count.zero?
ofail "#{problem_count} problems in #{formula_count} formulae"
end
end
end
attr_reader :formula, :text, :problems
BUILD_TIME_DEPS = %W[
autoconf
automake
boost-build
bsdmake
cmake
imake
FILEUTILS_METHODS = FileUtils.singleton_methods(false).join "|"
@text = FormulaText.new(formula.path)
@specs = %w{stable devel head}.map { |s| formula.send(s) }.compact
problem "Incorrect file permissions: chmod 644 #{formula.path}"
if text.has_DATA? and not text.has_END?
if text.has_END? and not text.has_DATA?
problem "'__END__' was found, but 'DATA' is not used"
end
problem "A `test do` test block should be added"
end
end
if formula.class < GithubGistFormula
problem "GithubGistFormula is deprecated, use Formula instead"
end
@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
end
if @@aliases.include?(dep.name)
problem "Dependency '#{dep.name}' is an alias; use the canonical name '#{dep.to_formula.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.each do |opt|
problem "Dependency #{dep} does not define option #{opt.name.inspect}"
case dep.name
when *BUILD_TIME_DEPS
next if dep.build? or dep.run?
problem <<-EOS.undent
#{dep} dependency should be
depends_on "#{dep}" => :build
Or if it is indeed a runtime denpendency
depends_on "#{dep}" => :run
EOS
when "git"
problem "Use `depends_on :git` instead of `depends_on 'git'`"
when "mercurial"
problem "Use `depends_on :hg` instead of `depends_on 'mercurial'`"
when "ruby"
problem "Don't use ruby as a dependency. We allow non-Homebrew ruby installations."
when 'gfortran'
problem "Use `depends_on :fortran` instead of `depends_on 'gfortran'`"
when 'open-mpi', 'mpich2'
problem <<-EOS.undent
There are multiple conflicting ways to install MPI. Use an MPIDependency:
depends_on :mpi => [<lang list>]
Where <lang list> is a comma delimited list that can include:
:cc, :cxx, :f77, :f90
EOS
end
problem "Can't find conflicting formula #{c.name.inspect}."
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
end
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 =~ %r[^http://github\.com/]
problem "Use https:// URLs for homepages on GitHub (URL is #{homepage})."
if homepage =~ %r[^https?://code\.google\.com/p/[^/]+[^/]$]
problem "Google Code homepage should end with a slash (URL is #{homepage})."
# Automatic redirect exists, but this is another hugely common error.
if homepage =~ %r[^http://code\.google\.com/]
problem "Google Code homepages should be https:// links (URL is #{homepage})."
end
# GNU has full SSL/TLS support but no auto-redirect.
if homepage =~ %r[^http://www\.gnu\.org/]
problem "GNU homepages should be https:// links (URL is #{homepage})."
end
# Savannah has full SSL/TLS support but no auto-redirect.
# Doesn't apply to the download links (boo), only the homepage.
if homepage =~ %r[^http://savannah\.nongnu\.org/]
problem "Savannah homepages should be https:// links (URL is #{homepage})."
end
if homepage =~ %r[^http://((?:trac|tools|www)\.)?ietf\.org]
problem "ietf homepages should be https:// links (URL is #{homepage})."
end
if homepage =~ %r[^http://((?:www)\.)?gnupg.org/]
problem "GnuPG homepages should be https:// links (URL is #{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 "The url should be styled `https://wiki.freedesktop.org/www/Software/project_name`, not #{homepage})."
else
problem "The url should be styled `https://wiki.freedesktop.org/project_name`, not #{homepage})."
end
end
if homepage =~ %r[^http://wiki\.freedesktop\.org/]
problem "Freedesktop's Wiki subdomain should be https:// (URL is #{homepage})."
end
# There's an auto-redirect here, but this mistake is incredibly common too.
if homepage =~ %r[^http://packages\.debian\.org]
problem "Debian homepage should be https:// links (URL is #{homepage})."
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 "Github Pages links should be https:// (URL is #{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 links.
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 "Gnome homepages should be https:// links (URL is #{homepage})."
end
urls.grep(%r[^(?:https?|ftp)://(?!alpha).+/gnu/]) do |u|
problem "\"ftpmirror.gnu.org\" is preferred for GNU software (url is #{u})."
# the rest of the checks apply to mirrors as well.
# Check a variety of SSL/TLS links that don't consistently auto-redirect
# or are overly common errors that need to be reduced & fixed over time.
urls.each do |p|
# Skip the main url link, as it can't be made SSL/TLS yet.
next if p =~ %r[/ftpmirror\.gnu\.org]
case p
when %r[^http://ftp\.gnu\.org/]
problem "ftp.gnu.org urls should be https://, not http:// (url is #{p})."
when %r[^http://code\.google\.com/]
problem "code.google.com urls should be https://, not http (url is #{p})."
when %r[^http://fossies\.org/]
problem "Fossies urls should be https://, not http (url is #{p})."
when %r[^http://mirrors\.kernel\.org/]
problem "mirrors.kernel urls should be https://, not http (url is #{p})."
when %r[^http://tools\.ietf\.org/]
problem "ietf urls should be https://, not http (url is #{p})."
end
end
next if p =~ %r[/svnroot/]
next if p =~ %r[svn\.sourceforge]
Jaime Marquínez Ferrándiz
committed
next unless p =~ %r[^https?://.*\b(sourceforge|sf)\.(com|net)]
problem "Don't use #{$1}use_mirror in SourceForge urls (url is #{p})."
problem "Don't use /download in SourceForge urls (url is #{p})."
if p =~ %r[^https?://sourceforge\.]
problem "Use http://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/"
problem "Don't use specific dl mirrors in SourceForge urls (url is #{p})."
Jaime Marquínez Ferrándiz
committed
if p.start_with? "http://downloads"
problem "Use https:// URLs for downloads from SourceForge (url is #{p})."
end
# Check for Google Code download urls, https:// is preferred
urls.grep(%r[^http://.*\.googlecode\.com/files.*]) do |u|
problem "Use https:// URLs for downloads from Google Code (url is #{u})."
end
# Check for new-url Google Code download urls, https:// is preferred
urls.grep(%r[^http://code\.google\.com/]) do |u|
problem "Use https:// URLs for downloads from code.google (url is #{u})."
end
# Check for git:// GitHub repo urls, https:// is preferred.
urls.grep(%r[^git://[^/]*github\.com/]) do |u|
problem "Use https:// URLs for accessing GitHub repositories (url is #{u})."
# Check for git:// Gitorious repo urls, https:// is preferred.
urls.grep(%r[^git://[^/]*gitorious\.org/]) do |u|
problem "Use https:// URLs for accessing Gitorious repositories (url is #{u})."
end
# Check for http:// GitHub repo urls, https:// is preferred.
urls.grep(%r[^http://github\.com/.*\.git$]) do |u|
problem "Use https:// URLs for accessing GitHub repositories (url is #{u})."
urls.select { |u| u =~ %r[https://.*github.*/(?:tar|zip)ball/] && u !~ %r[\.git$] }.each do |u|
problem "Use /archive/ URLs for GitHub tarballs (url is #{u})."
end
# Don't use GitHub .zip files
urls.select { |u| u =~ %r[https://.*github.*/(archive|releases)/.*\.zip$] && u !~ %r[releases/download] }.each do |u|
problem "Use GitHub tarballs rather than zipballs (url is #{u})."
end
if head_only?(formula) && formula.tap != "Homebrew/homebrew-head-only"
problem "Head-only (no stable download)"
end
if devel_only?(formula) && formula.tap != "Homebrew/homebrew-devel-only"
problem "Devel-only (no stable download)"
end
next unless spec = formula.send(name.downcase)
ra = ResourceAuditor.new(spec).audit
problems.concat ra.problems.map { |problem| "#{name}: #{problem}" }
ra = ResourceAuditor.new(resource).audit
problems.concat ra.problems.map { |problem|
"#{name} resource #{resource.name.inspect}: #{problem}"
}
spec.patches.select(&:external?).each { |p| audit_patch(p) }
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
if stable && stable.url =~ /#{Regexp.escape("ftp.gnome.org/pub/GNOME/sources")}/i
minor_version = stable.version.to_s[/\d\.(\d+)/, 1].to_i
if minor_version.odd?
problem "#{stable.version} is a development release"
end
end
legacy_patches = Patch.normalize_legacy_patches(formula.patches).grep(LegacyPatch)
if legacy_patches.any?
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 %r[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}"
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}"
when %r[^https?://github\.com/.*commit.*\.patch$]
problem "GitHub appends a git version to patches; use .diff instead."
problem "use \"scons *args\" instead of \"system 'scons', *args\""
if text =~ /system\s+['"]xcodebuild/
problem %{use "xcodebuild *args" instead of "system 'xcodebuild', *args"}
end
if text =~ /xcodebuild[ (]["'*]/ && text !~ /SYMROOT=/
problem %{xcodebuild should be passed an explicit "SYMROOT"}
if text =~ /Formula\.factory\(/
problem "\"Formula.factory(name)\" is deprecated in favor of \"Formula[name]\""
end
if line =~ /<(Formula|AmazonWebServicesFormula|ScriptFileFormula|GithubGistFormula)/
problem "Use a space in class inheritance: class Foo < #{$1}"
problem "Commented cmake call found"
problem "Please remove default template comments"
end
if line =~ /# Documentation:/
problem "Please remove default template comments"
end
if line =~ /# if this fails, try separate make\/make install steps/
problem "Please remove default template comments"
end
if line =~ /# The url of the archive/
problem "Please remove default template comments"
end
if line =~ /## Naming --/
problem "Please remove default template comments"
end
if line =~ /# if your formula requires any X11\/XQuartz components/
problem "Please remove default template comments"
end
if line =~ /# if your formula fails when building in parallel/
problem "Please remove default template comments"
end
if line =~ /# Remove unrecognized options if warned by configure/
problem "Please remove default template comments"
end
# 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
problem "\"inreplace <filenames> do |s|\" is preferred over \"|#{$1}|\"."
end
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
problem "Try not to concatenate paths in string interpolation:\n #{$1}"
if line =~ %r{\(\s*(prefix\s*\+\s*(['"])(bin|include|libexec|lib|sbin|share|Frameworks)[/'"])}
problem "\"(#{$1}...#{$2})\" should be \"(#{$3.downcase}+...)\""
if line =~ %r[(\#\{prefix\}/(bin|include|libexec|lib|sbin|share|Frameworks))]
problem "\"#{$1}\" should be \"\#{#{$2.downcase}}\""
if line =~ %r[((\#\{prefix\}/share/man/|\#\{man\}/)(man[1-8]))]
problem "#{lineno}: Trailing whitespace was found"
problem "Use spaces instead of tabs for indentation"
end
problem "Use \"depends_on :x11\" instead of \"ENV.x11\""
end
if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?(gcc|llvm-gcc|clang)['" ]}
problem "Use \"\#{ENV.cc}\" instead of hard-coding \"#{$3}\""
if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?((g|llvm-g|clang)\+\+)['" ]}
problem "Use \"\#{ENV.cxx}\" instead of hard-coding \"#{$3}\""
end
problem "Use ENV instead of invoking '#{$1}' to modify the environment"
end
problem "Use 'build.head?' instead of inspecting 'version'"
end
if line =~ /build\.include\?[\s\(]+['"]\-\-(.*)['"]/
problem "Reference '#{$1}' without dashes"
if line =~ /build\.include\?[\s\(]+['"]with(out)?-(.*)['"]/
problem "Use build.with#{$1}? \"#{$2}\" instead of build.include? 'with#{$1}-#{$2}'"
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?'"
if line =~ /ARGV\.(?!(debug\?|verbose\?|value[\(\s]))/
problem "Use build instead of ARGV to check options"
problem "Use new-style option definitions"
problem "Use new-style test definitions (test do)"
end
problem "Use MacOS.version instead of MACOS_VERSION"
end
cats = %w{leopard snow_leopard lion mountain_lion}.join("|")
problem "\"#{$&}\" is deprecated, use a comparison to MacOS.version instead"
problem "`skip_clean :all` is deprecated; brew no longer strips symbols\n" +
"\tPass explicit paths to prevent Homebrew from removing empty folders."
problem "`depends_on` can take requirement classes instead of instances"
problem "Define method #{$1.inspect} in the class body, not at the top-level"
end
problem "Use `depends_on :fortran` instead of `ENV.fortran`"
end
audit_conditional_dep($1.to_sym, $2, $&)
end
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 =~ /system (["'][^"' ]*(?:\s[^"' ]*)+["'])/
bad_system = $1
good_system = bad_system.gsub(" ", "\", \"")
problem "Use `system #{good_system}` instead of `system #{bad_system}` "
end
if line =~ /(require ["']formula["'])/
problem "`#{$1}` is now unnecessary"
end
def audit_caveats
caveats = formula.caveats
if caveats =~ /setuid/
problem "Don't recommend setuid in the caveats, suggest sudo instead."
end
end
def audit_prefix_has_contents
return unless formula.prefix.directory?
Pathname.glob("#{formula.prefix}/**/*") do |file|
next if file.directory?
basename = file.basename.to_s
next if Metafiles.copy?(basename)
next if %w[.DS_Store INSTALL_RECEIPT.json].include?(basename)
return
end
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)
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}'"
def audit_check_output(output)
problem(output) if output
text.without_patch.split("\n").each_with_index { |line, lineno| audit_line(line, lineno+1) }
def head_only?(formula)
formula.head && formula.devel.nil? && formula.stable.nil?
end
def devel_only?(formula)
formula.devel && formula.stable.nil?
class ResourceAuditor
attr_reader :problems
attr_reader :version, :checksum, :using, :specs, :url, :name
@name = resource.name
@version = resource.version
@checksum = resource.checksum
@url = resource.url
@using = resource.using
@specs = resource.specs
@problems = []
end
def audit
audit_version
audit_checksum
audit_download_strategy
self
end
def audit_version
if version.nil?
problem "missing version"
elsif version.to_s.empty?
problem "version is set to an empty string"
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
elsif not 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 =~ /^v/
problem "version #{version} should not have a leading 'v'"
end
end
def audit_checksum
return unless checksum
case checksum.hash_type
when :md5
problem "MD5 checksums are deprecated, please use SHA1 or SHA256"
return
when :sha1 then len = 40
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
if using == :ssl3 || using == CurlSSL3DownloadStrategy
problem "The SSL3 download strategy is deprecated, please choose a different URL"
elsif using == CurlUnsafeDownloadStrategy || 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
url_strategy = DownloadStrategyDetector.detect(url)
using_strategy = DownloadStrategyDetector.detect('', using)
if url_strategy == using_strategy
problem "Redundant :using value in URL"
end
end
def problem text
@problems << text
end
end