#!/usr/bin/env perl

# Copyright (C) 2011, 2012, 2013, 2014 Apple Inc. All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Filters the output of build-webkit into a more human-readable format.

use strict;
use warnings;

use CGI qw(escapeHTML);
use File::Basename;
use FindBin;
use lib $FindBin::Bin;
use Getopt::Long;
use VCSUtils;

use constant {
    STYLE_PLAIN => 0,
    STYLE_HEADER => 1,
    STYLE_SUCCESS => 2,
    STYLE_ALERT => 3,

    HTML_HEADER =><<HTMLHEADER,
<html>
    <head>
        <title>Build Log</title>
        <style>
            body { font-family: Monaco, monospace; font-size: 10px; color: #666; line-height: 1.5em; }
            h2 { margin: 1.5em 0 0 0; font-size: 1.0em; font-weight: bold; color: blue; }
            p { margin: 0; padding-left: 1.5em; border-left: 3px solid #fff; }
            p.alert { border-left-color: red; color: red; margin: 1.5em 0 0 0; }
            p.alert + p { margin: 1.5em 0 0 0; }
            p.alert + p.alert { margin: 0; }
            p.success { color: green; }
        </style>
    </head>
    <body>
HTMLHEADER

    HTML_FOOTER =><<HTMLFOOTER,
    </body>
</html>
HTMLFOOTER
};

sub printLine($$);
sub setLogfileOption($$);
sub setOutputFormatOption($$);
sub shouldIgnoreLine($$);
sub usageAndExit();

# Defined in VCSUtils.
sub possiblyColored($$);

# Global variables used only in global scope.
my $outputPath = "&STDOUT";
my $platform = "mac";
my $showHelp;

# Global variables used in global and subroutine scope.
our $logUnfilteredOutput;
our $outputFormat = "text";
our $unfilteredOutputPath = "build.log";
our $useColor = -t STDOUT;

sub usageAndExit()
{
    print STDERR <<__END__;
Usage: @{[ basename($0) ]} [options] buildlog1 [buildlog2 ...]
       build-webkit | @{[ basename($0) ]} [options]
  -h|--help     Show this help message
  -p|--platform Logfile type (default: $platform)
Output Options:
  -o|--output   Path for output (default: STDOUT)
  -f|--format   Output format (default: $outputFormat)
                  text: Plain text
                  html: Standalone HTML document
  --[no-]color  ANSI color output for text (default: on, if -o is STDOUT)
Unfiltered Logging Options:
  -l|--log      Save unfiltered output to file (see --log-file)
  --logfile     Path to save unfiltered output (implies --log, default: $unfilteredOutputPath)
__END__
    exit 1;
}

my $getOptionsResult = GetOptions(
    'h|help'                => \$showHelp,
    'o|output=s'            => \$outputPath,
    'p|platform=s'          => \$platform,
    'f|format=s'            => \&setOutputFormatOption,
    'color!'                => \$useColor,
    'l|log'                 => \$logUnfilteredOutput,
    'logfile=s'             => \&setLogfileOption,
);

if (-t STDIN || $showHelp || !$getOptionsResult) {
    usageAndExit();
}

open(OUTPUT_HANDLE, ">$outputPath") or die "Failed to open $outputPath : $!";
if ($logUnfilteredOutput) {
    open(UNFILTERED_OUTPUT_HANDLE, ">$unfilteredOutputPath") or die "Failed to open $unfilteredOutputPath : $!";
}

print OUTPUT_HANDLE HTML_HEADER if ($outputFormat eq "html");

my $buildFinished;
my $buildFailed = 0;
for (my $previousLine = "", my $line = <>; $line; $previousLine = $line, $line = <>) {
    print UNFILTERED_OUTPUT_HANDLE $line if $logUnfilteredOutput;

    chomp($line);

    next if shouldIgnoreLine($previousLine, $line);

    if ($line =~ /^={10}/) {
        printLine($line, STYLE_SUCCESS);
        $buildFinished = 1;
    } elsif ($line =~ /^===/) {
        printLine($line, STYLE_HEADER);
    } elsif ($line =~ /^note: using/) {
        printLine($line, STYLE_HEADER);
    } elsif ($line =~ /Checking Dependencies|Check dependencies|Create product structure|Write auxiliary files|LinkStoryboards/) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /\*\* BUILD SUCCEEDED \*\*/) {
        printLine("Build Succeeded", STYLE_SUCCESS);
    } elsif ($line =~ /^(\e\[1m)?(PhaseScriptExecution|RuleScriptExecution|ClCompile|CompileC|Distributed-CompileC|Ld|PBXCp|CpResource|CopyPNGFile|CopyTiffFile|CpHeader|Preprocess|Processing|ProcessInfoPlistFile|ProcessPCH|ProcessPCH\+\+|Touch|Libtool|CopyStringsFile|Mig|CreateUniversalBinary|Analyze|AnalyzeShallow|ProcessProductPackaging|CodeSign|Validate|SymLink|Updating|CompileDTraceScript|CompileXIB|StripNIB|CopyPlistFile|GenerateDSYMFile|GenerateTAPI|CompileStoryboard|ExternalBuildToolExecution)(\e\[0m)? ("[^"]+"|(\\|(?<=\\)\s|\S)+)?/) {
        my ($command, $path) = ($2, basename($4));
        $path =~ s/("|\\|\.[ah]$)//g;
        printLine("$command $path", STYLE_PLAIN);
    } elsif ($line =~ /^\S+mkdir .*?(\S+)$/) {
        my $path = basename($1);
        printLine("mkdir $path", STYLE_PLAIN);
    } elsif ($line =~ /^\S+\/usr\/bin\/tapi reexport .*?(\S+)$/) {
        my $path = basename($1);
        printLine("tapi $path", STYLE_PLAIN);
    } elsif ($line =~ /^plutil .*?(\S+)$/) {
        my $path = basename($1);
        printLine("plutil $path", STYLE_PLAIN);
    } elsif ($line =~ /^cp (\S+)/) {
        my $path = basename($1);
        printLine("cp $path", STYLE_PLAIN);
    } elsif ($line =~ /python (\S+\.py) (\S+)/) {
        my ($command, $path) = (basename($1), basename($2));
        printLine("python $command $path", STYLE_PLAIN);
    } elsif ($line =~ /^\/\S+?(strip|WebCoreExportFileGenerator) .*?(\/|\> )(\S+)/) {
        my ($command, $path) = (basename($1), basename($3));
        printLine("$command $path", STYLE_PLAIN);
    } elsif ($line =~ /^offlineasm\: /) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /^Generating bindings for the (\S+) builtin\./) {
        printLine("Generating $1 builtin", STYLE_PLAIN);
    } elsif ($line =~ /^Generating (bindings|messages? (header|receiver|dispatcher)|derived source) for (\S+)\.\.\./) {
        my ($command, $path) = ($1, basename($3));
        printLine("Generating $command $path", STYLE_PLAIN);
    } elsif ($line =~ /^(Generating|Merging) (\S+) (from|for) (\S+)/) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /^Postprocessed ANGLE header/) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /^Prepare build/) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /^GXCF:/) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /^Signing Identity:/) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /^Pre-processing (\S+) sandbox profile/) {
        printLine($line, STYLE_PLAIN);
    } elsif ($line =~ /^(\S+\/cc).*?(\S+)\.(out|exp)/) {
        my ($command, $path) = (basename($1), basename($2));
        printLine("$command $path", STYLE_PLAIN);
    } else {
        # This only gets hit if stderr is redirected to stdout.
        if (($line =~ /\*\* BUILD FAILED \*\*/) || ($line =~ /^Build FAILED./)) {
            $buildFailed = 1;
        }
        printLine($line, $buildFinished ? STYLE_SUCCESS : STYLE_ALERT);
    }
}

print OUTPUT_HANDLE HTML_FOOTER if ($outputFormat eq "html");

close(OUTPUT_HANDLE);
close(UNFILTERED_OUTPUT_HANDLE) if ($logUnfilteredOutput);

exit $buildFailed;

sub printLine($$)
{
    my ($line, $style) = @_;

    if ($outputFormat eq "html") {
        $line = escapeHTML($line);
        if    ($style == STYLE_HEADER)  { print OUTPUT_HANDLE "<h2>$line</h2>"; }
        elsif ($style == STYLE_SUCCESS) { print OUTPUT_HANDLE "<p class=\"success\">$line</p>"; }
        elsif ($style == STYLE_ALERT)   { print OUTPUT_HANDLE "<p class=\"alert\">$line</p>"; }
        else                            { print OUTPUT_HANDLE "<p>$line</p>"; }
    } else {
        if ($useColor) {
            my $colors = "reset";
            if ($style == STYLE_HEADER)  { $colors = "blue"; }
            if ($style == STYLE_SUCCESS) { $colors = "green"; }
            if ($style == STYLE_ALERT)   { $colors = "red"; }
            print OUTPUT_HANDLE possiblyColored($colors, $line);
        } else {
            print OUTPUT_HANDLE $line;
        }
    }
    print OUTPUT_HANDLE "\n";
}

sub setLogfileOption($$)
{
    my ($opt, $value) = @_;
    $unfilteredOutputPath = $value;
    $logUnfilteredOutput = 1;
}

sub setOutputFormatOption($$)
{
    my ($opt, $value) = @_;
    $value = lc($value);
    if ($value ne "html" && $value ne "text") {
        die "The $opt option must be either \"html\" or \"text\"";
    }
    $outputFormat = $value;
}

sub shouldShowSubsequentLine($)
{
    my ($line) = @_;

    return 1 if $line =~ /referenced from:$/;
    return 1 if $line =~ /(note:|error:)/;
    
    return 0;
}

sub shouldIgnoreLine($$)
{
    my ($previousLine, $line) = @_;

    return 1 if $line =~ /^\s*$/;
    return 1 if $line =~ /^Build settings from command line:/;
    return 1 if $line =~ /make(\[\d+\])?: Nothing to be done for `all'\./;
    return 1 if $line =~ /^JavaScriptCore\/create_hash_table/;
    return 1 if $line =~ /JavaScriptCore.framework\/PrivateHeaders\/create_hash_table/;
    return 1 if $line =~ /^JavaScriptCore\/pcre\/dftables/;
    return 1 if $line =~ /^Creating hashtable for /;
    return 1 if $line =~ /^Wrote output to /;
    return 1 if $line =~ /^UNDOCUMENTED: /;
    return 1 if $line =~ /libtool.*has no symbols/;
    return 1 if $line =~ /^# Lower case all the values, as CSS values are case-insensitive$/;
    return 1 if $line =~ /^if sort /;
    return 1 if $line =~ /set-webkit-configuration/;
    return 1 if $line =~ /^building file list/;
    return 1 if $line =~ /^\.\/$/;
    return 1 if $line =~ /^\S+\.h$/;
    return 1 if $line =~ /^\S+\/$/;
    return 1 if $line =~ /^sent \d+ bytes/;
    return 1 if $line =~ /^total size is/;
    return 1 if $line =~ /^\( (xcodebuild|if) /;
    return 1 if $line =~ /^warning\: detected internal install, passing entitlements to simulator anyway\./;
    return 1 if $line =~ /may not function in the Simulator because Ad Hoc/;
    return 1 if $line =~ /\/usr\/bin\/clang .*? \> \S+.sb/;
    return 1 if $line =~ / xcodebuild\[[0-9]+:[0-9a-f]+\]\s+DVTAssertions: Warning in .*XCClangResultsPostprocessor.m/;
    return 1 if $line =~ /^(Details|Object|Method|Function|Thread):/;
    return 1 if $line =~ /^Please file a bug at /;
    return 1 if $line =~ /created by an unsupported XCDependencyGraph build$/;

    if ($platform eq "win") {
        return 1 if $line =~ /^\s*(touch|perl|cat|rm -f|del|python|\/usr\/bin\/g\+\+|gperf|echo|sed|if \[ \-f|WebCore\/generate-export-file) /;
        return 1 if $line =~ /^\s*(if not exist \"|if errorlevel 1)/;
        return 1 if $line =~ /(^\s*|MSB3073:\s+)(set |REM |cmd \/c)/;
        return 1 if $line =~ /^\s*[cC]:\\[pP]rogram [fF]iles.*\\.*\\(CL|midl)\.exe /;
        return 1 if $line =~ /^\s*Processing .*\.(acf|h|idl)\s*$/;
        return 1 if $line =~ /^\s*printf /;
        return 1 if $line =~ /^\s*\/usr\/bin\/bash\s*/;
        return 1 if $line =~ /^\s*offlineasm: Nothing changed/;
        return 1 if $line =~ / \d+ File\(s\) copied/;
        return 1 if $line =~ /^\s*File not found - \*\.h/;
        return 1 if $line =~ /mkdir\s+\"/;
        return 1 if $line =~ /xcopy \/y \/d \"/;
        return 1 if $line =~ /\.obj\"\s*$/;
        return 1 if $line =~ /:\s+(cmd \/c|set)\s+/;
        return 1 if $line =~ /MSB3073:\s+$/;
        return 1 if $line =~ /MSB3073:\s+if not exist/;
        return 1 if $line =~ /which.exe bash/;
    } else {
        return 1 if $line =~ /^(touch|perl|cat|rm -f|python|\/usr\/bin\/g\+\+|\/bin\/ln|gperf|echo|sed|if \[ \-f|WebCore\/generate-export-file|write-file|chmod) /;
        return 1 if $line =~ /^    / && !shouldShowSubsequentLine($previousLine);
        return 1 if $line =~ /^printf /;
        return 1 if $line =~ /^offlineasm: Nothing changed/;
    }
    return 1 if $line =~ /^Showing first/;

    return 0;
}
