#########section from template follows!!!!!!
#Project attributes. Feel fee to edit them if you 
my $indir = $prjinfo{indir};
my $outdir = $prjinfo{outdir};#directory to put output to.
my $statedir = $prjinfo{stdir};#directory with state files that are safe to loose
	    #since they are automatically autogenerated
my $title = $prjinfo{title};#mostly not used
my $testcmd = $prjinfo{testcmdline};#command to run for testing
my ($precmd,$postcmd)  =($prjinfo{precmdline}, $prjinfo{postcmdline});

#host attributes
my $obfuspath = $prjinfo{obfuspath};
my $obfusbin = $prjinfo{obfusbin};


my $symfiles_subdir = 'list-of-symbols-for-obfuscator';
my $idents_from_html_fname = 'idents-from-html.txt';
my $mapping_filename = 'symbol-mapping.txt';
my $shortest_counts_fname = 'shortest-countsfile.txt';
my $shortest_state_fname = 'shortest-statefile.txt';
my $tmp_found_libs_fname = 'tmp-found-libs';
my @statefnames = ($idents_from_html_fname,$mapping_filename,qw(),
    $shortest_counts_fname, $shortest_state_fname);
my $filelist_fname = 'tmp-filelist.txt';
my $strinfinfo_dirname = 'stringinfo';



#subs to construct paths relative to indir/statedir/outdir
sub rpi { File::Spec->catfile($indir,$_[0]); }
sub rps { File::Spec->catfile($statedir,$_[0]); }
sub rpo { File::Spec->catfile($outdir,$_[0]); }
sub rpy {
    my %userfile_props = qw(
		main 	user-exceptions.txt 
		maini 	user-exceptions-nocase.txt	
    		anti    user-antiexceptions.txt
		mapping user-specified-mapping.txt
		suffix	user-suffixes-asis.txt);
    my $type = shift;
    my $nm = $userfile_props{$type};
    my $r = File::Spec->catfile($indir,"$symfiles_subdir/$nm");
    $r;
}

sub pq { "\"$_[0]\""; } #returns quoted pathname

my $filelist_full_fname = pq(rps($filelist_fname));
my $verbose;

my $plainperlpath = $^X || $ENV{STUNNIXWS_PERL_INTERP};

sub fill_unassigned_files
{
    #unknownfiles_handling
    my %files_assigned;
    {#fill %files_assigned
	foreach my $rl (values %modes_to_files)
	{
	    map {  $files_assigned{$_} = 1; } @$rl;
	};
    }
    return if !$prjinfo{unknownfiles_handling};
    my @modespec = @{$prjinfo{unknownfiles_handling}};    
    return if !@modespec;

    my $dir_was = getcwd();
    use POSIX qw(getcwd);
    chdir($indir);
    my @allfiles;
    _list_all_files(".", \@allfiles, 0);

    #now check all files found
    my @files_found;
    foreach my $pn (@allfiles)
    {
	$pn = substr($pn,1);

	next if $files_assigned{$pn};

	my $ext;
	$pn =~ m,[.]([^.]+)$,;
	$ext = $1 if length($1);
	next if !length($ext); #no extension
	my $mode_found;
	foreach(@modespec)
	{
	    my ($type,$pat,$mode) = @$_;
	    if ($type eq 'ext')
	    {
		my %exts = ($pat,1);
		map { $exts{$_} = 1} split(/\s+/,$pat);
		if ($exts{$ext} || $exts{".$ext"})
		{
		    $mode_found = $mode;
		    last;
		};
	    } elsif ($type eq 'rx') {
		$mode_found = $mode if ($pn =~ /$pat/);
	    };
	};
	if ($mode_found) {
	    my $mode = $mode_found;
    	    $modes_to_files{$mode} = [] if !$modes_to_files{$mode};
	    push @{$modes_to_files{$mode}}, $pn;
	    push @files_found, $pn;
	};
    };
    chdir $dir_was;
    @files_found;
}

my %excluded_dirs;
my @excluded_dirs_list = (qw(CVS .svn .git .hg));
@excluded_dirs{@excluded_dirs_list} = @excluded_dirs_list;


sub _list_all_files
{
    my ($dirnm,$listref, $depth) = @_;
    opendir(D,$dirnm);
    my @dirs;
    foreach (readdir(D)) {
	next if $_ eq '.' || $_ eq '..' || (($excluded_dirs{$_} ||
	    $_ eq 'list-of-symbols-for-obfuscator') && -d "$dirnm/$_");
	my $nm = "$dirnm/$_";
	push @dirs,$nm if -d $nm;
	push @$listref, $nm if -f $nm;
    };
    closedir(D);
    foreach (@dirs) { _list_all_files($_,$listref,$depth+1); }
};

sub run
{
    my ($flags,@args) = @_;
    @ARGV = @args;
    my ($op,$nostop); #from commandline
    $op = 'build';
    my @files;
    my $runcmderrcnt;
    {#specified on commandline
	use Getopt::Long;    
	my $help;
	return 1 if (!GetOptions('op=s',\$op, 'help', \$help,
	    'verbose+',\$verbose,	
	    'nostop',\$nostop,
	    #'force',\$force
	    ));
	my %op_ok;
	my @opnames_ok = qw(rebuildall rebuildall-and-test get-used-libraries
			build build-and-test clearout clearstate clearall);
	@op_ok{@opnames_ok} = @opnames_ok;
	if (!$op_ok{$op})
	{
	    _t("error: unknown value for '--op' encountered: $op");
	    return 1;
    	}
	if ($help)
	{
	    use Pod::Usage;
	    pod2usage(-verbose => 1);
	}
	@files = @ARGV;
    }
    if ($prjinfo{lang} eq 'perl' && $^O =~ /Win32/i)
    {
	#we have to compute path to perl.exe since $^X is typically "*/wperl.exe"
	$plainperlpath =~ s,([\\/])wperl.exe,\1perl.exe,i;
    };
    $plainperlpath = "\"$plainperlpath\" ";

    {
	my @added_files = fill_unassigned_files();
	if (@added_files)
	{
	    t("auto-added " . (0+@added_files) . " files to the project");
	    t2("their names are:\n " . join("\n",@added_files));
	};
    };
    chdir($indir);
    mkdir(rps($strinfinfo_dirname)); #ensure it exists before we do anything

    my @fnames = @files;# everything we should try to process
    if (!@fnames)
    {
        map { push @fnames, @$_ } values %modes_to_files;
    } else {
	#fix fnames passed
	@fnames = map {
	    substr($_,0,1) eq '/' ? $_ : "/$_";
	    } @fnames;
    }

    if ($op eq 'clearall' || $op eq 'clearout')
    {	
	t( 'trying to erase ' .  (0+@fnames)  . " output files");
	t2(' - with names [' . join(', ',@fnames) . ']');
	chdir($indir);
	unlink map { rpo($_) } @fnames;
	unlink map { 
	    rps(File::Spec->catfile($strinfinfo_dirname,$_)) 
	} @fnames;
	
    }
    if ($op eq 'clearall' || $op eq 'clearstate')
    {	
	my @fnames = @statefnames;
	t( 'trying to erase ' .  (0+@fnames)  . " state files");
	chdir($indir);
	unlink map { rps($_) } @fnames;
    }
    return 0 if index($op,'clear') == 0;

    chdir($indir);
    if ($op =~ /build/)
    {
	runcmd_other($precmd);
    }    
    my %fname_to_mode;#key is filename, value modeid; for files in question
    {#build %fname_to_mode
	my %fnames;
	@fnames{@fnames} = @fnames;
	foreach my $m (keys %modes_to_files)
	{	    
	    map { $fname_to_mode{$_} = $m if exists $fnames{$_} }  
		@{$modes_to_files{$m}};
	}
    }
    if ($op eq 'get-used-libraries')
    {
	#here we completely rebuild $fname_to_mode; %modes_to_files will never be touched
	$modes{_get_used_libraries} = {
		'title', 'special: get used libraries',
		'commandline', 		'',
		'handling_type',	'_protectcode',
		'nooutput',		1,
		'is_shortest_mangler' => 0,
                 'ident_extaction_type' => 'none'
	    };
	my %new_fname_to_mode;
	foreach my $fn (@fnames) 
	{ 
	    $new_fname_to_mode{$fn} = '_get_used_libraries' 
		if $modes{$fname_to_mode{$fn}}->{handling_type} eq '_protectcode';
	};
	%fname_to_mode = %new_fname_to_mode;
    }
    
    #create filter for comparing mtime of single file
    #initialize it with sub that marks all files as newer
    my $cb_grep_files_newer1 = sub { my ($f,$rlist) = @_; @$rlist; };
    if (index($op,'build')==0)
    {
	#set it to more inteligent filter that really checks mtime 
	$cb_grep_files_newer1 = sub { 
	    my ($f,$rlist) = @_;
	    my $fmtime = 0+(stat($f))[9];
	    grep { my $mt = 0+((stat(rpi($_)))[9]); $mt >= $fmtime; }
		 @$rlist;
	}
    }
    
    {#extract idents
	my @files_to_extr = grep { 
		length($modes{$fname_to_mode{$_}}->{ident_extaction_type}) &&
		($modes{$fname_to_mode{$_}}->{ident_extaction_type} ne 'none') } 
		keys %fname_to_mode;
	@files_to_extr = &$cb_grep_files_newer1(rps($idents_from_html_fname),
		    \@files_to_extr);	
	if (@files_to_extr)
	{
	    #we have some modes that require us to extact idents from html, do
	    #extraction now
	    chdir($indir);
	    my %files_to_extr;
	    map { $files_to_extr{$_} = $fname_to_mode{$_} }  @files_to_extr;
	    my $extrmode_to_files = group_by_key(\%files_to_extr);
	    while(my ($modenm,$filelist) = each %$extrmode_to_files)
	    {
		my $opts = $extractident_type_to_cmdline{
				$modes{$modenm}->{ident_extaction_type}}
			    . ' -l ' .  $filelist_full_fname;
		create_filelist(@$filelist);		
		my $cmdline = "$plainperlpath " . 
		    pq(File::Spec->rel2abs('get-idents-from-html.pl',$obfuspath)) .
		    " $opts  " .
		    rps($idents_from_html_fname);
		return t_out("no 'get-idents-from-html.pl' installed, aborting") 
		    if ! -r File::Spec->rel2abs('get-idents-from-html.pl',$obfuspath);
		t('extracting idents from ' . (0+@$filelist) . ' file(s) with options "' .
		    $opts . '"');
		t2(' - with names [' . join(', ',@$filelist) . ']');
		return 2 if ($runcmderrcnt+=runcmd($cmdline) && !$nostop);
	    }
	}
    }
    if ($op ne 'get-used-libraries')
    {#create directories
	chdir($indir);
	map { mkdir rpo($_); 
	    my @iprops = stat(rpi($_)); chmod($iprops[2],rpo($_)); } @alldirs;
    }
    my $num_modes_with_code = 0;
    my $num_modes_with_code_total = 0;#count of files that are in the project, irregarding
	    #whether they need to be rebuilt or not
    for(my $step=0;$step<2;++$step)
    {
	#process files. If using 'shortest' obfuscator, gather symbol counts
	#on step 0
	my $mode_to_files = group_by_key(\%fname_to_mode);
	my $check_mtimes = index($op,'build')==0;
	chdir($indir);
	while(my ($m,$files) = each %$mode_to_files)
	{
	    my @files_to_process;
	    foreach my $f (@$files)
	    {
		my ($inp,$outp) = (rpi($f),rpo($f));
		next if !-e $inp; #skip missing files
		my $skip = 0;
		if ($check_mtimes)
		{
		    my ($imt,$omt) = (0+(stat($inp))[9], 0+(stat($outp))[9]);
		    $skip = $omt > $imt;
		    t2($skip ? "will not rebuild '$f'" : "will rebuild '$f'");
		} else {
		    t2("rebuilding '$f' unconditionally");
		}
		$num_modes_with_code_total++;
		push @files_to_process, $f if !$skip;
	    }
    	    next if !@files_to_process;

            my @ALL_files_to_process = @files_to_process;
            my $max_files_per_run = 120;
	    $max_files_per_run = @ALL_files_to_process+0
		    if ($modes{$m}->{handling_type} ne '_protectcode');

           SPLITQUEUE:
            @files_to_process = splice(@ALL_files_to_process,0,$max_files_per_run);

	    create_filelist(@files_to_process);
	    my $cmdline = $plainperlpath .
		    pq(File::Spec->catfile($obfuspath,$obfusbin)) .
		    " $modes{$m}->{commandline} ";
	    return t_out("no '$obfusbin' installed, aborting") 
		    if ! -r File::Spec->rel2abs($obfusbin,$obfuspath);
	    t('rebuilding ' . (0+@files_to_process) . 
		    " file(s) with mode '$modes{$m}->{title}'")
		if ($step==1 || 
		    ($modes{$m}->{handling_type} eq '_protectcode' &&
		        $modes{$m}->{is_shortest_mangler}));

	    my $cbnm = "modeimpl$modes{$m}->{handling_type}";

	    my $cmdtail;
	    {   #compute $cmdtail

		if ($op ne 'get-used-libraries')
		{
		    my $cmd;
		    if (!($modes{$m}->{handling_type} eq '_protectcode' &&
			$modes{$m}->{is_shortest_mangler} && $step ==0)) {
		        $cmd .= " -d " . rps($mapping_filename) . ' ';
		    };
		    $cmd .= ' -x ' . rpy('main') if -f rpy('main');
		    $cmd .= ' -x ' . rps($idents_from_html_fname) 
					if -f rps($idents_from_html_fname);
		    $cmd .= ' -F ' . rpy('mapping') if -f rpy('mapping');
		    $cmd .= ' --excludeidentsfile-anycase ' . rpy('maini')
				if -f rpy('maini');
		    $cmd .= ' -X ' . rpy('anti') if -f rpy('anti');
		    $cmd .= ' --suffixes-asis-list ' . rpy('suffix')
				if -f rpy('suffix');
		    if ($prjinfo{exception_libs})
		    {
			foreach (keys %{$prjinfo{exception_libs}})
			{
			    $cmd .= " -x $_ ";
			}
		    }
		    if ($prjinfo{libs_used})
		    {
			foreach my $l (keys %{$prjinfo{libs_used}}) {
			    my $v = $prjinfo{libs_used}->{$l};
			    next if $v eq '-';
			    $cmd .= " --exceptions-for-library $v:$l ";
			}
		    };
		    $cmdtail = $cmd;
		} else {
		    $cmdtail = " --save-list-of-included-files " . rps($tmp_found_libs_fname);
		}
	    }
	    my $handle_files_by_one = 1;
	    if ($prjinfo{lang} ne 'perl')
	    {
		my $noout_part = $modes{$m}->{nooutput} ? "nooutput=1,":'';
		$cmdtail .= " -S multifile,outdir=$prjinfo{outdir},${noout_part}filelist=$filelist_full_fname ";
		$handle_files_by_one = 0;
	    };
	    if ($modes{$m}->{handling_type} eq '_protectcode' &&
		    $modes{$m}->{is_shortest_mangler})
	    {
		$cmdtail .= ' -i  shortest,countsfile=' .
			rps($shortest_counts_fname) . 
			' -i shortest,statefile=' . 
			      rps($shortest_state_fname);
	    }
	    if ($modes{$m}->{handling_type} eq '_protectcode' &&
		    $modes{$m}->{is_shortest_mangler} && $step ==0)
	    {
		$cmdtail .= ' -i  shortest,countsfile=' .
			rps($shortest_counts_fname) . 
			' -i shortest,statefile=' . 
			      rps($shortest_state_fname);
		if (!$handle_files_by_one)
		{
		    t("gathering symbol counts");
		    my $cmd = "$cmdline $cmdtail -i shortest,countupdate=1 " .
			    "  -S multifile,nooutput=1 ";
		    return 5 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
		} else {
		    foreach my $f (@files_to_process)
		    {
			my ($inp,$outp) = (rpi($f),rpo($f));
			my $cmd = $cmdline . " -o " . rps('/dummyfile') . " " . rpi($f) .
			    $cmdtail . " -i shortest,countupdate=1 ";
			t2("cmdline is $cmd");
			t("  gathering symbol counts in $f");
			return 3 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
		    };
		}	
	    }
	    goto SPLITQUEUE if ($step == 0 && @ALL_files_to_process);
	    next if $step ==0;
	    
	    ++$num_modes_with_code 
		    if $modes{$m}->{handling_type} eq '_protectcode';
	    #now process files
	    if (!$handle_files_by_one && 
		    $modes{$m}->{handling_type} eq '_protectcode')
	    {
		$cmdtail .=  ' --stringinfo-output-to=' .  rps($strinfinfo_dirname)
		    if $prjinfo{string_explorer};

		my $ident = '     ';
		t("rebuilding following files at once:\n$ident" . 
		    join("\n$ident",@files_to_process) );
		my $cmd = "$cmdline $cmdtail ";
		return 6 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
	    } else {	    
		foreach my $f (@files_to_process)
		{
		    my ($inp,$outp) = (rpi($f),rpo($f));
		    if ($modes{$m}->{handling_type} ne 
			    '_protectcode') {
			no strict "refs";
			&$cbnm($f,$m,$inp,$outp);
		    } else {
			$cmdtail .=  ' --stringinfo-output-to ' .  File::Spec->catfile(rps($strinfinfo_dirname), $f)
			    if $prjinfo{string_explorer};

			my $cmd = $cmdline . " -o " . rpo($f) . " " . rpi($f) .
			    $cmdtail;
			t2("cmdline is $cmd");
			t("  rebuilding $f");
			return 4 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
		    };
		}
	    }
	    goto SPLITQUEUE if @ALL_files_to_process;
	}
    }
    if (!$num_modes_with_code_total)
    {
	t(<<EOT);
WARNING: no files were treated as containing code!! Probably your files have
non-standard file extensions. Please go to Settings ~ "For files", select files
of same type, select corresponding processing mode in the dropdown list,
and click "Assign Processing Mode" button.
EOT
    };
    if (!$runcmderrcnt && $op =~ /build/)
    {
	runcmd_other($postcmd);
    }
    if (!$runcmderrcnt && index($op,'and-test')>0)
    {
	runcmd_other($testcmd);
    }
    0;
}

our $logbuf;

sub _t
{
    my (@strs) = @_;
    print STDERR join('',@strs);
}

sub t
{
    my (@strs) = @_;
    _t('',@strs,"\n");
}

sub t2
{
    my (@strs) = @_;
    _t('',@strs,"\n") if $verbose>=2;
}

sub t_out
{
    my (@strs) = @_;
    _t('',@strs);
}

sub td
{
    my (@strs) = @_;
    _t('',@strs,"\n");    
}

#returns hash, key is one values in $h, value is ref to list of keys in 
#source hash
sub group_by_key
{
    my ($h) = @_;
    my %o = ();
    while(my ($k,$v) = each %$h)
    {
	$o{$v} = [] if (!exists $o{$v});
	push @{$o{$v}}, $k;
    };
    \%o;
}

sub runcmd_other
{
    my ($cmd) = @_;
    runcmd($cmd) if $cmd =~ /\S/;
}

sub runcmd
{
    my ($cmd) = @_;
    open(SAVEERR,">&STDERR");
    my $logfile = rps('logfile');
    open(STDERR,">$logfile");
    system($cmd);
    my $st = $? << 8;
    open(STDERR,">&SAVEERR");
    if (-s $logfile)
    {
	open(F,"<$logfile");
	my @lines = <F>;
	if ($lines[0] =~ /syntax OK\s*$/ && $prjinfo{lang} eq 'perl')
	{
	    shift @lines;
	}	
	close(F);
	t_out(@lines) if @lines;
    }
    $st;
}

sub modeimpl_copyasis
{ 
    my ($relname,$modeid,$innm,$outnm) = @_; 
    _docopyfile($innm,$outnm);
};

sub modeimpl_dontcopy
{
    my ($relname,$modeid,$innm,$outnm) = @_; 
    #do nothing
}

sub _docopyfile
{
    my ($innm,$outnm) = @_;
    open(IN,"<$innm") || return;
    open(OUT,">$outnm") || return;
    binmode(IN);
    binmode(OUT);
    my ($len,$buf);
    while (($len = read(IN,$buf,16*1024)) != 0)
    {
	print OUT $buf;
	$buf = '';
    }
    close(IN);
    close(OUT);
    
    my @iprops = stat($innm);
    chmod($iprops[2],$outnm);
}

sub create_filelist
{
    my (@files_to_process) = @_;
    open(F1,">" . rps($filelist_fname));
    map { print F1 ".$_\n"; } @files_to_process;
    close F1; 
}

run({},@ARGV) if !$ENV{BS_DONT_INVOKE_RUN};


=pod 

=head1 NAME

buildscript.pl - script to reprocess all or only changed parts of the project
using one of Stunnix Obfuscators.

=head1 SYNOPSIS

perl B<buildscript.pl>
    S<[ B<--help> ]>
    S<[ B<--verbose ..>] [ B<--nostop> ] >
    S<[ B<--op> I<build>|I<build-and-test>|I<rebuildall-and-test>| >
    S<	        I<rebuildall>|I<clearout>|I<clearstate>|I<clearall>] >
    S<[ I<filenames...> ] >

=head1 DESCRIPTION

This is a perl script file that can be used to perform various operations
on the project created in Stunnix Obfuscator Project Manager. This script
can be recreated by running Stunnix Obfuscator Project Manager, then visiting
Project menu and clicking on the menuitem I<Export as buildscript> - you
will be prompted to enter name of file to save this script to.

In order to run this script, type I<perl buildscript.pl [arguments]>.

This script can be used to rebuild all files, rebuild only "dirty" files,
clear all output files and/or state files produced during build process - 
the operation performed is specified as the value of the I<op> commandline
option. It's possible to specify the set of files to apply 
operation 
to by passing space-separated list of file names (relative to the project 
"input" directory) after all commandline arguments. There are other switches 
to affect the behaviour.

This script is cross-platform, it will work correctly on any computer
provided that paths listed
in the script are correct for your system. It's possible to edit this file
by hand in order to tweak commandline options or set of files, provided
Perl language syntax is respected.

This script can be thought of as a replacement for Makefile for
I<make> utility, traditionally used for building large or complex projects.
It's recommended to generate such script rather than to write Makefile
instead since commandlines to be written are rather complex. So the 
recommended approach for people who prefer writing Makefiles for their 
projects is to create project in Stunnix Obfuscator Project Manager,
properly assign all options, export the buildscript and then use the
buildscript for building the project; if necessary, changes in the 
commandline options and adding set of files can be performed by editing
this script (though using Stunnix Obfuscator Project Manager would
still be more convenient for this task too).

If this script is invoked with empty commandline, all "dirty" output files 
(for which corresponding original files are more fresh than output files)
will be rebuilt.

Execution is aborted in case error occurs during processing of some 
file. It's possible to not abort processing in case of error by passing 
option I<--nostop>.

=head1 OPTIONS

=over 4

=item B<--op>

Specifies operation to be performed. Can be either of the following:

=over 2

=item I<build>

Process only those files, which were changed after their protected version
was built (or for which no protected version exists). This is the default
operation, it's performed when no I<--op> option was specified at all.

=item I<build-and-test>

Same as I<build>, but also runs B<test command> (specified in project options)
if processing all files was successful. It's useful to run some testsuite or
just navigate the browser to some page by using the test command.

=item I<rebuildall>

Processes all files in the project, without comparing the time of the
modification of input and output files.

=item I<rebuild-and-test>

Same as I<rebuild>, but also runs but B<test command> (specified in 
project options) if processing of all files was successful. 

=item I<clearout>

Clear all output files.

=item I<clearstate>

Clear all state files that were produced during building of the project -
such files are typically lists of exceptions extracted automatically and
mapping of original to protected symbols.

=item I<clearall>

Does everything that operations I<clearout> and I<clearstate>
do.

=back

=item B<--verbose>

Passing this option multiplie types increases level of verbosity of its
output.

=item B<--help>

Produces help on options the script supports.

=item B<--nostop>

Do not abort if some error occured while processing some file.

=back

=cut
