Created
November 19, 2013 11:22
-
-
Save hanxue/7543946 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'formula' | |
require 'utils' | |
require 'extend/ENV' | |
require 'formula_cellar_checks' | |
module Homebrew extend self | |
def audit | |
formula_count = 0 | |
problem_count = 0 | |
ENV.activate_extensions! | |
ENV.setup_build_environment | |
ff = if ARGV.named.empty? | |
Formula | |
else | |
ARGV.formulae | |
end | |
ff.each do |f| | |
fa = FormulaAuditor.new f | |
fa.audit | |
unless fa.problems.empty? | |
puts "#{f.name}:" | |
fa.problems.each { |p| puts " * #{p}" } | |
puts | |
formula_count += 1 | |
problem_count += fa.problems.size | |
end | |
end | |
unless problem_count.zero? | |
ofail "#{problem_count} problems in #{formula_count} formulae" | |
end | |
end | |
end | |
class Module | |
def redefine_const(name, value) | |
__send__(:remove_const, name) if const_defined?(name) | |
const_set(name, value) | |
end | |
end | |
# Formula extensions for auditing | |
class Formula | |
def head_only? | |
@head and @stable.nil? | |
end | |
def text | |
@text ||= FormulaText.new(@path) | |
end | |
end | |
class FormulaText | |
def initialize path | |
@text = path.open('r') { |f| f.read } | |
end | |
def without_patch | |
@text.split("__END__")[0].strip() | |
end | |
def has_DATA? | |
/\bDATA\b/ =~ @text | |
end | |
def has_END? | |
/^__END__$/ =~ @text | |
end | |
def has_trailing_newline? | |
/\Z\n/ =~ @text | |
end | |
end | |
class FormulaAuditor | |
include FormulaCellarChecks | |
attr_reader :f, :text, :problems | |
BUILD_TIME_DEPS = %W[ | |
autoconf | |
automake | |
boost-build | |
bsdmake | |
cmake | |
imake | |
intltool | |
libtool | |
pkg-config | |
scons | |
smake | |
swig | |
] | |
def initialize f | |
@f = f | |
@problems = [] | |
@text = f.text.without_patch | |
@specs = %w{stable devel head}.map { |s| f.send(s) }.compact | |
# We need to do this in case the formula defines a patch that uses DATA. | |
f.class.redefine_const :DATA, "" | |
end | |
def audit_file | |
unless f.path.stat.mode.to_s(8) == "100644" | |
problem "Incorrect file permissions: chmod 644 #{f.path}" | |
end | |
if f.text.has_DATA? and not f.text.has_END? | |
problem "'DATA' was found, but no '__END__'" | |
end | |
if f.text.has_END? and not f.text.has_DATA? | |
problem "'__END__' was found, but 'DATA' is not used" | |
end | |
unless f.text.has_trailing_newline? | |
problem "File should end with a newline" | |
end | |
end | |
def audit_deps | |
# Don't depend_on aliases; use full name | |
@@aliases ||= Formula.aliases | |
f.deps.select { |d| @@aliases.include? d.name }.each do |d| | |
real_name = d.to_formula.name | |
problem "Dependency '#{d}' is an alias; use the canonical name '#{real_name}'." | |
end | |
# Check for things we don't like to depend on. | |
# We allow non-Homebrew installs whenever possible. | |
f.deps.each do |dep| | |
begin | |
dep_f = dep.to_formula | |
rescue FormulaUnavailableError | |
problem "Can't find dependency #{dep.name.inspect}." | |
next | |
end | |
dep.options.reject do |opt| | |
# TODO -- fix for :recommended, should still allow --with-xyz | |
dep_f.build.has_option?(opt.name) | |
end.each do |opt| | |
problem "Dependency #{dep} does not define option #{opt.name.inspect}" | |
end | |
case dep.name | |
when *BUILD_TIME_DEPS | |
next if dep.build? or dep.run? | |
problem %{#{dep} dependency should be "depends_on '#{dep}' => :build"} | |
when "git", "ruby", "emacs", "mercurial" | |
problem <<-EOS.undent | |
Don't use #{dep} as a dependency. We allow non-Homebrew | |
#{dep} installations. | |
EOS | |
when 'python', 'python2', 'python3' | |
problem <<-EOS.undent | |
Don't use #{dep} as a dependency (string). | |
We have special `depends_on :python` (or :python2 or :python3 ) | |
that works with brewed and system Python and allows us to support | |
bindings for 2.x and 3.x in parallel and much more. | |
EOS | |
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 | |
end | |
end | |
def audit_conflicts | |
f.conflicts.each do |c| | |
begin | |
Formula.factory(c.name) | |
rescue FormulaUnavailableError | |
problem "Can't find conflicting formula #{c.name.inspect}." | |
end | |
end | |
end | |
def audit_urls | |
unless f.homepage =~ %r[^https?://] | |
problem "The homepage should start with http or https (url is #{f.homepage})." | |
end | |
# Check for http:// GitHub homepage urls, https:// is preferred. | |
# Note: only check homepages that are repo pages, not *.github.com hosts | |
if f.homepage =~ %r[^http://github\.com/] | |
problem "Use https:// URLs for homepages on GitHub (url is #{f.homepage})." | |
end | |
# Google Code homepages should end in a slash | |
if f.homepage =~ %r[^https?://code\.google\.com/p/[^/]+[^/]$] | |
problem "Google Code homepage should end with a slash (url is #{f.homepage})." | |
end | |
urls = @specs.map(&:url) | |
# Check GNU urls; doesn't apply to mirrors | |
urls.grep(%r[^(?:https?|ftp)://(?!alpha).+/gnu/]) do |u| | |
problem "\"ftpmirror.gnu.org\" is preferred for GNU software (url is #{u})." | |
end | |
# the rest of the checks apply to mirrors as well | |
urls.concat(@specs.map(&:mirrors).flatten) | |
# Check SourceForge urls | |
urls.each do |p| | |
# Skip if the URL looks like a SVN repo | |
next if p =~ %r[/svnroot/] | |
next if p =~ %r[svn\.sourceforge] | |
# Is it a sourceforge http(s) URL? | |
next unless p =~ %r[^https?://.*\bsourceforge\.com] | |
if p =~ /(\?|&)use_mirror=/ | |
problem "Don't use #{$1}use_mirror in SourceForge urls (url is #{p})." | |
end | |
if p =~ /\/download$/ | |
problem "Don't use /download in SourceForge urls (url is #{p})." | |
end | |
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/" | |
end | |
if p =~ %r[^http://\w+\.dl\.] | |
problem "Don't use specific dl mirrors in SourceForge urls (url is #{p})." | |
end | |
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})." | |
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})." | |
end | |
# Use new-style archive downloads | |
urls.select { |u| u =~ %r[https://.*/(?:tar|zip)ball/] && u !~ %r[\.git$] }.each do |u| | |
problem "Use /archive/ URLs for GitHub tarballs (url is #{u})." | |
end | |
end | |
def audit_specs | |
problem "Head-only (no stable download)" if f.head_only? | |
%w[Stable Devel HEAD].each do |name| | |
next unless spec = f.send(name.downcase) | |
ra = ResourceAuditor.new(spec).audit | |
problems.concat ra.problems.map { |problem| "#{name}: #{problem}" } | |
spec.resources.each_value do |resource| | |
ra = ResourceAuditor.new(resource).audit | |
problems.concat ra.problems.map { |problem| | |
"#{name} resource #{resource.name.inspect}: #{problem}" | |
} | |
end | |
end | |
end | |
def audit_patches | |
Patches.new(f.patches).select(&:external?).each do |p| | |
case p.url | |
when %r[raw\.github\.com], %r[gist\.github\.com/raw] | |
unless p.url =~ /[a-fA-F0-9]{40}/ | |
problem "GitHub/Gist patches should specify a revision:\n#{p.url}" | |
end | |
when %r[macports/trunk] | |
problem "MacPorts patches should specify a revision instead of trunk:\n#{p.url}" | |
end | |
end | |
end | |
def audit_text | |
if text =~ /system\s+['"]xcodebuild/ && text !~ /SYMROOT=/ | |
problem "xcodebuild should be passed an explicit \"SYMROOT\"" | |
end | |
end | |
def audit_line(line) | |
if line =~ /<(Formula|AmazonWebServicesFormula|ScriptFileFormula|GithubGistFormula)/ | |
problem "Use a space in class inheritance: class Foo < #{$1}" | |
end | |
# Commented-out cmake support from default template | |
if line =~ /# system "cmake/ | |
problem "Commented cmake call found" | |
end | |
# Comments from default template | |
if line =~ /# PLEASE REMOVE/ | |
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 =~ /# if your formula requires any X11\/XQuartz components/ | |
problem "Please remove default template comments" | |
end | |
if line =~ /# if your formula's build system can't parallelize/ | |
problem "Please remove default template comments" | |
end | |
# FileUtils is included in Formula | |
if line =~ /FileUtils\.(\w+)/ | |
problem "Don't need 'FileUtils.' before #{$1}." | |
end | |
# Check for long inreplace block vars | |
if line =~ /inreplace .* do \|(.{2,})\|/ | |
problem "\"inreplace <filenames> do |s|\" is preferred over \"|#{$1}|\"." | |
end | |
# Check for string interpolation of single values. | |
if line =~ /(system|inreplace|gsub!|change_make_var!).*[ ,]"#\{([\w.]+)\}"/ | |
problem "Don't need to interpolate \"#{$2}\" with #{$1}" | |
end | |
# Check for string concatenation; prefer interpolation | |
if line =~ /(#\{\w+\s*\+\s*['"][^}]+\})/ | |
problem "Try not to concatenate paths in string interpolation:\n #{$1}" | |
end | |
# Prefer formula path shortcuts in Pathname+ | |
if line =~ %r{\(\s*(prefix\s*\+\s*(['"])(bin|include|libexec|lib|sbin|share)[/'"])} | |
problem "\"(#{$1}...#{$2})\" should be \"(#{$3}+...)\"" | |
end | |
if line =~ %r[((man)\s*\+\s*(['"])(man[1-8])(['"]))] | |
problem "\"#{$1}\" should be \"#{$4}\"" | |
end | |
# Prefer formula path shortcuts in strings | |
if line =~ %r[(\#\{prefix\}/(bin|include|libexec|lib|sbin|share))] | |
problem "\"#{$1}\" should be \"\#{#{$2}}\"" | |
end | |
if line =~ %r[((\#\{prefix\}/share/man/|\#\{man\}/)(man[1-8]))] | |
problem "\"#{$1}\" should be \"\#{#{$3}}\"" | |
end | |
if line =~ %r[((\#\{share\}/(man)))[/'"]] | |
problem "\"#{$1}\" should be \"\#{#{$3}}\"" | |
end | |
if line =~ %r[(\#\{prefix\}/share/(info|man))] | |
problem "\"#{$1}\" should be \"\#{#{$2}}\"" | |
end | |
# Commented-out depends_on | |
if line =~ /#\s*depends_on\s+(.+)\s*$/ | |
problem "Commented-out dep #{$1}" | |
end | |
# No trailing whitespace, please | |
if line =~ /[\t ]+$/ | |
problem "Trailing whitespace was found" | |
end | |
if line =~ /if\s+ARGV\.include\?\s+'--(HEAD|devel)'/ | |
problem "Use \"if ARGV.build_#{$1.downcase}?\" instead" | |
end | |
if line =~ /make && make/ | |
problem "Use separate make calls" | |
end | |
if line =~ /^[ ]*\t/ | |
problem "Use spaces instead of tabs for indentation" | |
end | |
if line =~ /ENV\.x11/ | |
problem "Use \"depends_on :x11\" instead of \"ENV.x11\"" | |
end | |
# Avoid hard-coding compilers | |
unless f.name == 'go' # Go needs to set CC for cgo support. | |
if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?(gcc|llvm-gcc|clang)['" ]} | |
problem "Use \"\#{ENV.cc}\" instead of hard-coding \"#{$3}\"" | |
end | |
end | |
if line =~ %r{(system|ENV\[.+\]\s?=)\s?['"](/usr/bin/)?((g|llvm-g|clang)\+\+)['" ]} | |
problem "Use \"\#{ENV.cxx}\" instead of hard-coding \"#{$3}\"" | |
end | |
if line =~ /system\s+['"](env|export)/ | |
problem "Use ENV instead of invoking '#{$1}' to modify the environment" | |
end | |
if line =~ /version == ['"]HEAD['"]/ | |
problem "Use 'build.head?' instead of inspecting 'version'" | |
end | |
if line =~ /build\.include\?\s+['"]\-\-(.*)['"]/ | |
problem "Reference '#{$1}' without dashes" | |
end | |
if line =~ /build\.with\?\s+['"]-?-?with-(.*)['"]/ | |
problem "No double 'with': Use `build.with? '#{$1}'` to check for \"--with-#{$1}\"" | |
end | |
if line =~ /build\.without\?\s+['"]-?-?without-(.*)['"]/ | |
problem "No double 'without': Use `build.without? '#{$1}'` to check for \"--without-#{$1}\"" | |
end | |
unless f.name == 'mongodb' # Mongo writes out a Ruby script that uses ARGV | |
if line =~ /ARGV\.(?!(debug\?|verbose\?|value[\(\s]))/ | |
problem "Use build instead of ARGV to check options" | |
end | |
end | |
if line =~ /def options/ | |
problem "Use new-style option definitions" | |
end | |
if line =~ /MACOS_VERSION/ | |
problem "Use MacOS.version instead of MACOS_VERSION" | |
end | |
cats = %w{leopard snow_leopard lion mountain_lion}.join("|") | |
if line =~ /MacOS\.(?:#{cats})\?/ | |
problem "\"#{$&}\" is deprecated, use a comparison to MacOS.version instead" | |
end | |
if line =~ /skip_clean\s+:all/ | |
problem "`skip_clean :all` is deprecated; brew no longer strips symbols" | |
end | |
if line =~ /depends_on [A-Z][\w:]+\.new$/ | |
problem "`depends_on` can take requirement classes instead of instances" | |
end | |
if line =~ /^def (\w+).*$/ | |
problem "Define method #{$1.inspect} in the class body, not at the top-level" | |
end | |
if line =~ /ENV.fortran/ | |
problem "Use `depends_on :fortran` instead of `ENV.fortran`" | |
end | |
if line =~ /depends_on :(.+) (if.+|unless.+)$/ | |
audit_conditional_dep($1.to_sym, $2, $&) | |
end | |
if line =~ /depends_on ['"](.+)['"] (if.+|unless.+)$/ | |
audit_conditional_dep($1, $2, $&) | |
end | |
end | |
def audit_conditional_dep(dep, condition, line) | |
quoted_dep = quote_dep(dep) | |
dep = Regexp.escape(dep.to_s) | |
case condition | |
when /if build\.include\? ['"]with-#{dep}['"]$/, /if build\.with\? ['"]#{dep}['"]$/ | |
problem %{Replace #{line.inspect} with "depends_on #{quoted_dep} => :optional"} | |
when /unless build\.include\? ['"]without-#{dep}['"]$/, /unless build\.without\? ['"]#{dep}['"]$/ | |
problem %{Replace #{line.inspect} with "depends_on #{quoted_dep} => :recommended"} | |
end | |
end | |
def quote_dep(dep) | |
Symbol === dep ? dep.inspect : "'#{dep}'" | |
end | |
def audit_python | |
if text =~ /(def\s*)?which_python/ | |
problem "Replace `which_python` by `python.xy`, which returns e.g. 'python2.7'" | |
end | |
if text =~ /which\(?["']python/ | |
problem "Don't locate python with `which 'python'`, use `python.binary` instead" | |
end | |
if text =~ /LanguageModuleDependency.new\s?\(\s?:python/ | |
problem <<-EOS.undent | |
Python: Replace `LanguageModuleDependency.new(:python,'PyPi-name','module')` | |
by the new `depends_on :python => ['module' => 'PyPi-name']` | |
EOS | |
end | |
# Checks that apply only to code in def install | |
if text =~ /(\s*)def\s+install((.*\n)*?)(\1end)/ | |
install_body = $2 | |
if install_body =~ /system\(?\s*['"]python/ | |
problem "Instead of `system 'python', ...`, call `system python, ...`" | |
end | |
if text =~ /system\(?\s*python\.binary/ | |
problem "Instead of `system python.binary, ...`, call `system python, ...`" | |
end | |
end | |
# Checks that apply only to code in def caveats | |
if text =~ /(\s*)def\s+caveats((.*\n)*?)(\1end)/ || text =~ /(\s*)def\s+caveats;(.*?)end/ | |
caveats_body = $2 | |
if caveats_body =~ /[ \{=](python[23]?)\.(.*\w)/ | |
# So if in the body of caveats there is a `python.whatever` called, | |
# check that there is a guard like `if python` or similiar: | |
python = $1 | |
method = $2 | |
unless caveats_body =~ /(if python[23]?)|(if build\.with\?\s?\(?['"]python)|(unless build.without\?\s?\(?['"]python)/ | |
problem "Please guard `#{python}.#{method}` like so `#{python}.#{method} if #{python}`" | |
end | |
end | |
end | |
unless f.requirements.any?{ |r| r.kind_of?(PythonDependency) } | |
# So if there is no PythonDependency requirement, we can check if the | |
# formula still uses python and should add a `depends_on :python` | |
unless f.name.to_s =~ /(pypy[0-9]*)|(python[0-9]*)/ | |
if text =~ /system.["' ]?python([0-9"'])?/ | |
problem "If the formula uses Python, it should declare so by `depends_on :python#{$1}`" | |
end | |
if text =~ /setup\.py/ | |
problem <<-EOS.undent | |
If the formula installs Python bindings you should declare `depends_on :python[3]`" | |
EOS | |
end | |
end | |
end | |
# Todo: | |
# The python do ... end block is possibly executed twice. Once for | |
# python 2.x and once for 3.x. So if a `system 'make'` is called, a | |
# `system 'make clean'` should also be called at the end of the block. | |
end | |
def audit_check_output warning_and_description | |
return unless warning_and_description | |
warning, description = *warning_and_description | |
problem "#{warning}\n#{description}" | |
end | |
def audit_installed | |
audit_check_output(check_manpages) | |
audit_check_output(check_infopages) | |
audit_check_output(check_jars) | |
audit_check_output(check_non_libraries) | |
audit_check_output(check_non_executables(f.bin)) | |
audit_check_output(check_generic_executables(f.bin)) | |
audit_check_output(check_non_executables(f.sbin)) | |
audit_check_output(check_generic_executables(f.sbin)) | |
end | |
def audit | |
audit_file | |
audit_specs | |
audit_urls | |
audit_deps | |
audit_conflicts | |
audit_patches | |
audit_text | |
text.each_line { |line| audit_line(line) } | |
audit_python | |
audit_installed | |
end | |
private | |
def problem p | |
@problems << p | |
end | |
end | |
class ResourceAuditor | |
attr_reader :problems | |
attr_reader :version, :checksum, :using, :specs, :url | |
def initialize(resource) | |
@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.to_s.empty? | |
problem "invalid or missing version" | |
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 | |
return unless using | |
url_strategy = DownloadStrategyDetector.detect(url) | |
using_strategy = DownloadStrategyDetector.detect('', using) | |
if url_strategy == using_strategy | |
problem "redundant :using specification in URL" | |
end | |
end | |
def problem text | |
@problems << text | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment