| #!/usr/bin/env perl |
| # |
| # The LLVM Compiler Infrastructure |
| # |
| # This file is distributed under the University of Illinois Open Source |
| # License. See LICENSE.TXT for details. |
| # |
| ##===----------------------------------------------------------------------===## |
| # |
| # A script designed to wrap a build so that all calls to gcc are intercepted |
| # and piped to the static analyzer. |
| # |
| ##===----------------------------------------------------------------------===## |
| |
| use strict; |
| use warnings; |
| use FindBin qw($RealBin); |
| use Digest::MD5; |
| use File::Basename; |
| use Term::ANSIColor; |
| use Term::ANSIColor qw(:constants); |
| use Cwd; |
| use Sys::Hostname; |
| |
| my $Verbose = 0; # Verbose output from this script. |
| my $Prog = "scan-build"; |
| my $BuildName; |
| my $BuildDate; |
| my $CXX; # Leave undefined initially. |
| |
| my $TERM = $ENV{'TERM'}; |
| my $UseColor = (defined $TERM and $TERM eq 'xterm-color' and -t STDOUT |
| and defined $ENV{'SCAN_BUILD_COLOR'}); |
| |
| my $UserName = HtmlEscape(getpwuid($<) || 'unknown'); |
| my $HostName = HtmlEscape(hostname() || 'unknown'); |
| my $CurrentDir = HtmlEscape(getcwd()); |
| my $CurrentDirSuffix = basename($CurrentDir); |
| |
| my $CmdArgs; |
| |
| my $HtmlTitle; |
| |
| my $Date = localtime(); |
| |
| ##----------------------------------------------------------------------------## |
| # Diagnostics |
| ##----------------------------------------------------------------------------## |
| |
| sub Diag { |
| if ($UseColor) { |
| print BOLD, MAGENTA "$Prog: @_"; |
| print RESET; |
| } |
| else { |
| print "$Prog: @_"; |
| } |
| } |
| |
| sub DiagCrashes { |
| my $Dir = shift; |
| Diag ("The analyzer crashed on some source files.\n"); |
| Diag ("Preprocessed versions of crashed files were deposited in '$Dir/crashes'.\n"); |
| Diag ("Please consider submitting a bug report using these files:\n"); |
| Diag (" http://clang.llvm.org/StaticAnalysisUsage.html#filingbugs\n") |
| } |
| |
| sub DieDiag { |
| if ($UseColor) { |
| print BOLD, RED "$Prog: "; |
| print RESET, RED @_; |
| print RESET; |
| } |
| else { |
| print "$Prog: ", @_; |
| } |
| exit(0); |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # Some initial preprocessing of Clang options. |
| ##----------------------------------------------------------------------------## |
| |
| my $ClangSB = Cwd::realpath("$RealBin/clang"); |
| my $Clang = $ClangSB; |
| |
| if (! -x $ClangSB) { |
| $Clang = "clang"; |
| } |
| |
| my %AvailableAnalyses; |
| |
| # Query clang for analysis options. |
| open(PIPE, "-|", $Clang, "--help") or |
| DieDiag("Cannot execute '$Clang'\n"); |
| |
| my $FoundAnalysis = 0; |
| |
| while(<PIPE>) { |
| if ($FoundAnalysis == 0) { |
| if (/Available Source Code Analyses/) { |
| $FoundAnalysis = 1; |
| } |
| |
| next; |
| } |
| |
| if (/^\s\s\s\s([^\s]+)\s(.+)$/) { |
| next if ($1 =~ /-dump/ or $1 =~ /-view/ |
| or $1 =~ /-checker-simple/ or $1 =~ /-warn-uninit/); |
| |
| $AvailableAnalyses{$1} = $2; |
| next; |
| } |
| |
| last; |
| } |
| |
| close (PIPE); |
| |
| my %AnalysesDefaultEnabled = ( |
| '-warn-dead-stores' => 1, |
| '-checker-cfref' => 1, |
| '-warn-objc-methodsigs' => 1, |
| '-warn-objc-missing-dealloc' => 1, |
| '-warn-objc-unused-ivars' => 1, |
| ); |
| |
| ##----------------------------------------------------------------------------## |
| # GetHTMLRunDir - Construct an HTML directory name for the current sub-run. |
| ##----------------------------------------------------------------------------## |
| |
| sub GetHTMLRunDir { |
| |
| die "Not enough arguments." if (@_ == 0); |
| my $Dir = shift @_; |
| |
| my $TmpMode = 0; |
| if (!defined $Dir) { |
| $Dir = "/tmp"; |
| $TmpMode = 1; |
| } |
| |
| # Get current date and time. |
| |
| my @CurrentTime = localtime(); |
| |
| my $year = $CurrentTime[5] + 1900; |
| my $day = $CurrentTime[3]; |
| my $month = $CurrentTime[4] + 1; |
| |
| my $DateString = sprintf("%d-%02d-%02d", $year, $month, $day); |
| |
| # Determine the run number. |
| |
| my $RunNumber; |
| |
| if (-d $Dir) { |
| |
| if (! -r $Dir) { |
| DieDiag("directory '$Dir' exists but is not readable.\n"); |
| } |
| |
| # Iterate over all files in the specified directory. |
| |
| my $max = 0; |
| |
| opendir(DIR, $Dir); |
| my @FILES = grep { -d "$Dir/$_" } readdir(DIR); |
| closedir(DIR); |
| |
| foreach my $f (@FILES) { |
| |
| # Strip the prefix '$Prog-' if we are dumping files to /tmp. |
| if ($TmpMode) { |
| next if (!($f =~ /^$Prog-(.+)/)); |
| $f = $1; |
| } |
| |
| |
| my @x = split/-/, $f; |
| next if (scalar(@x) != 4); |
| next if ($x[0] != $year); |
| next if ($x[1] != $month); |
| next if ($x[2] != $day); |
| |
| if ($x[3] > $max) { |
| $max = $x[3]; |
| } |
| } |
| |
| $RunNumber = $max + 1; |
| } |
| else { |
| |
| if (-x $Dir) { |
| DieDiag("'$Dir' exists but is not a directory.\n"); |
| } |
| |
| if ($TmpMode) { |
| DieDiag("The directory '/tmp' does not exist or cannot be accessed.\n"); |
| } |
| |
| # $Dir does not exist. It will be automatically created by the |
| # clang driver. Set the run number to 1. |
| |
| $RunNumber = 1; |
| } |
| |
| die "RunNumber must be defined!" if (!defined $RunNumber); |
| |
| # Append the run number. |
| my $NewDir; |
| if ($TmpMode) { |
| $NewDir = "$Dir/$Prog-$DateString-$RunNumber"; |
| } |
| else { |
| $NewDir = "$Dir/$DateString-$RunNumber"; |
| } |
| system 'mkdir','-p',$NewDir; |
| return $NewDir; |
| } |
| |
| sub SetHtmlEnv { |
| |
| die "Wrong number of arguments." if (scalar(@_) != 2); |
| |
| my $Args = shift; |
| my $Dir = shift; |
| |
| die "No build command." if (scalar(@$Args) == 0); |
| |
| my $Cmd = $$Args[0]; |
| |
| if ($Cmd =~ /configure/) { |
| return; |
| } |
| |
| if ($Verbose) { |
| Diag("Emitting reports for this run to '$Dir'.\n"); |
| } |
| |
| $ENV{'CCC_ANALYZER_HTML'} = $Dir; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # ComputeDigest - Compute a digest of the specified file. |
| ##----------------------------------------------------------------------------## |
| |
| sub ComputeDigest { |
| my $FName = shift; |
| DieDiag("Cannot read $FName to compute Digest.\n") if (! -r $FName); |
| |
| # Use Digest::MD5. We don't have to be cryptographically secure. We're |
| # just looking for duplicate files that come from a non-malicious source. |
| # We use Digest::MD5 because it is a standard Perl module that should |
| # come bundled on most systems. |
| open(FILE, $FName) or DieDiag("Cannot open $FName when computing Digest.\n"); |
| binmode FILE; |
| my $Result = Digest::MD5->new->addfile(*FILE)->hexdigest; |
| close(FILE); |
| |
| # Return the digest. |
| return $Result; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # UpdatePrefix - Compute the common prefix of files. |
| ##----------------------------------------------------------------------------## |
| |
| my $Prefix; |
| |
| sub UpdatePrefix { |
| my $x = shift; |
| my $y = basename($x); |
| $x =~ s/\Q$y\E$//; |
| |
| # Ignore /usr, /Library, /System, /Developer |
| return if ( $x =~ /^\/usr/ or $x =~ /^\/Library/ |
| or $x =~ /^\/System/ or $x =~ /^\/Developer/); |
| |
| if (!defined $Prefix) { |
| $Prefix = $x; |
| return; |
| } |
| |
| chop $Prefix while (!($x =~ /^\Q$Prefix/)); |
| } |
| |
| sub GetPrefix { |
| return $Prefix; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # UpdateInFilePath - Update the path in the report file. |
| ##----------------------------------------------------------------------------## |
| |
| sub UpdateInFilePath { |
| my $fname = shift; |
| my $regex = shift; |
| my $newtext = shift; |
| |
| open (RIN, $fname) or die "cannot open $fname"; |
| open (ROUT, ">", "$fname.tmp") or die "cannot open $fname.tmp"; |
| |
| while (<RIN>) { |
| s/$regex/$newtext/; |
| print ROUT $_; |
| } |
| |
| close (ROUT); |
| close (RIN); |
| system("mv", "$fname.tmp", $fname); |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # ScanFile - Scan a report file for various identifying attributes. |
| ##----------------------------------------------------------------------------## |
| |
| # Sometimes a source file is scanned more than once, and thus produces |
| # multiple error reports. We use a cache to solve this problem. |
| |
| my %AlreadyScanned; |
| |
| sub ScanFile { |
| |
| my $Index = shift; |
| my $Dir = shift; |
| my $FName = shift; |
| |
| # Compute a digest for the report file. Determine if we have already |
| # scanned a file that looks just like it. |
| |
| my $digest = ComputeDigest("$Dir/$FName"); |
| |
| if (defined $AlreadyScanned{$digest}) { |
| # Redundant file. Remove it. |
| system ("rm", "-f", "$Dir/$FName"); |
| return; |
| } |
| |
| $AlreadyScanned{$digest} = 1; |
| |
| # At this point the report file is not world readable. Make it happen. |
| system ("chmod", "644", "$Dir/$FName"); |
| |
| # Scan the report file for tags. |
| open(IN, "$Dir/$FName") or DieDiag("Cannot open '$Dir/$FName'\n"); |
| |
| my $BugDesc = ""; |
| my $BugFile = ""; |
| my $BugCategory; |
| my $BugPathLength = 1; |
| my $BugLine = 0; |
| my $found = 0; |
| |
| while (<IN>) { |
| |
| last if ($found == 5); |
| |
| if (/<!-- BUGDESC (.*) -->$/) { |
| $BugDesc = $1; |
| ++$found; |
| } |
| elsif (/<!-- BUGFILE (.*) -->$/) { |
| $BugFile = $1; |
| UpdatePrefix($BugFile); |
| ++$found; |
| } |
| elsif (/<!-- BUGPATHLENGTH (.*) -->$/) { |
| $BugPathLength = $1; |
| ++$found; |
| } |
| elsif (/<!-- BUGLINE (.*) -->$/) { |
| $BugLine = $1; |
| ++$found; |
| } |
| elsif (/<!-- BUGCATEGORY (.*) -->$/) { |
| $BugCategory = $1; |
| ++$found; |
| } |
| } |
| |
| close(IN); |
| |
| if (!defined $BugCategory) { |
| $BugCategory = "Other"; |
| } |
| |
| push @$Index,[ $FName, $BugCategory, $BugDesc, $BugFile, $BugLine, |
| $BugPathLength ]; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # CopyFiles - Copy resource files to target directory. |
| ##----------------------------------------------------------------------------## |
| |
| sub CopyFiles { |
| |
| my $Dir = shift; |
| |
| my $JS = Cwd::realpath("$RealBin/sorttable.js"); |
| |
| DieDiag("Cannot find 'sorttable.js'.\n") |
| if (! -r $JS); |
| |
| system ("cp", $JS, "$Dir"); |
| |
| DieDiag("Could not copy 'sorttable.js' to '$Dir'.\n") |
| if (! -r "$Dir/sorttable.js"); |
| |
| my $CSS = Cwd::realpath("$RealBin/scanview.css"); |
| |
| DieDiag("Cannot find 'scanview.css'.\n") |
| if (! -r $CSS); |
| |
| system ("cp", $CSS, "$Dir"); |
| |
| DieDiag("Could not copy 'scanview.css' to '$Dir'.\n") |
| if (! -r $CSS); |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # Postprocess - Postprocess the results of an analysis scan. |
| ##----------------------------------------------------------------------------## |
| |
| sub Postprocess { |
| |
| my $Dir = shift; |
| my $BaseDir = shift; |
| |
| die "No directory specified." if (!defined $Dir); |
| |
| if (! -d $Dir) { |
| Diag("No bugs found.\n"); |
| return 0; |
| } |
| |
| opendir(DIR, $Dir); |
| my $Crashes = 0; |
| my @files = grep { if ($_ eq "crashes") { $Crashes++; } |
| /^report-.*\.html$/; } readdir(DIR); |
| closedir(DIR); |
| |
| if (scalar(@files) == 0 and $Crashes == 0) { |
| Diag("Removing directory '$Dir' because it contains no reports.\n"); |
| system ("rm", "-fR", $Dir); |
| return 0; |
| } |
| |
| # Scan each report file and build an index. |
| my @Index; |
| foreach my $file (@files) { ScanFile(\@Index, $Dir, $file); } |
| |
| # Scan the crashes directory and use the information in the .info files |
| # to update the common prefix directory. |
| if (-d "$Dir/crashes") { |
| opendir(DIR, "$Dir/crashes"); |
| my @files = grep { /[.]info.txt$/; } readdir(DIR); |
| closedir(DIR); |
| foreach my $file (@files) { |
| open IN, "$Dir/crashes/$file" or DieDiag("cannot open $file\n"); |
| my $Path = <IN>; |
| if (defined $Path) { UpdatePrefix($Path); } |
| close IN; |
| } |
| } |
| |
| # Generate an index.html file. |
| my $FName = "$Dir/index.html"; |
| open(OUT, ">", $FName) or DieDiag("Cannot create file '$FName'\n"); |
| |
| # Print out the header. |
| |
| print OUT <<ENDTEXT; |
| <html> |
| <head> |
| <title>${HtmlTitle}</title> |
| <link type="text/css" rel="stylesheet" href="scanview.css"/> |
| <script src="sorttable.js"></script> |
| <script language='javascript' type="text/javascript"> |
| function SetDisplay(RowClass, DisplayVal) |
| { |
| var Rows = document.getElementsByTagName("tr"); |
| for ( var i = 0 ; i < Rows.length; ++i ) { |
| if (Rows[i].className == RowClass) { |
| Rows[i].style.display = DisplayVal; |
| } |
| } |
| } |
| |
| function ToggleDisplay(CheckButton, ClassName) { |
| if (CheckButton.checked) { |
| SetDisplay(ClassName, ""); |
| } |
| else { |
| SetDisplay(ClassName, "none"); |
| } |
| } |
| </script> |
| <!-- SUMMARYENDHEAD --> |
| </head> |
| <body> |
| <h1>${HtmlTitle}</h1> |
| |
| <table> |
| <tr><th>User:</th><td>${UserName}\@${HostName}</td></tr> |
| <tr><th>Working Directory:</th><td>${CurrentDir}</td></tr> |
| <tr><th>Command Line:</th><td>${CmdArgs}</td></tr> |
| <tr><th>Date:</th><td>${Date}</td></tr> |
| ENDTEXT |
| |
| print OUT "<tr><th>Version:</th><td>${BuildName} (${BuildDate})</td></tr>\n" |
| if (defined($BuildName) && defined($BuildDate)); |
| |
| print OUT <<ENDTEXT; |
| </table> |
| ENDTEXT |
| |
| if (scalar(@files)) { |
| # Print out the summary table. |
| my %Totals; |
| |
| for my $row ( @Index ) { |
| my $bug_type = ($row->[2]); |
| my $bug_category = ($row->[1]); |
| my $key = "$bug_category:$bug_type"; |
| |
| if (!defined $Totals{$key}) { $Totals{$key} = [1,$bug_category,$bug_type]; } |
| else { $Totals{$key}->[0]++; } |
| } |
| |
| print OUT "<h2>Bug Summary</h2>"; |
| |
| if (defined $BuildName) { |
| print OUT "\n<p>Results in this analysis run are based on analyzer build <b>$BuildName</b>.</p>\n" |
| } |
| |
| print OUT <<ENDTEXT; |
| <table> |
| <thead><tr><td>Bug Type</td><td>Quantity</td><td class="sorttable_nosort">Display?</td></tr></thead> |
| ENDTEXT |
| |
| my $last_category; |
| |
| for my $key ( |
| sort { |
| my $x = $Totals{$a}; |
| my $y = $Totals{$b}; |
| my $res = $x->[1] cmp $y->[1]; |
| $res = $x->[2] cmp $y->[2] if ($res == 0); |
| $res |
| } keys %Totals ) |
| { |
| my $val = $Totals{$key}; |
| my $category = $val->[1]; |
| if (!defined $last_category or $last_category ne $category) { |
| $last_category = $category; |
| print OUT "<tr><th>$category</th><th colspan=2></th></tr>\n"; |
| } |
| my $x = lc $key; |
| $x =~ s/[ ,'":\/()]+/_/g; |
| print OUT "<tr><td class=\"SUMM_DESC\">"; |
| print OUT $val->[2]; |
| print OUT "</td><td>"; |
| print OUT $val->[0]; |
| print OUT "</td><td><center><input type=\"checkbox\" onClick=\"ToggleDisplay(this,'bt_$x');\" checked/></center></td></tr>\n"; |
| } |
| |
| # Print out the table of errors. |
| |
| print OUT <<ENDTEXT; |
| </table> |
| <h2>Reports</h2> |
| |
| <table class="sortable" style="table-layout:automatic"> |
| <thead><tr> |
| <td>Bug Group</td> |
| <td class="sorttable_sorted">Bug Type<span id="sorttable_sortfwdind"> ▾</span></td> |
| <td>File</td> |
| <td class="Q">Line</td> |
| <td class="Q">Path Length</td> |
| <td class="sorttable_nosort"></td> |
| <!-- REPORTBUGCOL --> |
| </tr></thead> |
| <tbody> |
| ENDTEXT |
| |
| my $prefix = GetPrefix(); |
| my $regex; |
| my $InFileRegex; |
| my $InFilePrefix = "File:</td><td>"; |
| |
| if (defined $prefix) { |
| $regex = qr/^\Q$prefix\E/is; |
| $InFileRegex = qr/\Q$InFilePrefix$prefix\E/is; |
| } |
| |
| for my $row ( sort { $a->[2] cmp $b->[2] } @Index ) { |
| my $x = "$row->[1]:$row->[2]"; |
| $x = lc $x; |
| $x =~ s/[ ,'":\/()]+/_/g; |
| |
| my $ReportFile = $row->[0]; |
| |
| print OUT "<tr class=\"bt_$x\">"; |
| print OUT "<td class=\"DESC\">"; |
| print OUT $row->[1]; |
| print OUT "</td>"; |
| print OUT "<td class=\"DESC\">"; |
| print OUT $row->[2]; |
| print OUT "</td>"; |
| |
| # Update the file prefix. |
| my $fname = $row->[3]; |
| |
| if (defined $regex) { |
| $fname =~ s/$regex//; |
| UpdateInFilePath("$Dir/$ReportFile", $InFileRegex, $InFilePrefix) |
| } |
| |
| print OUT "<td>"; |
| my @fname = split /\//,$fname; |
| if ($#fname > 0) { |
| while ($#fname >= 0) { |
| my $x = shift @fname; |
| print OUT $x; |
| if ($#fname >= 0) { |
| print OUT "<span class=\"W\"> </span>/"; |
| } |
| } |
| } |
| else { |
| print OUT $fname; |
| } |
| print OUT "</td>"; |
| |
| # Print out the quantities. |
| for my $j ( 4 .. 5 ) { |
| print OUT "<td class=\"Q\">$row->[$j]</td>"; |
| } |
| |
| # Print the rest of the columns. |
| for (my $j = 6; $j <= $#{$row}; ++$j) { |
| print OUT "<td>$row->[$j]</td>" |
| } |
| |
| # Emit the "View" link. |
| print OUT "<td><a href=\"$ReportFile#EndPath\">View Report</a></td>"; |
| |
| # Emit REPORTBUG markers. |
| print OUT "\n<!-- REPORTBUG id=\"$ReportFile\" -->\n"; |
| |
| # End the row. |
| print OUT "</tr>\n"; |
| } |
| |
| print OUT "</tbody>\n</table>\n\n"; |
| } |
| |
| if ($Crashes) { |
| # Read the crash directory for files. |
| opendir(DIR, "$Dir/crashes"); |
| my @files = grep { /[.]info.txt$/ } readdir(DIR); |
| closedir(DIR); |
| |
| if (scalar(@files)) { |
| print OUT <<ENDTEXT; |
| <h2>Analyzer Failures</h2> |
| |
| <p>The analyzer had problems processing the following files:</p> |
| |
| <table> |
| <thead><tr><td>Problem</td><td>Source File</td><td>Preprocessed File</td><td>STDERR Output</td></tr></thead> |
| ENDTEXT |
| |
| foreach my $file (sort @files) { |
| $file =~ /(.+).info.txt$/; |
| # Get the preprocessed file. |
| my $ppfile = $1; |
| # Open the info file and get the name of the source file. |
| open (INFO, "$Dir/crashes/$file") or |
| die "Cannot open $Dir/crashes/$file\n"; |
| my $srcfile = <INFO>; |
| chomp $srcfile; |
| my $problem = <INFO>; |
| chomp $problem; |
| close (INFO); |
| # Print the information in the table. |
| my $prefix = GetPrefix(); |
| if (defined $prefix) { $srcfile =~ s/^\Q$prefix//; } |
| print OUT "<tr><td>$problem</td><td>$srcfile</td><td><a href=\"crashes/$ppfile\">$ppfile</a></td><td><a href=\"crashes/$ppfile.stderr.txt\">$ppfile.stderr.txt</a></td></tr>\n"; |
| my $ppfile_clang = $ppfile; |
| $ppfile_clang =~ s/[.](.+)$/.clang.$1/; |
| print OUT " <!-- REPORTPROBLEM src=\"$srcfile\" file=\"crashes/$ppfile\" clangfile=\"crashes/$ppfile_clang\" stderr=\"crashes/$ppfile.stderr.txt\" info=\"crashes/$ppfile.info.txt\" -->\n"; |
| } |
| |
| print OUT <<ENDTEXT; |
| </table> |
| <p>Please consider submitting preprocessed files as <a href="http://clang.llvm.org/StaticAnalysisUsage.html#filingbugs">bug reports</a>. <!-- REPORTCRASHES --> </p> |
| ENDTEXT |
| } |
| } |
| |
| print OUT "</body></html>\n"; |
| close(OUT); |
| CopyFiles($Dir); |
| |
| # Make sure $Dir and $BaseDir are world readable/executable. |
| system("chmod", "755", $Dir); |
| if (defined $BaseDir) { system("chmod", "755", $BaseDir); } |
| |
| my $Num = scalar(@Index); |
| Diag("$Num bugs found.\n"); |
| if ($Num > 0 && -r "$Dir/index.html") { |
| Diag("Run 'scan-view $Dir' to examine bug reports.\n"); |
| } |
| |
| DiagCrashes($Dir) if ($Crashes); |
| |
| return $Num; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # RunBuildCommand - Run the build command. |
| ##----------------------------------------------------------------------------## |
| |
| sub AddIfNotPresent { |
| my $Args = shift; |
| my $Arg = shift; |
| my $found = 0; |
| |
| foreach my $k (@$Args) { |
| if ($k eq $Arg) { |
| $found = 1; |
| last; |
| } |
| } |
| |
| if ($found == 0) { |
| push @$Args, $Arg; |
| } |
| } |
| |
| sub RunBuildCommand { |
| |
| my $Args = shift; |
| my $IgnoreErrors = shift; |
| my $Cmd = $Args->[0]; |
| my $CCAnalyzer = shift; |
| |
| # Get only the part of the command after the last '/'. |
| if ($Cmd =~ /\/([^\/]+)$/) { |
| $Cmd = $1; |
| } |
| |
| if ($Cmd eq "gcc" or $Cmd eq "cc" or $Cmd eq "llvm-gcc" |
| or $Cmd eq "ccc-analyzer") { |
| shift @$Args; |
| unshift @$Args, $CCAnalyzer; |
| } |
| elsif ($IgnoreErrors) { |
| if ($Cmd eq "make" or $Cmd eq "gmake") { |
| AddIfNotPresent($Args,"-k"); |
| AddIfNotPresent($Args,"-i"); |
| } |
| elsif ($Cmd eq "xcodebuild") { |
| AddIfNotPresent($Args,"-PBXBuildsContinueAfterErrors=YES"); |
| } |
| } |
| |
| if ($Cmd eq "xcodebuild") { |
| # Disable distributed builds for xcodebuild. |
| AddIfNotPresent($Args,"-nodistribute"); |
| |
| # Disable PCH files until clang supports them. |
| AddIfNotPresent($Args,"GCC_PRECOMPILE_PREFIX_HEADER=NO"); |
| |
| # When 'CC' is set, xcodebuild uses it to do all linking, even if we are |
| # linking C++ object files. Set 'LDPLUSPLUS' so that xcodebuild uses 'g++' |
| # when linking such files. |
| die if (!defined $CXX); |
| my $LDPLUSPLUS = `which $CXX`; |
| $LDPLUSPLUS =~ s/\015?\012//; # strip newlines |
| $ENV{'LDPLUSPLUS'} = $LDPLUSPLUS; |
| } |
| |
| return (system(@$Args) >> 8); |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # DisplayHelp - Utility function to display all help options. |
| ##----------------------------------------------------------------------------## |
| |
| sub DisplayHelp { |
| |
| print <<ENDTEXT; |
| USAGE: $Prog [options] <build command> [build options] |
| |
| ENDTEXT |
| |
| if (defined $BuildName) { |
| print "ANALYZER BUILD: $BuildName ($BuildDate)\n\n"; |
| } |
| |
| print <<ENDTEXT; |
| OPTIONS: |
| |
| -analyze-headers - Also analyze functions in #included files. |
| |
| -o - Target directory for HTML report files. Subdirectories |
| will be created as needed to represent separate "runs" of |
| the analyzer. If this option is not specified, a directory |
| is created in /tmp to store the reports. |
| |
| -h - Display this message. |
| --help |
| |
| -k - Add a "keep on going" option to the specified build command. |
| --keep-going This option currently supports make and xcodebuild. |
| This is a convenience option; one can specify this |
| behavior directly using build options. |
| |
| --html-title [title] - Specify the title used on generated HTML pages. |
| --html-title=[title] If not specified, a default title will be used. |
| |
| --status-bugs - By default, the exit status of $Prog is the same as the |
| executed build command. Specifying this option causes the |
| exit status of $Prog to be 1 if it found potential bugs |
| and 0 otherwise. |
| |
| --use-cc [compiler path] - By default, $Prog uses 'gcc' to compile and link |
| --use-cc=[compiler path] your C and Objective-C code. Use this option |
| to specify an alternate compiler. |
| |
| --use-c++ [compiler path] - By default, $Prog uses 'g++' to compile and link |
| --use-c++=[compiler path] your C++ and Objective-C++ code. Use this option |
| to specify an alternate compiler. |
| |
| -v - Verbose output from $Prog and the analyzer. |
| A second and third '-v' increases verbosity. |
| |
| -V - View analysis results in a web browser when the build |
| --view completes. |
| |
| |
| AVAILABLE ANALYSES (multiple analyses may be specified): |
| |
| ENDTEXT |
| |
| foreach my $Analysis (sort keys %AvailableAnalyses) { |
| if (defined $AnalysesDefaultEnabled{$Analysis}) { |
| print " (+)"; |
| } |
| else { |
| print " "; |
| } |
| |
| print " $Analysis $AvailableAnalyses{$Analysis}\n"; |
| } |
| |
| print <<ENDTEXT |
| |
| NOTE: "(+)" indicates that an analysis is enabled by default unless one |
| or more analysis options are specified |
| |
| BUILD OPTIONS |
| |
| You can specify any build option acceptable to the build command. |
| |
| EXAMPLE |
| |
| $Prog -o /tmp/myhtmldir make -j4 |
| |
| The above example causes analysis reports to be deposited into |
| a subdirectory of "/tmp/myhtmldir" and to run "make" with the "-j4" option. |
| A different subdirectory is created each time $Prog analyzes a project. |
| The analyzer should support most parallel builds, but not distributed builds. |
| |
| ENDTEXT |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # HtmlEscape - HTML entity encode characters that are special in HTML |
| ##----------------------------------------------------------------------------## |
| |
| sub HtmlEscape { |
| # copy argument to new variable so we don't clobber the original |
| my $arg = shift || ''; |
| my $tmp = $arg; |
| |
| $tmp =~ s/([\<\>\'\"])/sprintf("&#%02x;", chr($1))/ge; |
| |
| return $tmp; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # ShellEscape - backslash escape characters that are special to the shell |
| ##----------------------------------------------------------------------------## |
| |
| sub ShellEscape { |
| # copy argument to new variable so we don't clobber the original |
| my $arg = shift || ''; |
| my $tmp = $arg; |
| |
| $tmp =~ s/([\!\;\\\'\"\`\<\>\|\s\(\)\[\]\?\#\$\^\&\*\=])/\\$1/g; |
| |
| return $tmp; |
| } |
| |
| ##----------------------------------------------------------------------------## |
| # Process command-line arguments. |
| ##----------------------------------------------------------------------------## |
| |
| my $AnalyzeHeaders = 0; |
| my $HtmlDir; # Parent directory to store HTML files. |
| my $IgnoreErrors = 0; # Ignore build errors. |
| my $ViewResults = 0; # View results when the build terminates. |
| my $ExitStatusFoundBugs = 0; # Exit status reflects whether bugs were found |
| my @AnalysesToRun; |
| |
| if (!@ARGV) { |
| DisplayHelp(); |
| exit 1; |
| } |
| |
| while (@ARGV) { |
| |
| # Scan for options we recognize. |
| |
| my $arg = $ARGV[0]; |
| |
| if ($arg eq "-h" or $arg eq "--help") { |
| DisplayHelp(); |
| exit 0; |
| } |
| |
| if ($arg eq '-analyze-headers') { |
| shift @ARGV; |
| $AnalyzeHeaders = 1; |
| next; |
| } |
| |
| if (defined $AvailableAnalyses{$arg}) { |
| shift @ARGV; |
| push @AnalysesToRun, $arg; |
| next; |
| } |
| |
| if ($arg eq "-o") { |
| shift @ARGV; |
| |
| if (!@ARGV) { |
| DieDiag("'-o' option requires a target directory name.\n"); |
| } |
| |
| $HtmlDir = shift @ARGV; |
| next; |
| } |
| |
| if ($arg =~ /^--html-title(=(.+))?$/) { |
| shift @ARGV; |
| |
| if ($2 eq '') { |
| if (!@ARGV) { |
| DieDiag("'--html-title' option requires a string.\n"); |
| } |
| |
| $HtmlTitle = shift @ARGV; |
| } else { |
| $HtmlTitle = $2; |
| } |
| |
| next; |
| } |
| |
| if ($arg eq "-k" or $arg eq "--keep-going") { |
| shift @ARGV; |
| $IgnoreErrors = 1; |
| next; |
| } |
| |
| if ($arg =~ /^--use-cc(=(.+))?$/) { |
| shift @ARGV; |
| my $cc; |
| |
| if ($2 eq "") { |
| if (!@ARGV) { |
| DieDiag("'--use-cc' option requires a compiler executable name.\n"); |
| } |
| $cc = shift @ARGV; |
| } |
| else { |
| $cc = $2; |
| } |
| |
| $ENV{"CCC_CC"} = $cc; |
| next; |
| } |
| |
| if ($arg =~ /^--use-c\+\+(=(.+))?$/) { |
| shift @ARGV; |
| |
| if ($2 eq "") { |
| if (!@ARGV) { |
| DieDiag("'--use-c++' option requires a compiler executable name.\n"); |
| } |
| $CXX = shift @ARGV; |
| } |
| else { |
| $CXX = $2; |
| } |
| next; |
| } |
| |
| if ($arg eq "-v") { |
| shift @ARGV; |
| $Verbose++; |
| next; |
| } |
| |
| if ($arg eq "-V" or $arg eq "--view") { |
| shift @ARGV; |
| $ViewResults = 1; |
| next; |
| } |
| |
| if ($arg eq "--status-bugs") { |
| shift @ARGV; |
| $ExitStatusFoundBugs = 1; |
| next; |
| } |
| |
| DieDiag("unrecognized option '$arg'\n") if ($arg =~ /^-/); |
| |
| last; |
| } |
| |
| if (!@ARGV) { |
| Diag("No build command specified.\n\n"); |
| DisplayHelp(); |
| exit 1; |
| } |
| |
| $CmdArgs = HtmlEscape(join(' ', map(ShellEscape($_), @ARGV))); |
| $HtmlTitle = "${CurrentDirSuffix} - scan-build results" |
| unless (defined($HtmlTitle)); |
| |
| # Determine the output directory for the HTML reports. |
| my $BaseDir = $HtmlDir; |
| $HtmlDir = GetHTMLRunDir($HtmlDir); |
| |
| # Set the appropriate environment variables. |
| SetHtmlEnv(\@ARGV, $HtmlDir); |
| |
| my $Cmd = Cwd::realpath("$RealBin/ccc-analyzer"); |
| |
| DieDiag("Executable 'ccc-analyzer' does not exist at '$Cmd'\n") |
| if (! -x $Cmd); |
| |
| if (! -x $ClangSB) { |
| Diag("'clang' executable not found in '$RealBin'.\n"); |
| Diag("Using 'clang' from path.\n"); |
| } |
| |
| if (defined $CXX) { |
| $ENV{'CXX'} = $CXX; |
| } |
| else { |
| $CXX = 'g++'; # This variable is used by other parts of scan-build |
| # that need to know a default C++ compiler to fall back to. |
| } |
| |
| $ENV{'CC'} = $Cmd; |
| $ENV{'CLANG'} = $Clang; |
| |
| if ($Verbose >= 2) { |
| $ENV{'CCC_ANALYZER_VERBOSE'} = 1; |
| } |
| |
| if ($Verbose >= 3) { |
| $ENV{'CCC_ANALYZER_LOG'} = 1; |
| } |
| |
| if (scalar(@AnalysesToRun) == 0) { |
| foreach my $key (keys %AnalysesDefaultEnabled) { |
| push @AnalysesToRun,$key; |
| } |
| } |
| |
| if ($AnalyzeHeaders) { |
| push @AnalysesToRun,"-analyzer-opt-analyze-headers"; |
| } |
| |
| $ENV{'CCC_ANALYZER_ANALYSIS'} = join ' ',@AnalysesToRun; |
| |
| # Run the build. |
| my $ExitStatus = RunBuildCommand(\@ARGV, $IgnoreErrors, $Cmd); |
| |
| # Postprocess the HTML directory. |
| my $NumBugs = Postprocess($HtmlDir, $BaseDir); |
| |
| if ($ViewResults and -r "$HtmlDir/index.html") { |
| Diag "Analysis run complete.\n"; |
| Diag "Viewing analysis results in '$HtmlDir' using scan-view.\n"; |
| my $ScanView = Cwd::realpath("$RealBin/scan-view"); |
| if (! -x $ScanView) { $ScanView = "scan-view"; } |
| exec $ScanView, "$HtmlDir"; |
| } |
| |
| if ($ExitStatusFoundBugs) { |
| exit 1 if ($NumBugs > 0); |
| exit 0; |
| } |
| |
| exit $ExitStatus; |
| |