#!/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####### COMPILING $numSources FILES USING 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; } }