1# -*- coding: utf-8 -*-
2
3#-------------------------------------------------------------------------
4# drawElements Quality Program utilities
5# --------------------------------------
6#
7# Copyright 2015 The Android Open Source Project
8#
9# Licensed under the Apache License, Version 2.0 (the "License");
10# you may not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12#
13#      http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS,
17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20#
21#-------------------------------------------------------------------------
22
23import os
24import re
25import sys
26import shlex
27import subprocess
28import multiprocessing
29import string
30
31try:
32	import threading
33except ImportError:
34	import dummy_threading as threading
35
36class NativeLib:
37	def __init__ (self, apiVersion, abiVersion, prebuiltDir):
38		self.apiVersion		= apiVersion
39		self.abiVersion		= abiVersion
40		self.prebuiltDir	= prebuiltDir
41
42	def __str__ (self):
43		return "(API: %s, ABI: %s)" % (self.apiVersion, self.abiVersion)
44
45	def __repr__ (self):
46		return "(API: %s, ABI: %s)" % (self.apiVersion, self.abiVersion)
47
48
49def getPlatform ():
50	if sys.platform.startswith('linux'):
51		return 'linux'
52	else:
53		return sys.platform
54
55def selectByOS (variants):
56	platform = getPlatform()
57	if platform in variants:
58		return variants[platform]
59	elif 'other' in variants:
60		return variants['other']
61	else:
62		raise Exception("No configuration for '%s'" % platform)
63
64def isExecutable (path):
65	return os.path.isfile(path) and os.access(path, os.X_OK)
66
67def which (binName):
68	for path in os.environ['PATH'].split(os.pathsep):
69		path = path.strip('"')
70		fullPath = os.path.join(path, binName)
71		if isExecutable(fullPath):
72			return fullPath
73
74	return None
75
76def isBinaryInPath (binName):
77	return which(binName) != None
78
79def selectFirstExistingBinary (filenames):
80	for filename in filenames:
81		if filename != None and isExecutable(filename):
82			return filename
83
84	return None
85
86def selectFirstExistingDir (paths):
87	for path in paths:
88		if path != None and os.path.isdir(path):
89			return path
90
91	return None
92
93def die (msg):
94	print msg
95	exit(-1)
96
97def shellquote(s):
98	return '"%s"' % s.replace('\\', '\\\\').replace('"', '\"').replace('$', '\$').replace('`', '\`')
99
100def execute (commandLine):
101	args	= shlex.split(commandLine)
102	retcode	= subprocess.call(args)
103	if retcode != 0:
104		raise Exception("Failed to execute '%s', got %d" % (commandLine, retcode))
105
106def execArgs (args):
107	# Make sure previous stdout prints have been written out.
108	sys.stdout.flush()
109	retcode	= subprocess.call(args)
110	if retcode != 0:
111		raise Exception("Failed to execute '%s', got %d" % (str(args), retcode))
112
113def execArgsInDirectory (args, cwd, linePrefix="", failOnNonZeroExit=True):
114
115	def readApplyPrefixAndPrint (source, prefix, sink):
116		while True:
117			line = source.readline()
118			if len(line) == 0: # EOF
119				break;
120			sink.write(prefix + line)
121
122	process = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
123	stdoutJob = threading.Thread(target=readApplyPrefixAndPrint, args=(process.stdout, linePrefix, sys.stdout))
124	stderrJob = threading.Thread(target=readApplyPrefixAndPrint, args=(process.stderr, linePrefix, sys.stderr))
125	stdoutJob.start()
126	stderrJob.start()
127	retcode = process.wait()
128	if failOnNonZeroExit and retcode != 0:
129		raise Exception("Failed to execute '%s', got %d" % (str(args), retcode))
130
131def serialApply(f, argsList):
132	for args in argsList:
133		f(*args)
134
135def parallelApply(f, argsList):
136	class ErrorCode:
137		def __init__ (self):
138			self.error = None;
139
140	def applyAndCaptureError (func, args, errorCode):
141		try:
142			func(*args)
143		except:
144			errorCode.error = sys.exc_info()
145
146	errorCode = ErrorCode()
147	jobs = []
148	for args in argsList:
149		job = threading.Thread(target=applyAndCaptureError, args=(f, args, errorCode))
150		job.start()
151		jobs.append(job)
152
153	for job in jobs:
154		job.join()
155
156	if errorCode.error:
157		raise errorCode.error[0], errorCode.error[1], errorCode.error[2]
158
159class Device:
160	def __init__(self, serial, product, model, device):
161		self.serial		= serial
162		self.product	= product
163		self.model		= model
164		self.device		= device
165
166	def __str__ (self):
167		return "%s: {product: %s, model: %s, device: %s}" % (self.serial, self.product, self.model, self.device)
168
169def getDevices (adb):
170	proc = subprocess.Popen([adb, 'devices', '-l'], stdout=subprocess.PIPE)
171	(stdout, stderr) = proc.communicate()
172
173	if proc.returncode != 0:
174		raise Exception("adb devices -l failed, got %d" % proc.returncode)
175
176	ptrn = re.compile(r'^([a-zA-Z0-9\.:]+)\s+.*product:([^\s]+)\s+model:([^\s]+)\s+device:([^\s]+)')
177	devices = []
178	for line in stdout.splitlines()[1:]:
179		if len(line.strip()) == 0:
180			continue
181
182		m = ptrn.match(line)
183		if m == None:
184			print "WARNING: Failed to parse device info '%s'" % line
185			continue
186
187		devices.append(Device(m.group(1), m.group(2), m.group(3), m.group(4)))
188
189	return devices
190
191def getWin32Generator ():
192	if which("jom.exe") != None:
193		return "NMake Makefiles JOM"
194	else:
195		return "NMake Makefiles"
196
197def isNinjaSupported ():
198	return which("ninja") != None
199
200def getUnixGenerator ():
201	if isNinjaSupported():
202		return "Ninja"
203	else:
204		return "Unix Makefiles"
205
206def getExtraBuildArgs (generator):
207	if generator == "Unix Makefiles":
208		return ["--", "-j%d" % multiprocessing.cpu_count()]
209	else:
210		return []
211
212NDK_HOST_OS_NAMES = [
213	"windows",
214	"windows-x86_64",
215	"darwin-x86",
216	"darwin-x86_64",
217	"linux-x86",
218	"linux-x86_64"
219]
220
221def getNDKHostOsName (ndkPath):
222	for name in NDK_HOST_OS_NAMES:
223		if os.path.exists(os.path.join(ndkPath, "prebuilt", name)):
224			return name
225
226	raise Exception("Couldn't determine NDK host OS")
227
228# deqp/android path
229ANDROID_DIR				= os.path.realpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
230
231# Build configuration
232NATIVE_LIBS				= [
233		#		  API		ABI				prebuiltsDir
234		NativeLib(13,		"armeabi-v7a",	'android-arm'),		# ARM v7a ABI
235		NativeLib(13,		"x86",			'android-x86'),		# x86
236		NativeLib(21,		"arm64-v8a",	'android-arm64'),	# ARM64 v8a ABI
237		NativeLib(21,		"x86_64",		'android-x86_64'),	# x86_64
238	]
239
240ANDROID_JAVA_API		= "android-22"
241NATIVE_LIB_NAME			= "libdeqp.so"
242
243def makeNdkVersionString (version):
244	minorVersionString = (chr(ord('a') + version[1]) if version[1] > 0 else "")
245	return "r%d%s" % (version[0], minorVersionString)
246
247ANDROID_NDK_VERSION			= (11,0)
248ANDROID_NDK_VERSION_STRING	= makeNdkVersionString(ANDROID_NDK_VERSION)
249def selectNDKPath ():
250	candidates =  [
251		os.path.expanduser("~/android-ndk-" + ANDROID_NDK_VERSION_STRING),
252		"C:/android/android-ndk-" + ANDROID_NDK_VERSION_STRING,
253		os.environ.get("ANDROID_NDK_PATH", None), # If not defined, return None
254	]
255
256	ndkPath = selectFirstExistingDir(candidates)
257
258	if ndkPath == None:
259		raise Exception("None of NDK directory candidates exist: %s. Check ANDROID_NDK_PATH in common.py" % candidates)
260
261	return ndkPath
262
263def noneSafePathJoin (*components):
264	if None in components:
265		return None
266	return os.path.join(*components)
267
268
269# NDK paths
270ANDROID_NDK_PATH				= selectNDKPath()
271ANDROID_NDK_HOST_OS				= getNDKHostOsName(ANDROID_NDK_PATH)
272ANDROID_NDK_TOOLCHAIN_VERSION	= "r11" # Toolchain file is selected based on this
273
274# Native code build settings
275CMAKE_GENERATOR			= selectByOS({
276		'win32':	getWin32Generator(),
277		'other':	getUnixGenerator()
278	})
279EXTRA_BUILD_ARGS		= getExtraBuildArgs(CMAKE_GENERATOR)
280
281# SDK paths
282ANDROID_SDK_PATH		= selectFirstExistingDir([
283		os.environ.get("ANDROID_SDK_PATH", None),
284		os.path.expanduser("~/android-sdk-linux"),
285		os.path.expanduser("~/android-sdk-mac_x86"),
286		"C:/android/android-sdk-windows",
287	])
288ANDROID_BIN				= selectFirstExistingBinary([
289		noneSafePathJoin(ANDROID_SDK_PATH, "tools", "android"),
290		noneSafePathJoin(ANDROID_SDK_PATH, "tools", "android.bat"),
291		which('android'),
292	])
293ADB_BIN					= selectFirstExistingBinary([
294		which('adb'), # \note Prefer adb in path to avoid version issues on dev machines
295		noneSafePathJoin(ANDROID_SDK_PATH, "platform-tools", "adb"),
296		noneSafePathJoin(ANDROID_SDK_PATH, "platform-tools", "adb.exe"),
297	])
298ZIPALIGN_BIN			= selectFirstExistingBinary([
299		noneSafePathJoin(ANDROID_SDK_PATH, "tools", "zipalign"),
300		noneSafePathJoin(ANDROID_SDK_PATH, "tools", "zipalign.exe"),
301		which('zipalign'),
302	])
303JARSIGNER_BIN			= which('jarsigner')
304
305# Apache ant
306ANT_BIN					= selectFirstExistingBinary([
307		which('ant'),
308		"C:/android/apache-ant-1.8.4/bin/ant.bat",
309		"C:/android/apache-ant-1.9.2/bin/ant.bat",
310		"C:/android/apache-ant-1.9.3/bin/ant.bat",
311		"C:/android/apache-ant-1.9.4/bin/ant.bat",
312	])
313
314def makeNameValueTuple (name):
315	return (name, str(eval(name)))
316
317CONFIG_VAR_NAMES = [
318		"ANDROID_DIR",
319		"NATIVE_LIBS",
320		"ANDROID_JAVA_API",
321		"NATIVE_LIB_NAME",
322		"ANDROID_NDK_PATH",
323		"ANDROID_NDK_HOST_OS",
324		"ANDROID_NDK_TOOLCHAIN_VERSION",
325		"CMAKE_GENERATOR",
326		"EXTRA_BUILD_ARGS",
327		"ANDROID_SDK_PATH",
328		"ANDROID_BIN",
329		"ADB_BIN",
330		"ZIPALIGN_BIN",
331		"JARSIGNER_BIN",
332		"ANT_BIN",
333	]
334CONFIG_STRINGS = [makeNameValueTuple(x) for x in CONFIG_VAR_NAMES]
335