• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2
3#-------------------------------------------------------------------------
4# drawElements Quality Program utilities
5# --------------------------------------
6#
7# Copyright 2017 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
23# \todo [2017-04-10 pyry]
24# * Use smarter asset copy in main build
25#   * cmake -E copy_directory doesn't copy timestamps which will cause
26#     assets to be always re-packaged
27# * Consider adding an option for downloading SDK & NDK
28
29import os
30import re
31import sys
32import glob
33import string
34import shutil
35import argparse
36import tempfile
37import xml.etree.ElementTree
38
39# Import from <root>/scripts
40sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
41
42from build.common import *
43from build.config import *
44from build.build import *
45
46class SDKEnv:
47	def __init__(self, path):
48		self.path				= path
49		self.buildToolsVersion	= SDKEnv.selectBuildToolsVersion(self.path)
50
51	@staticmethod
52	def getBuildToolsVersions (path):
53		buildToolsPath	= os.path.join(path, "build-tools")
54		versions		= []
55
56		if os.path.exists(buildToolsPath):
57			for item in os.listdir(buildToolsPath):
58				m = re.match(r'^([0-9]+)\.([0-9]+)\.([0-9]+)$', item)
59				if m != None:
60					versions.append((int(m.group(1)), int(m.group(2)), int(m.group(3))))
61
62		return versions
63
64	@staticmethod
65	def selectBuildToolsVersion (path):
66		preferred	= [(25, 0, 2)]
67		versions	= SDKEnv.getBuildToolsVersions(path)
68
69		if len(versions) == 0:
70			return (0,0,0)
71
72		for candidate in preferred:
73			if candidate in versions:
74				return candidate
75
76		# Pick newest
77		versions.sort()
78		return versions[-1]
79
80	def getPlatformLibrary (self, apiVersion):
81		return os.path.join(self.path, "platforms", "android-%d" % apiVersion, "android.jar")
82
83	def getBuildToolsPath (self):
84		return os.path.join(self.path, "build-tools", "%d.%d.%d" % self.buildToolsVersion)
85
86class NDKEnv:
87	def __init__(self, path):
88		self.path		= path
89		self.version	= NDKEnv.detectVersion(self.path)
90		self.hostOsName	= NDKEnv.detectHostOsName(self.path)
91
92	@staticmethod
93	def getKnownAbis ():
94		return ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]
95
96	@staticmethod
97	def getAbiPrebuiltsName (abiName):
98		prebuilts = {
99			"armeabi-v7a":	'android-arm',
100			"arm64-v8a":	'android-arm64',
101			"x86":			'android-x86',
102			"x86_64":		'android-x86_64',
103		}
104
105		if not abiName in prebuilts:
106			raise Exception("Unknown ABI: " + abiName)
107
108		return prebuilts[abiName]
109
110	@staticmethod
111	def detectVersion (path):
112		propFilePath = os.path.join(path, "source.properties")
113		try:
114			with open(propFilePath) as propFile:
115				for line in propFile:
116					keyValue = map(lambda x: string.strip(x), line.split("="))
117					if keyValue[0] == "Pkg.Revision":
118						versionParts = keyValue[1].split(".")
119						return tuple(map(int, versionParts[0:2]))
120		except Exception as e:
121			raise Exception("Failed to read source prop file '%s': %s" % (propFilePath, str(e)))
122		except:
123			raise Exception("Failed to read source prop file '%s': unkown error")
124
125		raise Exception("Failed to detect NDK version (does %s/source.properties have Pkg.Revision?)" % path)
126
127	@staticmethod
128	def isHostOsSupported (hostOsName):
129		os			= HostInfo.getOs()
130		bits		= HostInfo.getArchBits()
131		hostOsParts	= hostOsName.split('-')
132
133		if len(hostOsParts) > 1:
134			assert(len(hostOsParts) == 2)
135			assert(hostOsParts[1] == "x86_64")
136
137			if bits != 64:
138				return False
139
140		if os == HostInfo.OS_WINDOWS:
141			return hostOsParts[0] == 'windows'
142		elif os == HostInfo.OS_LINUX:
143			return hostOsParts[0] == 'linux'
144		elif os == HostInfo.OS_OSX:
145			return hostOsParts[0] == 'darwin'
146		else:
147			raise Exception("Unhandled HostInfo.getOs() '%d'" % os)
148
149	@staticmethod
150	def detectHostOsName (path):
151		hostOsNames = [
152			"windows",
153			"windows-x86_64",
154			"darwin-x86",
155			"darwin-x86_64",
156			"linux-x86",
157			"linux-x86_64"
158		]
159
160		for name in hostOsNames:
161			if os.path.exists(os.path.join(path, "prebuilt", name)):
162				return name
163
164		raise Exception("Failed to determine NDK host OS")
165
166class Environment:
167	def __init__(self, sdk, ndk):
168		self.sdk		= sdk
169		self.ndk		= ndk
170
171class Configuration:
172	def __init__(self, env, buildPath, abis, nativeApi, nativeBuildType, gtfTarget, verbose, layers, angle):
173		self.env				= env
174		self.sourcePath			= DEQP_DIR
175		self.buildPath			= buildPath
176		self.abis				= abis
177		self.nativeApi			= nativeApi
178		self.javaApi			= 22
179		self.nativeBuildType	= nativeBuildType
180		self.gtfTarget			= gtfTarget
181		self.verbose			= verbose
182		self.layers				= layers
183		self.angle				= angle
184		self.cmakeGenerator		= selectFirstAvailableGenerator([NINJA_GENERATOR, MAKEFILE_GENERATOR, NMAKE_GENERATOR])
185
186	def check (self):
187		if self.cmakeGenerator == None:
188			raise Exception("Failed to find build tools for CMake")
189
190		if not os.path.exists(self.env.ndk.path):
191			raise Exception("Android NDK not found at %s" % self.env.ndk.path)
192
193		if not NDKEnv.isHostOsSupported(self.env.ndk.hostOsName):
194			raise Exception("NDK '%s' is not supported on this machine" % self.env.ndk.hostOsName)
195
196		if self.env.ndk.version[0] < 15:
197			raise Exception("Android NDK version %d is not supported; build requires NDK version >= 15" % (self.env.ndk.version[0]))
198
199		if self.env.sdk.buildToolsVersion == (0,0,0):
200			raise Exception("No build tools directory found at %s" % os.path.join(self.env.sdk.path, "build-tools"))
201
202		androidBuildTools = ["aapt", "zipalign", "dx"]
203		for tool in androidBuildTools:
204			if which(tool, [self.env.sdk.getBuildToolsPath()]) == None:
205				raise Exception("Missing Android build tool: %s" % toolPath)
206
207		requiredToolsInPath = ["javac", "jar", "jarsigner", "keytool"]
208		for tool in requiredToolsInPath:
209			if which(tool) == None:
210				raise Exception("%s not in PATH" % tool)
211
212def log (config, msg):
213	if config.verbose:
214		print msg
215
216def executeAndLog (config, args):
217	if config.verbose:
218		print " ".join(args)
219	execute(args)
220
221# Path components
222
223class ResolvablePathComponent:
224	def __init__ (self):
225		pass
226
227class SourceRoot (ResolvablePathComponent):
228	def resolve (self, config):
229		return config.sourcePath
230
231class BuildRoot (ResolvablePathComponent):
232	def resolve (self, config):
233		return config.buildPath
234
235class NativeBuildPath (ResolvablePathComponent):
236	def __init__ (self, abiName):
237		self.abiName = abiName
238
239	def resolve (self, config):
240		return getNativeBuildPath(config, self.abiName)
241
242class GeneratedResSourcePath (ResolvablePathComponent):
243	def __init__ (self, package):
244		self.package = package
245
246	def resolve (self, config):
247		packageComps	= self.package.getPackageName(config).split('.')
248		packageDir		= os.path.join(*packageComps)
249
250		return os.path.join(config.buildPath, self.package.getAppDirName(), "src", packageDir, "R.java")
251
252def resolvePath (config, path):
253	resolvedComps = []
254
255	for component in path:
256		if isinstance(component, ResolvablePathComponent):
257			resolvedComps.append(component.resolve(config))
258		else:
259			resolvedComps.append(str(component))
260
261	return os.path.join(*resolvedComps)
262
263def resolvePaths (config, paths):
264	return list(map(lambda p: resolvePath(config, p), paths))
265
266class BuildStep:
267	def __init__ (self):
268		pass
269
270	def getInputs (self):
271		return []
272
273	def getOutputs (self):
274		return []
275
276	@staticmethod
277	def expandPathsToFiles (paths):
278		"""
279		Expand mixed list of file and directory paths into a flattened list
280		of files. Any non-existent input paths are preserved as is.
281		"""
282
283		def getFiles (dirPath):
284			for root, dirs, files in os.walk(dirPath):
285				for file in files:
286					yield os.path.join(root, file)
287
288		files = []
289		for path in paths:
290			if os.path.isdir(path):
291				files += list(getFiles(path))
292			else:
293				files.append(path)
294
295		return files
296
297	def isUpToDate (self, config):
298		inputs				= resolvePaths(config, self.getInputs())
299		outputs				= resolvePaths(config, self.getOutputs())
300
301		assert len(inputs) > 0 and len(outputs) > 0
302
303		expandedInputs		= BuildStep.expandPathsToFiles(inputs)
304		expandedOutputs		= BuildStep.expandPathsToFiles(outputs)
305
306		existingInputs		= filter(os.path.exists, expandedInputs)
307		existingOutputs		= filter(os.path.exists, expandedOutputs)
308
309		if len(existingInputs) != len(expandedInputs):
310			for file in expandedInputs:
311				if file not in existingInputs:
312					print "ERROR: Missing input file: %s" % file
313			die("Missing input files")
314
315		if len(existingOutputs) != len(expandedOutputs):
316			return False # One or more output files are missing
317
318		lastInputChange		= max(map(os.path.getmtime, existingInputs))
319		firstOutputChange	= min(map(os.path.getmtime, existingOutputs))
320
321		return lastInputChange <= firstOutputChange
322
323	def update (config):
324		die("BuildStep.update() not implemented")
325
326def getNativeBuildPath (config, abiName):
327	return os.path.join(config.buildPath, "%s-%s-%d" % (abiName, config.nativeBuildType, config.nativeApi))
328
329def buildNativeLibrary (config, abiName):
330	def makeNDKVersionString (version):
331		minorVersionString = (chr(ord('a') + version[1]) if version[1] > 0 else "")
332		return "r%d%s" % (version[0], minorVersionString)
333
334	def getBuildArgs (config, abiName):
335		args = ['-DDEQP_TARGET=android',
336				'-DDEQP_TARGET_TOOLCHAIN=ndk-modern',
337				'-DCMAKE_C_FLAGS=-Werror',
338				'-DCMAKE_CXX_FLAGS=-Werror',
339				'-DANDROID_NDK_HOST_OS=%s' % config.env.ndk.hostOsName,
340				'-DANDROID_NDK_PATH=%s' % config.env.ndk.path,
341				'-DANDROID_ABI=%s' % abiName,
342				'-DDE_ANDROID_API=%s' % config.nativeApi,
343				'-DGLCTS_GTF_TARGET=%s' % config.gtfTarget]
344
345		if config.angle is not None:
346			args.append('-DANGLE_LIBS=%s' % os.path.join(config.angle, abiName))
347
348		return args
349
350	nativeBuildPath	= getNativeBuildPath(config, abiName)
351	buildConfig		= BuildConfig(nativeBuildPath, config.nativeBuildType, getBuildArgs(config, abiName))
352
353	build(buildConfig, config.cmakeGenerator, ["deqp"])
354
355def executeSteps (config, steps):
356	for step in steps:
357		if not step.isUpToDate(config):
358			step.update(config)
359
360def parsePackageName (manifestPath):
361	tree = xml.etree.ElementTree.parse(manifestPath)
362
363	if not 'package' in tree.getroot().attrib:
364		raise Exception("'package' attribute missing from root element in %s" % manifestPath)
365
366	return tree.getroot().attrib['package']
367
368class PackageDescription:
369	def __init__ (self, appDirName, appName, hasResources = True):
370		self.appDirName		= appDirName
371		self.appName		= appName
372		self.hasResources	= hasResources
373
374	def getAppName (self):
375		return self.appName
376
377	def getAppDirName (self):
378		return self.appDirName
379
380	def getPackageName (self, config):
381		manifestPath	= resolvePath(config, self.getManifestPath())
382
383		return parsePackageName(manifestPath)
384
385	def getManifestPath (self):
386		return [SourceRoot(), "android", self.appDirName, "AndroidManifest.xml"]
387
388	def getResPath (self):
389		return [SourceRoot(), "android", self.appDirName, "res"]
390
391	def getSourcePaths (self):
392		return [
393				[SourceRoot(), "android", self.appDirName, "src"]
394			]
395
396	def getAssetsPath (self):
397		return [BuildRoot(), self.appDirName, "assets"]
398
399	def getClassesJarPath (self):
400		return [BuildRoot(), self.appDirName, "bin", "classes.jar"]
401
402	def getClassesDexPath (self):
403		return [BuildRoot(), self.appDirName, "bin", "classes.dex"]
404
405	def getAPKPath (self):
406		return [BuildRoot(), self.appDirName, "bin", self.appName + ".apk"]
407
408# Build step implementations
409
410class BuildNativeLibrary (BuildStep):
411	def __init__ (self, abi):
412		self.abi = abi
413
414	def isUpToDate (self, config):
415		return False
416
417	def update (self, config):
418		log(config, "BuildNativeLibrary: %s" % self.abi)
419		buildNativeLibrary(config, self.abi)
420
421class GenResourcesSrc (BuildStep):
422	def __init__ (self, package):
423		self.package = package
424
425	def getInputs (self):
426		return [self.package.getResPath(), self.package.getManifestPath()]
427
428	def getOutputs (self):
429		return [[GeneratedResSourcePath(self.package)]]
430
431	def update (self, config):
432		aaptPath	= which("aapt", [config.env.sdk.getBuildToolsPath()])
433		dstDir		= os.path.dirname(resolvePath(config, [GeneratedResSourcePath(self.package)]))
434
435		if not os.path.exists(dstDir):
436			os.makedirs(dstDir)
437
438		executeAndLog(config, [
439				aaptPath,
440				"package",
441				"-f",
442				"-m",
443				"-S", resolvePath(config, self.package.getResPath()),
444				"-M", resolvePath(config, self.package.getManifestPath()),
445				"-J", resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "src"]),
446				"-I", config.env.sdk.getPlatformLibrary(config.javaApi)
447			])
448
449# Builds classes.jar from *.java files
450class BuildJavaSource (BuildStep):
451	def __init__ (self, package, libraries = []):
452		self.package	= package
453		self.libraries	= libraries
454
455	def getSourcePaths (self):
456		srcPaths = self.package.getSourcePaths()
457
458		if self.package.hasResources:
459			srcPaths.append([BuildRoot(), self.package.getAppDirName(), "src"]) # Generated sources
460
461		return srcPaths
462
463	def getInputs (self):
464		inputs = self.getSourcePaths()
465
466		for lib in self.libraries:
467			inputs.append(lib.getClassesJarPath())
468
469		return inputs
470
471	def getOutputs (self):
472		return [self.package.getClassesJarPath()]
473
474	def update (self, config):
475		srcPaths	= resolvePaths(config, self.getSourcePaths())
476		srcFiles	= BuildStep.expandPathsToFiles(srcPaths)
477		jarPath		= resolvePath(config, self.package.getClassesJarPath())
478		objPath		= resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "obj"])
479		classPaths	= [objPath] + [resolvePath(config, lib.getClassesJarPath()) for lib in self.libraries]
480		pathSep		= ";" if HostInfo.getOs() == HostInfo.OS_WINDOWS else ":"
481
482		if os.path.exists(objPath):
483			shutil.rmtree(objPath)
484
485		os.makedirs(objPath)
486
487		for srcFile in srcFiles:
488			executeAndLog(config, [
489					"javac",
490					"-source", "1.7",
491					"-target", "1.7",
492					"-d", objPath,
493					"-bootclasspath", config.env.sdk.getPlatformLibrary(config.javaApi),
494					"-classpath", pathSep.join(classPaths),
495					"-sourcepath", pathSep.join(srcPaths),
496					srcFile
497				])
498
499		if not os.path.exists(os.path.dirname(jarPath)):
500			os.makedirs(os.path.dirname(jarPath))
501
502		try:
503			pushWorkingDir(objPath)
504			executeAndLog(config, [
505					"jar",
506					"cf",
507					jarPath,
508					"."
509				])
510		finally:
511			popWorkingDir()
512
513class BuildDex (BuildStep):
514	def __init__ (self, package, libraries):
515		self.package	= package
516		self.libraries	= libraries
517
518	def getInputs (self):
519		return [self.package.getClassesJarPath()] + [lib.getClassesJarPath() for lib in self.libraries]
520
521	def getOutputs (self):
522		return [self.package.getClassesDexPath()]
523
524	def update (self, config):
525		dxPath		= which("dx", [config.env.sdk.getBuildToolsPath()])
526		srcPaths	= resolvePaths(config, self.getInputs())
527		dexPath		= resolvePath(config, self.package.getClassesDexPath())
528		jarPaths	= [resolvePath(config, self.package.getClassesJarPath())]
529
530		for lib in self.libraries:
531			jarPaths.append(resolvePath(config, lib.getClassesJarPath()))
532
533		executeAndLog(config, [
534				dxPath,
535				"--dex",
536				"--output", dexPath
537			] + jarPaths)
538
539class CreateKeystore (BuildStep):
540	def __init__ (self):
541		self.keystorePath	= [BuildRoot(), "debug.keystore"]
542
543	def getOutputs (self):
544		return [self.keystorePath]
545
546	def isUpToDate (self, config):
547		return os.path.exists(resolvePath(config, self.keystorePath))
548
549	def update (self, config):
550		executeAndLog(config, [
551				"keytool",
552				"-genkey",
553				"-keystore", resolvePath(config, self.keystorePath),
554				"-storepass", "android",
555				"-alias", "androiddebugkey",
556				"-keypass", "android",
557				"-keyalg", "RSA",
558				"-keysize", "2048",
559				"-validity", "10000",
560				"-dname", "CN=, OU=, O=, L=, S=, C=",
561			])
562
563# Builds APK without code
564class BuildBaseAPK (BuildStep):
565	def __init__ (self, package, libraries = []):
566		self.package	= package
567		self.libraries	= libraries
568		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "base.apk"]
569
570	def getResPaths (self):
571		paths = []
572		for pkg in [self.package] + self.libraries:
573			if pkg.hasResources:
574				paths.append(pkg.getResPath())
575		return paths
576
577	def getInputs (self):
578		return [self.package.getManifestPath()] + self.getResPaths()
579
580	def getOutputs (self):
581		return [self.dstPath]
582
583	def update (self, config):
584		aaptPath	= which("aapt", [config.env.sdk.getBuildToolsPath()])
585		dstPath		= resolvePath(config, self.dstPath)
586
587		if not os.path.exists(os.path.dirname(dstPath)):
588			os.makedirs(os.path.dirname(dstPath))
589
590		args = [
591			aaptPath,
592			"package",
593			"-f",
594			"-M", resolvePath(config, self.package.getManifestPath()),
595			"-I", config.env.sdk.getPlatformLibrary(config.javaApi),
596			"-F", dstPath,
597		]
598
599		for resPath in self.getResPaths():
600			args += ["-S", resolvePath(config, resPath)]
601
602		if config.verbose:
603			args.append("-v")
604
605		executeAndLog(config, args)
606
607def addFilesToAPK (config, apkPath, baseDir, relFilePaths):
608	aaptPath		= which("aapt", [config.env.sdk.getBuildToolsPath()])
609	maxBatchSize	= 25
610
611	pushWorkingDir(baseDir)
612	try:
613		workQueue = list(relFilePaths)
614
615		while len(workQueue) > 0:
616			batchSize	= min(len(workQueue), maxBatchSize)
617			items		= workQueue[0:batchSize]
618
619			executeAndLog(config, [
620					aaptPath,
621					"add",
622					"-f", apkPath,
623				] + items)
624
625			del workQueue[0:batchSize]
626	finally:
627		popWorkingDir()
628
629def addFileToAPK (config, apkPath, baseDir, relFilePath):
630	addFilesToAPK(config, apkPath, baseDir, [relFilePath])
631
632class AddJavaToAPK (BuildStep):
633	def __init__ (self, package):
634		self.package	= package
635		self.srcPath	= BuildBaseAPK(self.package).getOutputs()[0]
636		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-java.apk"]
637
638	def getInputs (self):
639		return [
640				self.srcPath,
641				self.package.getClassesDexPath(),
642			]
643
644	def getOutputs (self):
645		return [self.dstPath]
646
647	def update (self, config):
648		srcPath		= resolvePath(config, self.srcPath)
649		dstPath		= resolvePath(config, self.getOutputs()[0])
650		dexPath		= resolvePath(config, self.package.getClassesDexPath())
651
652		shutil.copyfile(srcPath, dstPath)
653		addFileToAPK(config, dstPath, os.path.dirname(dexPath), os.path.basename(dexPath))
654
655class AddAssetsToAPK (BuildStep):
656	def __init__ (self, package, abi):
657		self.package	= package
658		self.buildPath	= [NativeBuildPath(abi)]
659		self.srcPath	= AddJavaToAPK(self.package).getOutputs()[0]
660		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-assets.apk"]
661
662	def getInputs (self):
663		return [
664				self.srcPath,
665				self.buildPath + ["assets"]
666			]
667
668	def getOutputs (self):
669		return [self.dstPath]
670
671	@staticmethod
672	def getAssetFiles (buildPath):
673		allFiles = BuildStep.expandPathsToFiles([os.path.join(buildPath, "assets")])
674		return [os.path.relpath(p, buildPath) for p in allFiles]
675
676	def update (self, config):
677		srcPath		= resolvePath(config, self.srcPath)
678		dstPath		= resolvePath(config, self.getOutputs()[0])
679		buildPath	= resolvePath(config, self.buildPath)
680		assetFiles	= AddAssetsToAPK.getAssetFiles(buildPath)
681
682		shutil.copyfile(srcPath, dstPath)
683
684		addFilesToAPK(config, dstPath, buildPath, assetFiles)
685
686class AddNativeLibsToAPK (BuildStep):
687	def __init__ (self, package, abis):
688		self.package	= package
689		self.abis		= abis
690		self.srcPath	= AddAssetsToAPK(self.package, "").getOutputs()[0]
691		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-native-libs.apk"]
692
693	def getInputs (self):
694		paths = [self.srcPath]
695		for abi in self.abis:
696			paths.append([NativeBuildPath(abi), "libdeqp.so"])
697		return paths
698
699	def getOutputs (self):
700		return [self.dstPath]
701
702	def update (self, config):
703		srcPath		= resolvePath(config, self.srcPath)
704		dstPath		= resolvePath(config, self.getOutputs()[0])
705		pkgPath		= resolvePath(config, [BuildRoot(), self.package.getAppDirName()])
706		libFiles	= []
707
708		# Create right directory structure first
709		for abi in self.abis:
710			libSrcPath	= resolvePath(config, [NativeBuildPath(abi), "libdeqp.so"])
711			libRelPath	= os.path.join("lib", abi, "libdeqp.so")
712			libAbsPath	= os.path.join(pkgPath, libRelPath)
713
714			if not os.path.exists(os.path.dirname(libAbsPath)):
715				os.makedirs(os.path.dirname(libAbsPath))
716
717			shutil.copyfile(libSrcPath, libAbsPath)
718			libFiles.append(libRelPath)
719
720			if config.layers:
721				layersGlob = os.path.join(config.layers, abi, "libVkLayer_*.so")
722				libVkLayers = glob.glob(layersGlob)
723				for layer in libVkLayers:
724					layerFilename = os.path.basename(layer)
725					layerRelPath = os.path.join("lib", abi, layerFilename)
726					layerAbsPath = os.path.join(pkgPath, layerRelPath)
727					shutil.copyfile(layer, layerAbsPath)
728					libFiles.append(layerRelPath)
729					print "Adding layer binary: %s" % (layer,)
730
731			if config.angle:
732				angleGlob = os.path.join(config.angle, abi, "lib*_angle.so")
733				libAngle = glob.glob(angleGlob)
734				for lib in libAngle:
735					libFilename = os.path.basename(lib)
736					libRelPath = os.path.join("lib", abi, libFilename)
737					libAbsPath = os.path.join(pkgPath, libRelPath)
738					shutil.copyfile(lib, libAbsPath)
739					libFiles.append(libRelPath)
740					print "Adding ANGLE binary: %s" % (lib,)
741
742		shutil.copyfile(srcPath, dstPath)
743		addFilesToAPK(config, dstPath, pkgPath, libFiles)
744
745class SignAPK (BuildStep):
746	def __init__ (self, package):
747		self.package		= package
748		self.srcPath		= AddNativeLibsToAPK(self.package, []).getOutputs()[0]
749		self.dstPath		= [BuildRoot(), self.package.getAppDirName(), "tmp", "signed.apk"]
750		self.keystorePath	= CreateKeystore().getOutputs()[0]
751
752	def getInputs (self):
753		return [self.srcPath, self.keystorePath]
754
755	def getOutputs (self):
756		return [self.dstPath]
757
758	def update (self, config):
759		srcPath		= resolvePath(config, self.srcPath)
760		dstPath		= resolvePath(config, self.dstPath)
761
762		executeAndLog(config, [
763				"jarsigner",
764				"-keystore", resolvePath(config, self.keystorePath),
765				"-storepass", "android",
766				"-keypass", "android",
767				"-signedjar", dstPath,
768				srcPath,
769				"androiddebugkey"
770			])
771
772def getBuildRootRelativeAPKPath (package):
773	return os.path.join(package.getAppDirName(), package.getAppName() + ".apk")
774
775class FinalizeAPK (BuildStep):
776	def __init__ (self, package):
777		self.package		= package
778		self.srcPath		= SignAPK(self.package).getOutputs()[0]
779		self.dstPath		= [BuildRoot(), getBuildRootRelativeAPKPath(self.package)]
780		self.keystorePath	= CreateKeystore().getOutputs()[0]
781
782	def getInputs (self):
783		return [self.srcPath]
784
785	def getOutputs (self):
786		return [self.dstPath]
787
788	def update (self, config):
789		srcPath			= resolvePath(config, self.srcPath)
790		dstPath			= resolvePath(config, self.dstPath)
791		zipalignPath	= os.path.join(config.env.sdk.getBuildToolsPath(), "zipalign")
792
793		executeAndLog(config, [
794				zipalignPath,
795				"-f", "4",
796				srcPath,
797				dstPath
798			])
799
800def getBuildStepsForPackage (abis, package, libraries = []):
801	steps = []
802
803	assert len(abis) > 0
804
805	# Build native code first
806	for abi in abis:
807		steps += [BuildNativeLibrary(abi)]
808
809	# Build library packages
810	for library in libraries:
811		if library.hasResources:
812			steps.append(GenResourcesSrc(library))
813		steps.append(BuildJavaSource(library))
814
815	# Build main package .java sources
816	if package.hasResources:
817		steps.append(GenResourcesSrc(package))
818	steps.append(BuildJavaSource(package, libraries))
819	steps.append(BuildDex(package, libraries))
820
821	# Build base APK
822	steps.append(BuildBaseAPK(package, libraries))
823	steps.append(AddJavaToAPK(package))
824
825	# Add assets from first ABI
826	steps.append(AddAssetsToAPK(package, abis[0]))
827
828	# Add native libs to APK
829	steps.append(AddNativeLibsToAPK(package, abis))
830
831	# Finalize APK
832	steps.append(CreateKeystore())
833	steps.append(SignAPK(package))
834	steps.append(FinalizeAPK(package))
835
836	return steps
837
838def getPackageAndLibrariesForTarget (target):
839	deqpPackage	= PackageDescription("package", "dEQP")
840	ctsPackage	= PackageDescription("openglcts", "Khronos-CTS", hasResources = False)
841
842	if target == 'deqp':
843		return (deqpPackage, [])
844	elif target == 'openglcts':
845		return (ctsPackage, [deqpPackage])
846	else:
847		raise Exception("Uknown target '%s'" % target)
848
849def findNDK ():
850	ndkBuildPath = which('ndk-build')
851	if ndkBuildPath != None:
852		return os.path.dirname(ndkBuildPath)
853	else:
854		return None
855
856def findSDK ():
857	sdkBuildPath = which('android')
858	if sdkBuildPath != None:
859		return os.path.dirname(os.path.dirname(sdkBuildPath))
860	else:
861		return None
862
863def getDefaultBuildRoot ():
864	return os.path.join(tempfile.gettempdir(), "deqp-android-build")
865
866def parseArgs ():
867	nativeBuildTypes	= ['Release', 'Debug', 'MinSizeRel', 'RelWithAsserts', 'RelWithDebInfo']
868	defaultNDKPath		= findNDK()
869	defaultSDKPath		= findSDK()
870	defaultBuildRoot	= getDefaultBuildRoot()
871
872	parser = argparse.ArgumentParser(os.path.basename(__file__),
873		formatter_class=argparse.ArgumentDefaultsHelpFormatter)
874	parser.add_argument('--native-build-type',
875		dest='nativeBuildType',
876		default="RelWithAsserts",
877		choices=nativeBuildTypes,
878		help="Native code build type")
879	parser.add_argument('--build-root',
880		dest='buildRoot',
881		default=defaultBuildRoot,
882		help="Root build directory")
883	parser.add_argument('--abis',
884		dest='abis',
885		default=",".join(NDKEnv.getKnownAbis()),
886		help="ABIs to build")
887	parser.add_argument('--native-api',
888		type=int,
889		dest='nativeApi',
890		default=21,
891		help="Android API level to target in native code")
892	parser.add_argument('--sdk',
893		dest='sdkPath',
894		default=defaultSDKPath,
895		help="Android SDK path",
896		required=(True if defaultSDKPath == None else False))
897	parser.add_argument('--ndk',
898		dest='ndkPath',
899		default=defaultNDKPath,
900		help="Android NDK path",
901		required=(True if defaultNDKPath == None else False))
902	parser.add_argument('-v', '--verbose',
903		dest='verbose',
904		help="Verbose output",
905		default=False,
906		action='store_true')
907	parser.add_argument('--target',
908		dest='target',
909		help='Build target',
910		choices=['deqp', 'openglcts'],
911		default='deqp')
912	parser.add_argument('--kc-cts-target',
913		dest='gtfTarget',
914		default='gles32',
915		choices=['gles32', 'gles31', 'gles3', 'gles2', 'gl'],
916		help="KC-CTS (GTF) target API (only used in openglcts target)")
917	parser.add_argument('--layers-path',
918		dest='layers',
919		default=None,
920		required=False)
921	parser.add_argument('--angle-path',
922		dest='angle',
923		default=None,
924		required=False)
925
926	args = parser.parse_args()
927
928	def parseAbis (abisStr):
929		knownAbis	= set(NDKEnv.getKnownAbis())
930		abis		= []
931
932		for abi in abisStr.split(','):
933			abi = abi.strip()
934			if not abi in knownAbis:
935				raise Exception("Unknown ABI: %s" % abi)
936			abis.append(abi)
937
938		return abis
939
940	# Custom parsing & checks
941	try:
942		args.abis = parseAbis(args.abis)
943		if len(args.abis) == 0:
944			raise Exception("--abis can't be empty")
945	except Exception as e:
946		print "ERROR: %s" % str(e)
947		parser.print_help()
948		sys.exit(-1)
949
950	return args
951
952if __name__ == "__main__":
953	args		= parseArgs()
954
955	ndk			= NDKEnv(os.path.realpath(args.ndkPath))
956	sdk			= SDKEnv(os.path.realpath(args.sdkPath))
957	buildPath	= os.path.realpath(args.buildRoot)
958	env			= Environment(sdk, ndk)
959	config		= Configuration(env, buildPath, abis=args.abis, nativeApi=args.nativeApi, nativeBuildType=args.nativeBuildType, gtfTarget=args.gtfTarget, verbose=args.verbose,
960						 layers=args.layers, angle=args.angle)
961
962	try:
963		config.check()
964	except Exception as e:
965		print "ERROR: %s" % str(e)
966		print ""
967		print "Please check your configuration:"
968		print "  --sdk=%s" % args.sdkPath
969		print "  --ndk=%s" % args.ndkPath
970		sys.exit(-1)
971
972	pkg, libs	= getPackageAndLibrariesForTarget(args.target)
973	steps		= getBuildStepsForPackage(config.abis, pkg, libs)
974
975	executeSteps(config, steps)
976
977	print ""
978	print "Built %s" % os.path.join(buildPath, getBuildRootRelativeAPKPath(pkg))
979