#!/usr/bin/perl
# Sixgill: Static assertion checker for C/C++ programs.
# Copyright (C) 2009-2010 Stanford University
# Author: Brian Hackett
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your
option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more
details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <
http://www.gnu.org/licenses/>.
# do a complete run of the system from raw
source to reports. this requires
# various run_monitor processes to be running in the background (maybe on other
# machines) and watching a shared poll_file for jobs. if the
output directory
# for this
script already exists then an incremental analysis will be performed
# and the reports will only reflect the changes since the earlier run.
use strict;
use warnings;
use IO::Handle;
use File::Basename qw(basename dirname);
use Getopt::Long;
use Cwd;
#################################
# environment specific settings #
#################################
my $WORKDIR;
my $SIXGILL_BIN;
# poll file shared with the run_monitor
script.
my $poll_file;
# root directory of the project.
my $build_dir;
# directory containing gcc wrapper scripts.
my $wrap_dir;
# optional file with annotations from the web interface.
my $ann_file =
"";
# optional
output directory to do a diff against.
my $old_dir =
"";
# run in the foreground
my $foreground;
my $builder =
"make -j4";
my $suppress_logs;
GetOptions(
"build-root|b=s" => \$build_dir,
"poll-file=s" => \$poll_file,
"no-logs!" => \$suppress_logs,
"work-dir=s" => \$WORKDIR,
"sixgill-binaries|binaries|b=s" => \$SIXGILL_BIN,
"wrap-dir=s" => \$wrap_dir,
"annotations-file|annotations|a=s" => \$ann_file,
"old-dir|old=s" => \$old_dir,
"foreground!" => \$foreground,
"buildcommand=s" => \$builder,
)
or die;
if (not -d $build_dir) {
mkdir($build_dir);
}
if ($old_dir ne
"" && not -d $old_dir) {
die
"Old directory '$old_dir' does not exist\n";
}
$WORKDIR ||=
"sixgill-work";
mkdir($WORKDIR, 0755) if ! -d $WORKDIR;
$poll_file ||=
"$WORKDIR/poll.file";
$build_dir ||=
"$WORKDIR/js-inbound-xgill";
if (!defined $SIXGILL_BIN) {
chomp(my $path = `which xmanager`);
if ($path) {
use File::Basename qw(dirname);
$SIXGILL_BIN = dirname($path);
} else {
die
"Cannot find sixgill binaries. Use the -b option.";
}
}
$wrap_dir ||=
"$WORKDIR/xgill-inbound/wrap_gcc";
$wrap_dir =
"$SIXGILL_BIN/../scripts/wrap_gcc" if not (-e
"$wrap_dir/basecc");
die
"Bad wrapper directory: $wrap_dir" if not (-e
"$wrap_dir/basecc");
#
code to clean the project from $build_dir.
sub clean_project {
system(
"make clean");
}
#
code to build the project from $build_dir.
sub build_project {
return system($builder) >> 8;
}
our %kill_on_exit;
END {
for my $pid (keys %kill_on_exit) {
kill($pid);
}
}
# commands to start the various xgill binaries. timeouts can be specified
# for the backend analyses here, and a memory limit can be specified for
# xmanager if desired (and USE_COUNT_ALLOCATOR is defined in util/alloc.h).
my $xmanager =
"$SIXGILL_BIN/xmanager";
my $xsource =
"$SIXGILL_BIN/xsource";
my $xmemlocal =
"$SIXGILL_BIN/xmemlocal -timeout=20";
my $xinfer =
"$SIXGILL_BIN/xinfer -timeout=60";
my $xcheck =
"$SIXGILL_BIN/xcheck -timeout=30";
# prefix directory to strip off
source files.
my $prefix_dir = $build_dir;
##########################
# general purpose
script #
##########################
# Prevent ccache from being used. I don
't think this does any good. The problem
# I
'm struggling with is that if autoconf.mk still has 'ccache gcc
' in it, the
# builds fail in a mysterious way.
$ENV{CCACHE_COMPILERCHECK} =
'date +%s.%N';
delete $ENV{CCACHE_PREFIX};
my $usage =
"USAGE: run_complete result-dir\n";
my $result_dir = shift or die $usage;
if (not $foreground) {
my $pid = fork();
if ($pid != 0) {
print
"Forked, exiting...\n";
exit(0);
}
}
# if the result directory does not already exist,
mark for a clean build.
my $do_clean = 0;
if (not (-d $result_dir)) {
$do_clean = 1;
mkdir $result_dir;
}
if (!$suppress_logs) {
my $log_file =
"$result_dir/complete.log";
open(OUT,
">>", $log_file) or die
"append to $log_file: $!";
OUT->autoflush(1); # don
't buffer writes to the main log.
# redirect stdout and stderr to the log.
STDOUT->fdopen(\*OUT,
"w");
STDERR->fdopen(\*OUT,
"w");
}
# pids to wait on before exiting. these are collating worker
output.
my @waitpids;
chdir $result_dir;
# to do a partial run, comment out the commands here you don
't want to do.
my $status = run_build();
# end of run commands.
for my $pid (@waitpids) {
waitpid($pid, 0);
$status ||= $? >> 8;
}
print
"Exiting run_complete with status $status\n";
exit $status;
# get the IP
address which a freshly created manager is listening on.
sub get_manager_address
{
my $log_file = shift or die;
# give the manager one second to start, any longer and something
's broken.
sleep(1);
my $log_data = `cat $log_file`;
my ($port) = $log_data =~ /Listening on ([\.\:0-9]*)/
or die
"no manager found";
print OUT
"Connecting to manager on port $port\n" unless $suppress_logs;
print
"Connecting to manager on port $port.\n";
return $1;
}
sub logging_suffix {
my ($show_logs, $log_file) = @_;
return $show_logs ?
"2>&1 | tee $log_file"
:
"> $log_file 2>&1";
}
sub run_build
{
print
"build started: ";
print scalar(localtime());
print
"\n";
# fork off a process to run the build.
defined(my $pid = fork) or die;
# log file for the manager.
my $manager_log_file =
"$result_dir/build_manager.log";
if (!$pid) {
# this is the child process, fork another process to run a manager.
defined(my $pid = fork) or die;
my $logging = logging_suffix($suppress_logs, $manager_log_file);
exec(
"$xmanager -terminate-on-assert $logging") if (!$pid);
$kill_on_exit{$pid} = 1;
if (!$suppress_logs) {
# open new streams to redirect stdout and stderr.
open(LOGOUT,
"> $result_dir/build.log");
open(LOGERR,
"> $result_dir/build_err.log");
STDOUT->fdopen(\*LOGOUT,
"w");
STDERR->fdopen(\*LOGERR,
"w");
}
my $
address = get_manager_address($manager_log_file);
# write the configuration file for the wrapper
script.
my $config_file =
"$WORKDIR/xgill.config";
open(CONFIG,
">", $config_file) or die
"create $config_file: $!";
print CONFIG
"$prefix_dir\n";
print CONFIG Cwd::abs_path(
"$result_dir/build_xgill.log").
"\n";
print CONFIG
"$address\n";
my @extra = (
"-fplugin-arg-xgill-mangle=1");
push(@extra,
"-fplugin-arg-xgill-annfile=$ann_file")
if ($ann_file ne
"" && -e $ann_file);
print CONFIG join(
" ", @extra) .
"\n";
close(CONFIG);
# Tell the wrapper where to find the config
$ENV{
"XGILL_CONFIG"} = Cwd::abs_path($config_file);
# If overriding $CC, use GCCDIR to tell the wrapper scripts where the
# real compiler is. If $CC is not set, then the wrapper
script will
# search $PATH anyway.
if (exists $ENV{CC}) {
$ENV{GCCDIR} = dirname($ENV{CC});
}
# Force the wrapper scripts to be run in place of the compiler during
# whatever build process we use.
$ENV{CC} =
"$wrap_dir/" . basename($ENV{CC} //
"gcc");
$ENV{CXX} =
"$wrap_dir/" . basename($ENV{CXX} //
"g++");
# do the build, cleaning if necessary.
chdir $build_dir;
clean_project() if ($do_clean);
my $exit_status = build_project();
# signal the manager that it
's over.
system(
"$xsource -remote=$address -end-manager");
# wait for the manager to clean up and terminate.
print
"Waiting for manager to finish (build status $exit_status)...\n";
waitpid($pid, 0);
my $manager_status = $?;
delete $kill_on_exit{$pid};
# build is finished, the complete run can resume.
# return value only useful if --foreground
print
"Exiting with status " . ($manager_status || $exit_status) .
"\n";
exit($manager_status || $exit_status);
}
# this is the complete process, wait for the build to finish.
waitpid($pid, 0);
my $status = $? >> 8;
print
"build finished (status $status): ";
print scalar(localtime());
print
"\n";
return $status;
}
sub run_pass
{
my ($name, $
command) = @_;
my $log_file =
"$result_dir/manager.$name.log";
# extra commands to pass to the manager.
my $manager_extra =
"";
$manager_extra .=
"-modset-wait=10" if ($name eq
"xmemlocal");
# fork off a manager process for the analysis.
defined(my $pid = fork) or die;
my $logging = logging_suffix($suppress_logs, $log_file);
exec(
"$xmanager $manager_extra $logging") if (!$pid);
my $
address = get_manager_address($log_file);
# write the poll file for this pass.
if (! -d dirname($poll_file)) {
system(
"mkdir",
"-p", dirname($poll_file));
}
open(POLL,
"> $poll_file");
print POLL
"$command\n";
print POLL
"$result_dir/$name\n";
print POLL
"$address\n";
close(POLL);
print
"$name started: ";
print scalar(localtime());
print
"\n";
waitpid($pid, 0);
unlink($poll_file);
print
"$name finished: ";
print scalar(localtime());
print
"\n";
# collate the worker
's output into a single file. make this asynchronous
# so we can wait a bit and make sure we get all worker
output.
defined($pid = fork) or die;
if (!$pid) {
sleep(20);
exec(
"cat $name.*.log > $name.log");
}
push(@waitpids, $pid);
}
# the names of all directories containing reports to archive.
my $indexes;
sub run_index
{
my ($name, $kind) = @_;
return if (not (-e
"report_$kind.xdb"));
print
"$name started: ";
print scalar(localtime());
print
"\n";
# make an index for the report diff if applicable.
if ($old_dir ne
"") {
system(
"make_index $kind $old_dir > $name.diff.log");
system(
"mv $kind diff_$kind");
$indexes .=
" diff_$kind";
}
# make an index for the full set of reports.
system(
"make_index $kind > $name.log");
$indexes .=
" $kind";
print
"$name finished: ";
print scalar(localtime());
print
"\n";
}
sub archive_indexes
{
print
"archive started: ";
print scalar(localtime());
print
"\n";
system(
"tar -czf reports.tgz $indexes");
system(
"rm -rf $indexes");
print
"archive finished: ";
print scalar(localtime());
print
"\n";
}