#! /usr/bin/perl
# prints summary of Linux kernel build errors/warnings
# TBD:
# allow "-sX" as well as "-s X";
# report filename from this kind of error:
#^ERROR: "__divdi3" [drivers/media/video/cx88/cx88xx.ko] undefined!

$VERSION = "v0.8";

# hash tables for errors/warnings per file name (filename is hash entry):
my %errors;
my %warnings;

my $errcount = 0;
my $warncount = 0;
my $errflag = 0;		# 1 = error, 2 = warning
my $files_err_or_warn = 0;
my $ld_locn = "";		# file ident if available
my $section_mismatches = 0;

my $infile = "";
my $line;
my $key;
my $totals_only = 0;
my $errs_only = 0;
my $sortby = 0;			# 0 = filename, 1 = num errors, 2 = num warnings
				# by  -s n,         -s e,           -s w

sub usage()
{
	print "buildsummary [options] <filenames>  {$VERSION}\n";
	print "options:     -s n        sort by filename [default]\n";
	print "             -s e        sort by error count\n";
	print "             -s w        sort by warnings count\n";
	print "             -t          print totals only\n";
	print "             -e          errors only, ignore warnings\n";
	exit 1;
}

sub clear_totals()
{
	$errcount = 0;
	$warncount = 0;
	$files_err_or_warn = 0;
	%errors = ();
	%warnings = ();
}

my @knowndirs = ('Documentation', 'arch', 'block', 'crypto',
	'drivers', 'firmware', 'fs', 'include', 'init', 'ipc',
	'kernel', 'lib', 'mm', 'net', 'samples', 'scripts',
	'security', 'sound', 'usr', 'virt');

sub is_known_dir($)
{
	my $dir = shift;

	###print "dbg: is_known_dir: checking :$dir:\n";
	foreach $kd (@knowndirs) {
		if ($kd eq $dir) {
			return 1;
		}
	}

	return 0;
}

sub shorter_key($)
{
	my $key = shift;
	my $part;

	###print "dbg: shorter_key: beg. key = $key;\n";
	while (1) {
		if (substr($key, 0, 1) eq '/') {
			$key = substr($key, 1);
		}
		if ($key =~ m!(.*?)/!) {	# not greedy
			$part = $1;
			if (is_known_dir($part)) {
				return $key;
			}

			$key =~ s!.*?/!!;	# truncate that top dir level
		}
		else {
			return $key;
		}
	}

	return $key;
}

my @ld_errors = (
		"(.*?):.*?: undefined reference",
		"\\(.*?\\+0x.*?\\): undefined reference",
		"(.*?):.*?: more undefined references",
		"(.*?):.*?: multiple definition"
		);

my @ld_warnings = (
		"(.*?): warning: changing start of section",
		"(.*?): warning: dot moved backwards before",
		"(.*?): warning: undefined reference to",
		"(.*?): warning: more undefined references to",
		"(.*?): warning: undefined reference to",
		"(.*?): warning: more undefined references to"
		);

sub is_ld_error($)
{
	my $line = shift;
	$ld_locn = "ld";

	for (my $ix = 0; $ix < int(@ld_errors); $ix++) {
		my $err = $ld_errors[$ix];
		if ($line =~ m/$err/) {
			##print STDERR "possible ld_locn=\'$1\'\n";
			$ld_locn = $1 unless $1 eq "";
			return 1;
		}
	}
	return 0;
}

sub is_ld_warning($)
{
	my $line = shift;
	$ld_locn = "ld";

	for (my $ix = 0; $ix < int(@ld_warnings); $ix++) {
		my $warn = $ld_warnings[$ix];
		if ($line =~ m/$warn/) {
			##print STDERR "possible ld_locn=\'$1\'\n";
			$ld_locn = $1 unless $1 eq "";
			return 1;
		}
	}
	return 0;
}

# main:

if (int(@ARGV) == 0 || $ARGV[0] eq "-h") {
	usage();
}

while ($#ARGV >= 0)
{
	$arg = shift;
	if (substr($arg, 0, 1) ne '-') {
		unshift(@ARGV, $arg);
		last;
	}

	if ($arg eq "-t") {
		$totals_only = 1;
	}
	elsif ($arg eq "-e") {
		$errs_only = 1;
	}
	elsif ($arg eq "-s") {		# sort by which
		my $which = shift;
		if ($which eq "n") {
			$sortby = 0;
		}
		elsif ($which eq "e") {
			$sortby = 1;
		}
		elsif ($which eq "w") {
			$sortby = 2;
		}
		else {
			print "invalid sort option: $which\n";
			usage();
		}
	}
	else {
		print "invalid option: $arg\n";
		usage();
	}
}

# don't allow $errs_only && $sortby == 2 (warnings)
if ($errs_only && $sortby == 2)
{
	die "cannot report errors only && sort by number of warnings\n";
}

foreach $infile (@ARGV)
{
	open (INFILE, $infile) or die "cannot open '$infile'\n";

LINE:
	while ($line = <INFILE>) {	# read lines from INFILE
		###$linenum++;
		chomp $line;
		###printf "dbg: line:$line\n";

		# lines that end with a backslash are a problem for (my) Perl,
		# so ignore them for now... change trailing backslash to space:
		my $len = length($line);
		if (substr($line, $len - 1, 1) eq '\\') {
			substr($line, $len - 1, 1) = ' ';
		}

		#!# if $line contains a '(' but no closing ')',
		# the regex bombs out, so ignore this check for now:
		if (0) {
		# if line begins with "$infile:", drop it;
		if ((qq/$line/ =~ m/\Q^.*$infile:\E/) ||
		    (m/\Q^.*$infile:\E/ =~ qq/$line/)) {
			$line =~ s/.*$infile://;
		}
		}

		if ($line =~ /^error:/i) {
			$key = "ld";
			$errcount++;
			$errflag = 1;
		}
		elsif ($line =~ /^warning:.*Section mismatch/i && !$errs_only) {
			$key = "ld";
			$section_mismatches++;
			$errflag = 2;
		}
		elsif ($line =~ /^warning:/i && !$errs_only) {
			$key = "ld";
			$warncount++;
			$errflag = 2;
		}
		elsif ($line =~ /(.*:).*error:/i) {
			$key = $1;
			$errcount++;
			$errflag = 1;
		}
		elsif ($line =~ /(.*:).*warning:/i && !$errs_only) {
			$key = $1;
			$warncount++;
			$errflag = 2;
		}
		# capture ld errors/warnings here:
		elsif (is_ld_warning($line)) {
			$key = $ld_locn;
			$warncount++;
			$errflag = 2;
		}
		elsif (is_ld_error($line)) {
			$key = $ld_locn;
			$errcount++;
			$errflag = 1;
		}
		else {
			next LINE;
		}

		# $key could contain filename:linenumber: but if so,
		# drop the ":linenumber:" parts of it
		if ($key =~ /(.*?:)(.*?:)/) {	# not greedy
			$key = $1;
			$key =~ s/:$//;
		}

		###print "dbg: found:key=$key; line=$line\n";

		if ($errors{$key} == 0 && $warnings{$key} == 0) {
			$files_err_or_warn++;
		}
		# add this error or warning to its hash entry
		if ($errflag == 1) {
			$errors{$key}++;
			$warnings{$key} += 0;
		}
		else {	# errflag=2/warning
			if (!$errs_only) {
				$warnings{$key}++;
				$errors{$key} += 0;
			}
		}
	} # end one infile
	close INFILE;

	if ($totals_only == 0) {

	if ($sortby == 0) {		# by filename
		foreach $key (sort keys %errors) {
			##$files_err_or_warn++;
			my $shortkey = shorter_key($key);
			print "file: $shortkey: errors: $errors{$key}, warnings: $warnings{$key}\n";
		}
	}
	elsif ($sortby == 1) {		# by number of errors
		foreach $key (sort { $errors{$b} <=> $errors{$a} } keys %errors) {
			##$files_err_or_warn++;
			my $shortkey = shorter_key($key);
			print "file: $shortkey: errors: $errors{$key}, warnings: $warnings{$key}\n";
		}
	}
	else {				# by number of warnings
		foreach $key (sort { $warnings{$b} <=> $warnings{$a} } keys %warnings) {
			##$files_err_or_warn++;
			my $shortkey = shorter_key($key);
			print "file: $shortkey: errors: $errors{$key}, warnings: $warnings{$key}\n";
		}
	}
	}

	print "$infile: totals: error/warning files: $files_err_or_warn, errors: $errcount, warnings: $warncount, Section mismatches: $section_mismatches\n";
	print "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n";

	clear_totals();

} # end for all infiles

# end buildsummary.pl;

