build_package.pl
author Simon Howkins <simonh@symbian.org>
Fri, 11 Dec 2009 16:07:54 +0000
changeset 62 2797c7d55e8b
parent 61 cfb15e4d62a4
child 65 d0bd2fd2c468
permissions -rw-r--r--
Removed drivce space check for platform builds, as there's already one in the FBF anyway, and platform builds are not generally expected to run concurrently (which was the rationale for the check in the bootstrap.)

# Copyright (c) 2009 Symbian Foundation Ltd
# This component and the accompanying materials are made available
# under the terms of the License "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Symbian Foundation Ltd - initial contribution.
#
# Contributors:
#
# Description:
# This is a helper script which allocates unique drive letter and build number
# then starts a package build by running FBF bootstrap and build targets

use strict;

use Getopt::Long;
use File::Path;

my $sBOOTSTRAP_DIR="C:\\Apps\\FBF\\bootstrap";
my $sJOB_BASE_DIR="fbf_project";
my $nMAX_JOBDIR_AGE_SECONDS = 86400; # max number of seconds after which the letter is forcibly released
my $nLOCK_FILE_MAX_ATTEMPTS = 5;
my $sNUMBERS_FILE="\\\\v800020\\Publish\\SF_builds\\numbers.txt";
my $sLETTERS_FILE="letters.txt";
my $nMAX_LETTER_AGE_SECONDS = 86400; # max number of seconds after which the letter is forcibly released

my $sFbfProjectRepo = "\\\\bishare\\mercurial_development\\oss\\FCL\\interim\\fbf\\projects\\packages";
my $sFbfProjectDir = '';
my $sSubProject = '';
my $sSubprojVariant = '';
my $sSBSConfig = '';
#my $sSourcesFile = '';
#my $sModelFile = '';
my $sFbfConfigRepo="\\\\bishare\\mercurial_development\\oss\\FCL\\interim\\fbf\\configs\\default";
my $sFbfConfigDir = '';
my $nCmdLineNumber;
my $sDiamondsTag = '';
my $bHudson = 0;
my $bPublish = 1;
my %hHlmDefines = ();
my $bDisableAntiVirus = 0;
my $bHelp = 0;
GetOptions((
	'configrepo=s' => \$sFbfConfigRepo,
	'configdir=s' => \$sFbfConfigDir,
	'projectrepo=s' => \$sFbfProjectRepo,
	'projectdir=s' => \$sFbfProjectDir,
	'subproj=s' => \$sSubProject,
	'variant=s' => \$sSubprojVariant,
	'sbsconfig=s' => \$sSBSConfig,
	#'sources=s' => \$sSourcesFile,
	#'model=s' => \$sModelFile,
	'number=s' => \$nCmdLineNumber,
	'tag=s' => \$sDiamondsTag,
	'hudson!' => \$bHudson,
	'publish!' => \$bPublish,
	'define=s' => \%hHlmDefines,
	'disableav=s' => \$bDisableAntiVirus,
	'help!' => \$bHelp
));

if ($bHelp or !($sSubProject or $sFbfProjectRepo or $sFbfProjectDir))
{
	print "Usage: build_package.pl --subproj=RELPATH [OPTIONS]\n";
	print "       build_package.pl --projectrepo=REPO [OPTIONS]\n";
	print "where OPTIONS are:\n";
	print "\t--subproj=RELPATH Select subproject located at RELPATH (relative to the root of the project repository)\n";
	print "\t--variant=VARIANT If specified use sources_VARIANT.csv instead of sources.csv and add \"VARIANT\" as tag for this build\n";
	print "\t--sbsconfig=CONFIG Pass on CONFIG as configuration to SBS (can also be a comma separated list, e.g. 'armv5,winscw')\n";
	print "\t--projectrepo=REPO[#REV] Use repository REPO at revision REV for the project (instead of \\\\bishare\\mercurial_internal\\fbf\\projects\\packages)\n";
	print "\t--projectdir=DIR Use DIR location for the project (exclusive with --projectrepo).\n";
	#print "\t--sources=FILE ...\n";
	#print "\t--model=FILE ...\n";
	print "\t--configrepo=REPO[#REV] Use repository REPO at revision REV for the config (instead of \\\\bishare\\mercurial_internal\\fbf\\config\\default)\n";
	print "\t--configdir=DIR Use DIR location for the config (exclusive with --configrepo).\n";
	print "\t--number=N Force build number to N\n";
	print "\t--tag=TAG Apply Diamonds tag TAG to this build\n";
	print "\t--hudson Checks that there is at least NUMBER_OF_PROCESSORS X 10 GB available on the working drive\n";
	print "\t--nopublish Use \\numbers_test.txt for numbers and disable publishing\n";
	print "\t--define ATTRIBUTE=VALUE Pass -D statements to the Helium Framework\n";
	print "\t--disableav Disable Anti-Virus for the duration of the build (also sync with other concurrent package builds)\n";
	exit(0);
}

if ($sSubProject and $sSubProject !~ m,^([^/]+)/[^/]+/([^/]+)$,)
{
	print "ERROR: Option --subproj must be in the format codeline/layer/package (e.g. MCL/os/boardsupport)\n";
	exit(0);
}

#if (!$sFbfProjectRepo and !$sFbfProjectDir and (!$sSourcesFile or !$sModelFile))
#{
#	print "Error: If you don't provide --projectrepo or --projectdir then you have to provide both --sources and --model\n";
#	exit(0);
#}

my $sWORKING_DRIVE = find_working_drive();
print "Will use drive $sWORKING_DRIVE as working drive for this build\n";

if ($bHudson)
{
	my $nProcessors = $ENV{'NUMBER_OF_PROCESSORS'};
	my $diroutput = `dir /-C $sWORKING_DRIVE`;
	my $nBytesFree = 0;
	$nBytesFree = $1 if ($diroutput =~ /(\d+) bytes free/);
	my $nNeededSpace = 10*$nProcessors*1073741824;
	#print "Needed space is $nNeededSpace\n";
	if ($nBytesFree < $nNeededSpace)
	{
		print "ERROR: Available disk space on working drive ($nBytesFree bytes) is not enough to run a package build with Hudson.\n";
		exit(1);
	}
}

my $sFbfProjectRev = '';
if ($sFbfProjectRepo =~ m,(.*)#(.*),)
{
	$sFbfProjectRepo = $1;
	$sFbfProjectRev = $2;
}
my $sFbfConfigRev = '';
if ($sFbfConfigRepo =~ m,(.*)#(.*),)
{
	$sFbfConfigRepo = $1;
	$sFbfConfigRev = $2;
}

my $sHlmDefineOpt = '';
for (keys %hHlmDefines)
{
	$sHlmDefineOpt .= "-D$_=$hHlmDefines{$_} ";
}

my $sNoPublishOpt = "";
$sNoPublishOpt = "-Dsf.spec.publish.enable=false" if ( !$bPublish );
$sNUMBERS_FILE = "$sWORKING_DRIVE\\numbers_test.txt" if ( !$bPublish );

my $sJobLabel = 'job';
if ($sSubProject)
{
	$sSubProject =~ m,^([^/]+)/[^/]+/([^/]+)$,;
	$sJobLabel = $2;
}
elsif ($sFbfProjectRepo)
{
	$sFbfProjectRepo =~ m,(.*[\\/])?([^\\^/]+),;
	$sJobLabel = $2;
}
elsif ($sFbfProjectDir)
{
	$sFbfProjectDir =~ m,(.*[\\/])?([^\\^/]+),;
	$sJobLabel = $2;
}
#elsif ($sSourcesFile)
#{
#	$sSourcesFile =~ m,/(adaptation|app|mw|os|ostools|tools)[\\/]([^\\^/]+),i;
#	$sJobLabel = $2;
#	$sSourcesFile =~ m,(.*[\\/])?([^\\^/]+),;
#	$sJobLabel = $2 if (!$sJobLabel);
#}
mkdir("$sWORKING_DRIVE\\$sJOB_BASE_DIR") if (!-d "$sWORKING_DRIVE\\$sJOB_BASE_DIR");
my $sJobDir = mkdir_unique("$sWORKING_DRIVE\\$sJOB_BASE_DIR\\$sJobLabel");
print "Created project dir $sWORKING_DRIVE\\$sJOB_BASE_DIR\\$sJobDir\n";

print("cd $sBOOTSTRAP_DIR\n");
chdir("$sBOOTSTRAP_DIR");
print "###### BOOTSTRAP ######\n";
my $sConfigArg = "-Dsf.config.repo=$sFbfConfigRepo";
$sConfigArg .= " -Dsf.config.rev=$sFbfConfigRev" if ($sFbfConfigRev);
$sConfigArg = "-Dsf.config.dir=$sFbfConfigDir" if ($sFbfConfigDir);
my $sProjectArg = "-Dsf.project.repo=$sFbfProjectRepo";
$sProjectArg .= " -Dsf.project.rev=$sFbfProjectRev" if ($sFbfProjectRev);
$sProjectArg = "-Dsf.project.dir=$sFbfProjectDir" if ($sFbfProjectDir);
my $sBootstrapCmd = "hlm -f bootstrap.xml $sConfigArg $sProjectArg -Dsf.target.dir=$sJobDir";
print("$sBootstrapCmd\n");
system($sBootstrapCmd);

# check that $sNUMBERS_FILE exists, otherwise create it
if (!-f $sNUMBERS_FILE)
{
	open FILE, ">$sNUMBERS_FILE";
	print FILE "\n";
	close FILE;
}

my $sJobNumberKey = '';
my $sPackage = '';
my $sPlatform = '';
my $nUnformattedNumber = 0;
if ($nCmdLineNumber)
{
	$nUnformattedNumber = $nCmdLineNumber;
}
elsif ($sFbfProjectRepo)
{
	if ($sSubProject)
	{
		# key = <package>_<codeline>, e.g. for subproj=MCL/os/boardsupport -> key=boardsupport_MCL
		$sSubProject =~ m,^([^/]+)/[^/]+/([^/]+)$,;
		$sPackage = $2;
		$sPlatform = $1;
		$sJobNumberKey = "$2_$1";
	}
	else
	{
		# key = hash of the rev.0 of the package project repo
		my $sRevZeroHash = get_rev_zero_hash($sFbfProjectRepo);
		$sJobNumberKey = $sRevZeroHash;
	}
	$nUnformattedNumber = get_job_number($sJobNumberKey);
}
my $nJobNumber = sprintf("%.3d", $nUnformattedNumber);
print "For build key $sJobNumberKey got assigned number \"$nJobNumber\"\n";

# check that $sLETTERS_FILE exists, otherwise create it
if (!-f "$sWORKING_DRIVE\\$sLETTERS_FILE")
{
	open FILE, ">$sWORKING_DRIVE\\$sLETTERS_FILE";
	print FILE "\n";
	close FILE;
}

# acquire drive letter
my $sDriveLetter = acquire_drive_letter();
die "Could not acquire drive letter" if (!$sDriveLetter);
print "acquired drive letter: $sDriveLetter\n";

# disable antivirus:
# done after the acquisition of the drive letter as letter file
# is used as a means to sync with concurrent package builds
if ($bDisableAntiVirus)
{
	print "disabling anti-virus\n";
	my $disableav_cmd = "av.bat /stop";
	print "$disableav_cmd\n";
	system($disableav_cmd);
}

my $sJobRootDirArg = "-Dsf.spec.job.rootdir=$sWORKING_DRIVE\\fbf_job";

my $sSubProjArg = '';
$sSubProjArg = "-Dsf.subproject.path=$sSubProject" if ($sSubProject);
my $sVariantArg = '';
$sVariantArg = "-Dsf.spec.sourcesync.sourcespecfile=sources_$sSubprojVariant.csv" if ($sSubprojVariant);
my $sSBSConfigArg = '';
$sSBSConfigArg = "-Dsf.spec.sbs.config=\"$sSBSConfig\"" if ($sSBSConfig);
my $sAllTags = '';
$sAllTags = $sDiamondsTag if ($sDiamondsTag);
$sAllTags .= ',' if ($sAllTags and $sSubprojVariant);
$sAllTags .= $sSubprojVariant if ($sSubprojVariant);
my $sTagsArg = "";
$sTagsArg = "-Dsf.spec.publish.diamonds.tag=\"$sAllTags\"" if ($sAllTags);
print("cd $sJobDir\\sf-config\n");
chdir("$sJobDir\\sf-config");
print "###### BUILD PREPARATION ######\n";
my $sPreparationCmd = "hlm sf-prep -Dsf.project.type=package $sSubProjArg -Dsf.spec.job.number=$nJobNumber -Dsf.spec.job.drive=$sDriveLetter: $sTagsArg $sNoPublishOpt $sJobRootDirArg $sHlmDefineOpt $sVariantArg $sSBSConfigArg";
print("$sPreparationCmd\n");
system($sPreparationCmd);

print "###### EXECUTE BUILD ######\n";
my $sBuildallCmd = "hlm sf-build-all -Dsf.project.type=package $sSubProjArg -Dsf.spec.job.number=$nJobNumber -Dsf.spec.job.drive=$sDriveLetter: $sTagsArg $sNoPublishOpt $sJobRootDirArg $sHlmDefineOpt $sVariantArg $sSBSConfigArg";
print("$sBuildallCmd\n");
system($sBuildallCmd);

print("cd $sBOOTSTRAP_DIR\n");
chdir("$sBOOTSTRAP_DIR");

# release the drive letter and optionally re-enable the AV
release_drive_letter_and_reenable_av($sDriveLetter);
system("subst $sDriveLetter: /d"); # this is not required, but it's a good idea to keep things in order
print "drive letter $sDriveLetter released (and drive unsubsted)\n";

if ($bHudson)
{
	print "cleaning job directories...\n";
	if (-d "$sWORKING_DRIVE\\$sJOB_BASE_DIR\\$sJobLabel") # project dir
	{
		print "rmdir /S $sWORKING_DRIVE\\$sJOB_BASE_DIR\\$sJobLabel\n";
		system("rmdir /S /Q $sWORKING_DRIVE\\$sJOB_BASE_DIR\\$sJobLabel");
	}
	if (-d "$sWORKING_DRIVE\\fbf_job\\$sPackage\_$sPlatform.$nJobNumber") # build drive
	{
		print "rmdir /S $sWORKING_DRIVE\\fbf_job\\$sPackage\_$sPlatform.$nJobNumber\n";
		system("rmdir /S /Q $sWORKING_DRIVE\\fbf_job\\$sPackage\_$sPlatform.$nJobNumber");
	}
}

sub find_working_drive
{
	my @drive_list = ('E', 'G', 'D', 'C');
	
	for my $drive (@drive_list)
	{
		return "$drive:" if (-d "$drive:/");
	}
	
	die "Could not find suitable working drive.";
}

sub mkdir_unique
{
	my ($sBaseDir) = @_;
	
	# check that the path where the new dir must be created exists.
	$sBaseDir =~ m,(.*[\\/])?(.*),;
	mkpath($1) if ($1 && !-d $1);
	
	my $nI = 0;
	my $sNewDirName = "$sBaseDir";
	while(!mkdir($sNewDirName))
	{
		$nI++;
		$sNewDirName = "$sBaseDir.$nI";
	}
	
	return $sNewDirName;
}

sub get_rev_zero_hash
{
	my ($sFbfProjectRepo) = @_;
	
	my $sOutput = `hg -R $sFbfProjectRepo identify -r0`;
	
	# remove leading and trailing spaces
	$sOutput =~ s,^\s+,,;
	$sOutput =~ s,\s+$,,;
	
	# remove tags e.g. "1fc39a7e9d79 tip"
	$sOutput =~ s,([0-9a-z]+)\s+.*,$1,;
	
	return $sOutput;
}

sub get_job_number
{
	my ($sKey) = @_;
	
	$sKey=lc($sKey);
	
	my %hnNumbers = ();
	
	my $nAttempts = 0;
	my $bGotNumber = 0;
	do
	{
		open(FILE, "+<$sNUMBERS_FILE") or die("Can't open $sNUMBERS_FILE");
		if ( flock(FILE, 6) )
		{
			my $sLine;
			while ($sLine = <FILE>)
			{
				$hnNumbers{lc($1)} = $2 if ($sLine =~ m%(.*),(.*)%);
			}
			
			$hnNumbers{$sKey} = 0 if (! $hnNumbers{$sKey} );
			$hnNumbers{$sKey} = $hnNumbers{$sKey} + 1;
			
			seek(FILE, 0, 0);

			for my $sStr ( keys(%hnNumbers) )
			{
				print FILE "$sStr,$hnNumbers{$sStr}\n";
			}
			truncate(FILE,tell(FILE));
			
			$bGotNumber = 1;
		}
		else
		{
			$nAttempts ++;
			sleep(3);
		}
		close(FILE);
	}
	until ( $bGotNumber or $nAttempts == $nLOCK_FILE_MAX_ATTEMPTS );
	
	return $hnNumbers{$sKey};
}

sub acquire_drive_letter
{
	my %hsPidsAndTimestamps = ();
	
	my $sLetterToRelease = '';
	
	my $nAttempts = 0;
	my $bAcquired = 0;
	do
	{
		open(FILE, "+<$sWORKING_DRIVE\\$sLETTERS_FILE") or die("Can't open $sWORKING_DRIVE\\$sLETTERS_FILE");
		if ( flock(FILE, 6) )
		{
			my $sLine;
			while ($sLine = <FILE>)
			{
				if ($sLine =~ m%([^,]*),(.*)%)
				{
					my $sLetter=$1;
					my $sString=$2;
					
					$sString=~m%([^,]*),(.*)%;
					my $nPid=$1;
					my $nTimestamp=$2;
					
					if (time()-$nTimestamp<=$nMAX_LETTER_AGE_SECONDS or $nTimestamp == 0 or $nTimestamp == -1)
					{
						$hsPidsAndTimestamps{$sLetter} = $sString;
					}
					else
					{
						# lease has expired: unsubst drive letter and don't add to hash
						system("subst $sLetter: /d");
						print "forced release of letter: $sLetter (and drive unsubsted)\n";
					}
				}
			}
			
			for my $sNewLetter ('H'..'Y')
			{
				if (! $hsPidsAndTimestamps{$sNewLetter})
				{
					my $sTimestamp = time();
					$hsPidsAndTimestamps{$sNewLetter} = "$$,$sTimestamp";
					$sLetterToRelease = $sNewLetter;
					last;
				}
			}
			
			seek(FILE, 0, 0);

			for my $sLetter ( keys(%hsPidsAndTimestamps) )
			{
				print FILE "$sLetter,$hsPidsAndTimestamps{$sLetter}\n";
			}
			truncate(FILE,tell(FILE));
			
			$bAcquired = 1;
		}
		else
		{
			$nAttempts ++;
			sleep(3);
		}
		close(FILE);
	}
	until ( $bAcquired or $nAttempts == $nLOCK_FILE_MAX_ATTEMPTS );
	
	return $sLetterToRelease;
}

sub release_drive_letter_and_reenable_av
{
	my ($sLetterToRelease) = @_;
	
	my %hsPidsAndTimestamps = ();
	
	my $nAttempts = 0;
	my $bAcquired = 0;
	do
	{
		open(FILE, "+<$sWORKING_DRIVE\\$sLETTERS_FILE") or die("Can't open $sWORKING_DRIVE\\$sLETTERS_FILE");
		if ( flock(FILE, 6) )
		{
			my $sLine;
			while ($sLine = <FILE>)
			{
				$hsPidsAndTimestamps{$1} = $2 if ($sLine =~ m%([^,]*),(.*)%);
			}
			
			delete $hsPidsAndTimestamps{$sLetterToRelease};
			
			seek(FILE, 0, 0);
			
			# if there are no other builds at this time then enable back the antivirus
			my $nConcurrentBuilds = scalar(keys(%hsPidsAndTimestamps));
			if ($nConcurrentBuilds == 0)
			{
				if ($bDisableAntiVirus)
				{
					print "enabling anti-virus\n";
					my $enableav_cmd = "av.bat /start";
					print "$enableav_cmd\n";
					system($enableav_cmd);
				}
			}

			for my $sLetter ( keys(%hsPidsAndTimestamps) )
			{
				print FILE "$sLetter,$hsPidsAndTimestamps{$sLetter}\n";
			}
			truncate(FILE,tell(FILE));
			
			$bAcquired = 1;
		}
		else
		{
			$nAttempts ++;
			sleep(3);
		}
		close(FILE);
	}
	until ( $bAcquired or $nAttempts == $nLOCK_FILE_MAX_ATTEMPTS );
}