Support parallel build and install in on-device apk scripts.

Add -p flag to build.py to build native libs in parallel. Add -p
flag to install.py to install to multiple devices in parallel.

Change-Id: I042de5eae321e61455d330949c7f07577725ba15
diff --git a/android/scripts/build.py b/android/scripts/build.py
index 3e42371..1fb54e8 100644
--- a/android/scripts/build.py
+++ b/android/scripts/build.py
@@ -53,9 +53,8 @@
 	# Make build directory if necessary
 	if not os.path.exists(buildDir):
 		os.makedirs(buildDir)
-		os.chdir(buildDir)
 		toolchainFile = '%s/framework/delibs/cmake/toolchain-android-%s.cmake' % (deqpDir, common.ANDROID_NDK_TOOLCHAIN_VERSION)
-		common.execArgs([
+		common.execArgsInDirectory([
 				'cmake',
 				'-G%s' % common.CMAKE_GENERATOR,
 				'-DCMAKE_TOOLCHAIN_FILE=%s' % toolchainFile,
@@ -66,10 +65,9 @@
 				'-DCMAKE_BUILD_TYPE=%s' % buildType,
 				'-DDEQP_TARGET=android',
 				deqpDir
-			])
+			], buildDir)
 
-	os.chdir(buildDir)
-	common.execArgs(['cmake', '--build', '.'] + common.EXTRA_BUILD_ARGS)
+	common.execArgsInDirectory(['cmake', '--build', '.'] + common.EXTRA_BUILD_ARGS, buildDir)
 
 	if not os.path.exists(libsDir):
 		os.makedirs(libsDir)
@@ -138,7 +136,7 @@
 			'bin/dEQP-release.apk'
 		])
 
-def build (buildRoot=common.ANDROID_DIR, isRelease=False, nativeBuildType="Release", javaApi=common.ANDROID_JAVA_API):
+def build (buildRoot=common.ANDROID_DIR, isRelease=False, nativeBuildType="Release", javaApi=common.ANDROID_JAVA_API, doParallelBuild=False):
 	curDir = os.getcwd()
 
 	try:
@@ -159,8 +157,11 @@
 			shutil.rmtree(libTargetDir)
 
 		# Build native code
-		for lib in common.NATIVE_LIBS:
-			buildNative(buildRoot, libTargetDir, lib, nativeBuildType)
+		nativeBuildArgs = [(buildRoot, libTargetDir, nativeLib, nativeBuildType) for nativeLib in common.NATIVE_LIBS]
+		if doParallelBuild:
+			common.parallelApply(buildNative, nativeBuildArgs)
+		else:
+			common.serialApply(buildNative, nativeBuildArgs)
 
 		# Copy assets
 		if os.path.exists(assetsSrcDir):
@@ -186,10 +187,11 @@
 	parser.add_argument('--build-root', dest='buildRoot', default=common.ANDROID_DIR, help="Root directory for storing build results.")
 	parser.add_argument('--dump-config', dest='dumpConfig', action='store_true', help="Print out all configurations variables")
 	parser.add_argument('--java-api', dest='javaApi', default=common.ANDROID_JAVA_API, help="Set the API signature for the java build.")
+	parser.add_argument('-p', '--parallel-build', dest='parallelBuild', action="store_true", help="Build native libraries in parallel.")
 
 	args = parser.parse_args()
 
 	if args.dumpConfig:
 		dumpConfig()
 
-	build(buildRoot=os.path.abspath(args.buildRoot), isRelease=args.isRelease, nativeBuildType=args.nativeBuildType, javaApi=args.javaApi)
+	build(buildRoot=os.path.abspath(args.buildRoot), isRelease=args.isRelease, nativeBuildType=args.nativeBuildType, javaApi=args.javaApi, doParallelBuild=args.parallelBuild)
diff --git a/android/scripts/common.py b/android/scripts/common.py
index 674d818..d8e5b1b 100644
--- a/android/scripts/common.py
+++ b/android/scripts/common.py
@@ -8,6 +8,11 @@
 import multiprocessing
 import string
 
+try:
+	import threading
+except ImportError:
+	import dummy_threading as threading
+
 class NativeLib:
 	def __init__ (self, apiVersion, abiVersion):
 		self.apiVersion	= apiVersion
@@ -82,7 +87,43 @@
 	sys.stdout.flush()
 	retcode	= subprocess.call(args)
 	if retcode != 0:
- 		raise Exception("Failed to execute '%s', got %d" % (str(args), retcode))
+		raise Exception("Failed to execute '%s', got %d" % (str(args), retcode))
+
+def execArgsInDirectory (args, cwd):
+	# Make sure previous stdout prints have been written out.
+	sys.stdout.flush()
+	process = subprocess.Popen(args, cwd=cwd)
+	retcode = process.wait()
+	if retcode != 0:
+		raise Exception("Failed to execute '%s', got %d" % (str(args), retcode))
+
+def serialApply(f, argsList):
+	for args in argsList:
+		f(*args)
+
+def parallelApply(f, argsList):
+	class ErrorCode:
+		def __init__ (self):
+			self.error = None;
+
+	def applyAndCaptureError (func, args, errorCode):
+		try:
+			func(*args)
+		except:
+			errorCode.error = sys.exc_info()
+
+	errorCode = ErrorCode()
+	jobs = []
+	for args in argsList:
+		job = threading.Thread(target=applyAndCaptureError, args=(f, args, errorCode))
+		job.start()
+		jobs.append(job)
+
+	for job in jobs:
+		job.join()
+
+	if errorCode.error:
+		raise errorCode.error[0], errorCode.error[1], errorCode.error[2]
 
 class Device:
 	def __init__(self, serial, product, model, device):
@@ -99,7 +140,7 @@
 	(stdout, stderr) = proc.communicate()
 
 	if proc.returncode != 0:
-		raise Exception("adb devices -l failed, got %d" % retcode)
+		raise Exception("adb devices -l failed, got %d" % proc.returncode)
 
 	ptrn = re.compile(r'^([a-zA-Z0-9]+)\s+.*product:([^\s]+)\s+model:([^\s]+)\s+device:([^\s]+)')
 	devices = []
diff --git a/android/scripts/install.py b/android/scripts/install.py
index 2ef02cd..317dd84 100644
--- a/android/scripts/install.py
+++ b/android/scripts/install.py
@@ -7,43 +7,39 @@
 
 import common
 
-def install (extraArgs = []):
-	curDir = os.getcwd()
-	try:
-		os.chdir(common.ANDROID_DIR)
+def install (extraArgs = [], printPrefix=""):
+	print printPrefix + "Removing old dEQP Package...\n",
+	common.execArgsInDirectory([common.ADB_BIN] + extraArgs + [
+			'uninstall',
+			'com.drawelements.deqp'
+		], common.ANDROID_DIR)
+	print printPrefix + "Remove complete\n",
 
-		print "Removing old dEQP Package..."
-		common.execArgs([common.ADB_BIN] + extraArgs + [
-				'uninstall',
-				'com.drawelements.deqp'
-			])
-		print ""
+	print printPrefix + "Installing dEQP Package...\n",
+	common.execArgsInDirectory([common.ADB_BIN] + extraArgs + [
+			'install',
+			'-r',
+			'package/bin/dEQP-debug.apk'
+		], common.ANDROID_DIR)
+	print printPrefix + "Install complete\n",
 
-		print "Installing dEQP Package..."
-		common.execArgs([common.ADB_BIN] + extraArgs + [
-				'install',
-				'-r',
-				'package/bin/dEQP-debug.apk'
-			])
-		print ""
+def installToDevice (device, printPrefix=""):
+	print printPrefix + "Installing to %s (%s)...\n" % (device.serial, device.model),
+	install(['-s', device.serial], printPrefix)
 
-	finally:
-		# Restore working dir
-		os.chdir(curDir)
-
-def installToDevice (device):
-	print "Installing to %s (%s)..." % (device.serial, device.model)
-	install(['-s', device.serial])
-
-def installToAllDevices ():
+def installToAllDevices (doParallel):
 	devices = common.getDevices(common.ADB_BIN)
-	for device in devices:
-		installToDevice(device)
+	padLen = max([len(device.model) for device in devices])+1
+	if doParallel:
+		common.parallelApply(installToDevice, [(device, ("(%s):%s" % (device.model, ' ' * (padLen - len(device.model))))) for device in devices]);
+	else:
+		common.serialApply(installToDevice, [(device, ) for device in devices]);
 
 if __name__ == "__main__":
 	if len(sys.argv) > 1:
 		if sys.argv[1] == '-a':
-			installToAllDevices()
+			doParallel = '-p' in sys.argv[1:]
+			installToAllDevices(doParallel)
 		else:
 			install(sys.argv[1:])
 	else: