| #!/usr/bin/perl |
| |
| use strict; |
| use warnings; |
| |
| use File::Basename; |
| use File::Spec; |
| use File::Temp; |
| use POSIX; |
| |
| sub makeJob(\@$); |
| sub forkAndCompileFiles(\@$); |
| sub Exec($); |
| sub waitForChild(\@); |
| sub cleanup(\@); |
| |
| my $debug = 0; |
| |
| chomp(my $clexe = `cygpath -u '$ENV{'VS80COMNTOOLS'}/../../VC/bin/cl.exe'`); |
| |
| if ($debug) { |
| print STDERR "Received " . @ARGV . " arguments:\n"; |
| foreach my $arg (@ARGV) { |
| print STDERR "$arg\n"; |
| } |
| } |
| |
| my $commandFile; |
| foreach my $arg (@ARGV) { |
| if ($arg =~ /^[\/-](E|EP|P)$/) { |
| print STDERR "The invoking process wants preprocessed source, so let's hand off this whole command to the real cl.exe\n" if $debug; |
| Exec("\"$clexe\" \"" . join('" "', @ARGV) . "\""); |
| } elsif ($arg =~ /^@(.*)$/) { |
| chomp($commandFile = `cygpath -u '$1'`); |
| } |
| } |
| |
| die "No command file specified!" unless $commandFile; |
| die "Couldn't find $commandFile!" unless -f $commandFile; |
| |
| my @sources; |
| |
| open(COMMAND, '<:raw:encoding(UTF16-LE):crlf:utf8', $commandFile) or die "Couldn't open $commandFile!"; |
| |
| # The first line of the command file contains all the options to cl.exe plus the first (possibly quoted) filename |
| my $firstLine = <COMMAND>; |
| $firstLine =~ s/\r?\n$//; |
| |
| # To find the start of the first filename, look for either the last space on the line. |
| # If the filename is quoted, the last character on the line will be a quote, so look for the quote before that. |
| my $firstFileIndex; |
| print STDERR "Last character of first line = '" . substr($firstLine, -1, 1) . "'\n" if $debug; |
| if (substr($firstLine, -1, 1) eq '"') { |
| print STDERR "First file is quoted\n" if $debug; |
| $firstFileIndex = rindex($firstLine, '"', length($firstLine) - 2); |
| } else { |
| print STDERR "First file is NOT quoted\n" if $debug; |
| $firstFileIndex = rindex($firstLine, ' ') + 1; |
| } |
| |
| my $options = substr($firstLine, 0, $firstFileIndex) . join(' ', @ARGV[1 .. $#ARGV]); |
| my $possibleFirstFile = substr($firstLine, $firstFileIndex); |
| if ($possibleFirstFile =~ /\.(cpp|c)/) { |
| push(@sources, $possibleFirstFile); |
| } else { |
| $options .= " $possibleFirstFile"; |
| } |
| |
| print STDERR "######## Found options $options ##########\n" if $debug; |
| print STDERR "####### Found first source file $sources[0] ########\n" if @sources && $debug; |
| |
| # The rest of the lines of the command file just contain source files, one per line |
| while (my $source = <COMMAND>) { |
| chomp($source); |
| $source =~ s/^\s+//; |
| $source =~ s/\s+$//; |
| push(@sources, $source) if length($source); |
| } |
| close(COMMAND); |
| |
| my $numSources = @sources; |
| exit unless $numSources > 0; |
| |
| my $numJobs; |
| if ($options =~ s/-j\s*([0-9]+)//) { |
| $numJobs = $1; |
| } else { |
| chomp($numJobs = `num-cpus`); |
| } |
| |
| print STDERR "\n\n####### RUNNING AT MOST $numJobs PARALLEL INSTANCES OF cl.exe ###########\n\n";# if $debug; |
| |
| # Magic determination of job size |
| # The hope is that by splitting the source files up into 2*$numJobs pieces, we |
| # won't suffer too much if one job finishes much more quickly than another. |
| # However, we don't want to split it up too much due to cl.exe overhead, so set |
| # the minimum job size to 5. |
| my $jobSize = POSIX::ceil($numSources / (2 * $numJobs)); |
| $jobSize = $jobSize < 5 ? 5 : $jobSize; |
| |
| print STDERR "######## jobSize = $jobSize ##########\n" if $debug; |
| |
| # Sort the source files randomly so that we don't end up with big clumps of large files (aka SVG) |
| sub fisher_yates_shuffle(\@) |
| { |
| my ($array) = @_; |
| for (my $i = @{$array}; --$i; ) { |
| my $j = int(rand($i+1)); |
| next if $i == $j; |
| @{$array}[$i,$j] = @{$array}[$j,$i]; |
| } |
| } |
| |
| fisher_yates_shuffle(@sources); # permutes @array in place |
| |
| my @children; |
| my @tmpFiles; |
| my $status = 0; |
| while (@sources) { |
| while (@sources && @children < $numJobs) { |
| my $pid; |
| my $tmpFile; |
| my $job = makeJob(@sources, $jobSize); |
| ($pid, $tmpFile) = forkAndCompileFiles(@{$job}, $options); |
| |
| print STDERR "####### Spawned child with PID $pid and tmpFile $tmpFile ##########\n" if $debug; |
| push(@children, $pid); |
| push(@tmpFiles, $tmpFile); |
| } |
| |
| $status |= waitForChild(@children); |
| } |
| |
| while (@children) { |
| $status |= waitForChild(@children); |
| } |
| cleanup(@tmpFiles); |
| |
| exit WEXITSTATUS($status); |
| |
| |
| sub makeJob(\@$) |
| { |
| my ($files, $jobSize) = @_; |
| |
| my @job; |
| if (@{$files} > ($jobSize * 1.5)) { |
| @job = splice(@{$files}, -$jobSize); |
| } else { |
| # Compile all the remaining files in this job to avoid having a small job later |
| @job = splice(@{$files}); |
| } |
| |
| return \@job; |
| } |
| |
| sub forkAndCompileFiles(\@$) |
| { |
| print STDERR "######## forkAndCompileFiles()\n" if $debug; |
| my ($files, $options) = @_; |
| |
| if ($debug) { |
| foreach my $file (@{$files}) { |
| print STDERR "######## $file\n"; |
| } |
| } |
| |
| my (undef, $tmpFile) = File::Temp::tempfile('clcommandXXXXX', DIR => File::Spec->tmpdir, OPEN => 0); |
| |
| my $pid = fork(); |
| die "Fork failed" unless defined($pid); |
| |
| unless ($pid) { |
| # Child process |
| open(TMP, '>:raw:encoding(UTF16-LE):crlf:utf8', $tmpFile) or die "Couldn't open $tmpFile"; |
| print TMP "$options\n"; |
| foreach my $file (@{$files}) { |
| print TMP "$file\n"; |
| } |
| close(TMP); |
| |
| chomp(my $winTmpFile = `cygpath -m $tmpFile`); |
| Exec "\"$clexe\" \@\"$winTmpFile\""; |
| } else { |
| return ($pid, $tmpFile); |
| } |
| } |
| |
| sub Exec($) |
| { |
| my ($command) = @_; |
| |
| print STDERR "Exec($command)\n" if $debug; |
| |
| exec($command); |
| } |
| |
| sub waitForChild(\@) |
| { |
| my ($children) = @_; |
| |
| return unless @{$children}; |
| |
| my $deceased = wait(); |
| my $status = $?; |
| print STDERR "######## Child with PID $deceased finished ###########\n" if $debug; |
| for (my $i = 0; $i < @{$children}; $i++) { |
| if ($children->[$i] == $deceased) { |
| splice(@{$children}, $i, 1); |
| last; |
| } |
| } |
| |
| return $status; |
| } |
| |
| sub cleanup(\@) |
| { |
| my ($tmpFiles) = @_; |
| |
| foreach my $file (@{$tmpFiles}) { |
| unlink $file; |
| } |
| } |