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 = list(map(lambda x: x.strip(), 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, minApi, 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			= 28
179		self.minApi				= minApi
180		self.nativeBuildType	= nativeBuildType
181		self.gtfTarget			= gtfTarget
182		self.verbose			= verbose
183		self.layers				= layers
184		self.angle				= angle
185		self.cmakeGenerator		= selectFirstAvailableGenerator([NINJA_GENERATOR, MAKEFILE_GENERATOR, NMAKE_GENERATOR])
186
187	def check (self):
188		if self.cmakeGenerator == None:
189			raise Exception("Failed to find build tools for CMake")
190
191		if not os.path.exists(self.env.ndk.path):
192			raise Exception("Android NDK not found at %s" % self.env.ndk.path)
193
194		if not NDKEnv.isHostOsSupported(self.env.ndk.hostOsName):
195			raise Exception("NDK '%s' is not supported on this machine" % self.env.ndk.hostOsName)
196
197		if self.env.ndk.version[0] < 15:
198			raise Exception("Android NDK version %d is not supported; build requires NDK version >= 15" % (self.env.ndk.version[0]))
199
200		if not (self.minApi <= self.javaApi <= self.nativeApi):
201			raise Exception("Requires: min-api (%d) <= java-api (%d) <= native-api (%d)" % (self.minApi, self.javaApi, self.nativeApi))
202
203		if self.env.sdk.buildToolsVersion == (0,0,0):
204			raise Exception("No build tools directory found at %s" % os.path.join(self.env.sdk.path, "build-tools"))
205
206		androidBuildTools = ["aapt", "zipalign", "dx"]
207		for tool in androidBuildTools:
208			if which(tool, [self.env.sdk.getBuildToolsPath()]) == None:
209				raise Exception("Missing Android build tool: %s" % toolPath)
210
211		requiredToolsInPath = ["javac", "jar", "jarsigner", "keytool"]
212		for tool in requiredToolsInPath:
213			if which(tool) == None:
214				raise Exception("%s not in PATH" % tool)
215
216def log (config, msg):
217	if config.verbose:
218		print(msg)
219
220def executeAndLog (config, args):
221	if config.verbose:
222		print(" ".join(args))
223	execute(args)
224
225# Path components
226
227class ResolvablePathComponent:
228	def __init__ (self):
229		pass
230
231class SourceRoot (ResolvablePathComponent):
232	def resolve (self, config):
233		return config.sourcePath
234
235class BuildRoot (ResolvablePathComponent):
236	def resolve (self, config):
237		return config.buildPath
238
239class NativeBuildPath (ResolvablePathComponent):
240	def __init__ (self, abiName):
241		self.abiName = abiName
242
243	def resolve (self, config):
244		return getNativeBuildPath(config, self.abiName)
245
246class GeneratedResSourcePath (ResolvablePathComponent):
247	def __init__ (self, package):
248		self.package = package
249
250	def resolve (self, config):
251		packageComps	= self.package.getPackageName(config).split('.')
252		packageDir		= os.path.join(*packageComps)
253
254		return os.path.join(config.buildPath, self.package.getAppDirName(), "src", packageDir, "R.java")
255
256def resolvePath (config, path):
257	resolvedComps = []
258
259	for component in path:
260		if isinstance(component, ResolvablePathComponent):
261			resolvedComps.append(component.resolve(config))
262		else:
263			resolvedComps.append(str(component))
264
265	return os.path.join(*resolvedComps)
266
267def resolvePaths (config, paths):
268	return list(map(lambda p: resolvePath(config, p), paths))
269
270class BuildStep:
271	def __init__ (self):
272		pass
273
274	def getInputs (self):
275		return []
276
277	def getOutputs (self):
278		return []
279
280	@staticmethod
281	def expandPathsToFiles (paths):
282		"""
283		Expand mixed list of file and directory paths into a flattened list
284		of files. Any non-existent input paths are preserved as is.
285		"""
286
287		def getFiles (dirPath):
288			for root, dirs, files in os.walk(dirPath):
289				for file in files:
290					yield os.path.join(root, file)
291
292		files = []
293		for path in paths:
294			if os.path.isdir(path):
295				files += list(getFiles(path))
296			else:
297				files.append(path)
298
299		return files
300
301	def isUpToDate (self, config):
302		inputs				= resolvePaths(config, self.getInputs())
303		outputs				= resolvePaths(config, self.getOutputs())
304
305		assert len(inputs) > 0 and len(outputs) > 0
306
307		expandedInputs		= BuildStep.expandPathsToFiles(inputs)
308		expandedOutputs		= BuildStep.expandPathsToFiles(outputs)
309
310		existingInputs		= list(filter(os.path.exists, expandedInputs))
311		existingOutputs		= list(filter(os.path.exists, expandedOutputs))
312
313		if len(existingInputs) != len(expandedInputs):
314			for file in expandedInputs:
315				if file not in existingInputs:
316					print("ERROR: Missing input file: %s" % file)
317			die("Missing input files")
318
319		if len(existingOutputs) != len(expandedOutputs):
320			return False # One or more output files are missing
321
322		lastInputChange		= max(map(os.path.getmtime, existingInputs))
323		firstOutputChange	= min(map(os.path.getmtime, existingOutputs))
324
325		return lastInputChange <= firstOutputChange
326
327	def update (config):
328		die("BuildStep.update() not implemented")
329
330def getNativeBuildPath (config, abiName):
331	return os.path.join(config.buildPath, "%s-%s-%d" % (abiName, config.nativeBuildType, config.nativeApi))
332
333def clearCMakeCacheVariables(args):
334	# New value, so clear the necessary cmake variables
335	args.append('-UANGLE_LIBS')
336	args.append('-UGLES1_LIBRARY')
337	args.append('-UGLES2_LIBRARY')
338	args.append('-UEGL_LIBRARY')
339
340def buildNativeLibrary (config, abiName):
341	def makeNDKVersionString (version):
342		minorVersionString = (chr(ord('a') + version[1]) if version[1] > 0 else "")
343		return "r%d%s" % (version[0], minorVersionString)
344
345	def getBuildArgs (config, abiName):
346		args = ['-DDEQP_TARGET=android',
347				'-DDEQP_TARGET_TOOLCHAIN=ndk-modern',
348				'-DCMAKE_C_FLAGS=-Werror',
349				'-DCMAKE_CXX_FLAGS=-Werror',
350				'-DANDROID_NDK_PATH=%s' % config.env.ndk.path,
351				'-DANDROID_ABI=%s' % abiName,
352				'-DDE_ANDROID_API=%s' % config.nativeApi,
353				'-DGLCTS_GTF_TARGET=%s' % config.gtfTarget]
354
355		if config.angle is None:
356			# Find any previous builds that may have embedded ANGLE libs and clear the CMake cache
357			for abi in NDKEnv.getKnownAbis():
358				cMakeCachePath = os.path.join(getNativeBuildPath(config, abi), "CMakeCache.txt")
359				try:
360					if 'ANGLE_LIBS' in open(cMakeCachePath).read():
361						clearCMakeCacheVariables(args)
362				except IOError:
363					pass
364		else:
365			cMakeCachePath = os.path.join(getNativeBuildPath(config, abiName), "CMakeCache.txt")
366			angleLibsDir = os.path.join(config.angle, abiName)
367			# Check if the user changed where the ANGLE libs are being loaded from
368			try:
369				if angleLibsDir not in open(cMakeCachePath).read():
370					clearCMakeCacheVariables(args)
371			except IOError:
372				pass
373			args.append('-DANGLE_LIBS=%s' % angleLibsDir)
374
375		return args
376
377	nativeBuildPath	= getNativeBuildPath(config, abiName)
378	buildConfig		= BuildConfig(nativeBuildPath, config.nativeBuildType, getBuildArgs(config, abiName))
379
380	build(buildConfig, config.cmakeGenerator, ["deqp"])
381
382def executeSteps (config, steps):
383	for step in steps:
384		if not step.isUpToDate(config):
385			step.update(config)
386
387def parsePackageName (manifestPath):
388	tree = xml.etree.ElementTree.parse(manifestPath)
389
390	if not 'package' in tree.getroot().attrib:
391		raise Exception("'package' attribute missing from root element in %s" % manifestPath)
392
393	return tree.getroot().attrib['package']
394
395class PackageDescription:
396	def __init__ (self, appDirName, appName, hasResources = True):
397		self.appDirName		= appDirName
398		self.appName		= appName
399		self.hasResources	= hasResources
400
401	def getAppName (self):
402		return self.appName
403
404	def getAppDirName (self):
405		return self.appDirName
406
407	def getPackageName (self, config):
408		manifestPath	= resolvePath(config, self.getManifestPath())
409
410		return parsePackageName(manifestPath)
411
412	def getManifestPath (self):
413		return [SourceRoot(), "android", self.appDirName, "AndroidManifest.xml"]
414
415	def getResPath (self):
416		return [SourceRoot(), "android", self.appDirName, "res"]
417
418	def getSourcePaths (self):
419		return [
420				[SourceRoot(), "android", self.appDirName, "src"]
421			]
422
423	def getAssetsPath (self):
424		return [BuildRoot(), self.appDirName, "assets"]
425
426	def getClassesJarPath (self):
427		return [BuildRoot(), self.appDirName, "bin", "classes.jar"]
428
429	def getClassesDexPath (self):
430		return [BuildRoot(), self.appDirName, "bin", "classes.dex"]
431
432	def getAPKPath (self):
433		return [BuildRoot(), self.appDirName, "bin", self.appName + ".apk"]
434
435# Build step implementations
436
437class BuildNativeLibrary (BuildStep):
438	def __init__ (self, abi):
439		self.abi = abi
440
441	def isUpToDate (self, config):
442		return False
443
444	def update (self, config):
445		log(config, "BuildNativeLibrary: %s" % self.abi)
446		buildNativeLibrary(config, self.abi)
447
448class GenResourcesSrc (BuildStep):
449	def __init__ (self, package):
450		self.package = package
451
452	def getInputs (self):
453		return [self.package.getResPath(), self.package.getManifestPath()]
454
455	def getOutputs (self):
456		return [[GeneratedResSourcePath(self.package)]]
457
458	def update (self, config):
459		aaptPath	= which("aapt", [config.env.sdk.getBuildToolsPath()])
460		dstDir		= os.path.dirname(resolvePath(config, [GeneratedResSourcePath(self.package)]))
461
462		if not os.path.exists(dstDir):
463			os.makedirs(dstDir)
464
465		executeAndLog(config, [
466				aaptPath,
467				"package",
468				"-f",
469				"-m",
470				"-S", resolvePath(config, self.package.getResPath()),
471				"-M", resolvePath(config, self.package.getManifestPath()),
472				"-J", resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "src"]),
473				"-I", config.env.sdk.getPlatformLibrary(config.javaApi)
474			])
475
476# Builds classes.jar from *.java files
477class BuildJavaSource (BuildStep):
478	def __init__ (self, package, libraries = []):
479		self.package	= package
480		self.libraries	= libraries
481
482	def getSourcePaths (self):
483		srcPaths = self.package.getSourcePaths()
484
485		if self.package.hasResources:
486			srcPaths.append([BuildRoot(), self.package.getAppDirName(), "src"]) # Generated sources
487
488		return srcPaths
489
490	def getInputs (self):
491		inputs = self.getSourcePaths()
492
493		for lib in self.libraries:
494			inputs.append(lib.getClassesJarPath())
495
496		return inputs
497
498	def getOutputs (self):
499		return [self.package.getClassesJarPath()]
500
501	def update (self, config):
502		srcPaths	= resolvePaths(config, self.getSourcePaths())
503		srcFiles	= BuildStep.expandPathsToFiles(srcPaths)
504		jarPath		= resolvePath(config, self.package.getClassesJarPath())
505		objPath		= resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "obj"])
506		classPaths	= [objPath] + [resolvePath(config, lib.getClassesJarPath()) for lib in self.libraries]
507		pathSep		= ";" if HostInfo.getOs() == HostInfo.OS_WINDOWS else ":"
508
509		if os.path.exists(objPath):
510			shutil.rmtree(objPath)
511
512		os.makedirs(objPath)
513
514		for srcFile in srcFiles:
515			executeAndLog(config, [
516					"javac",
517					"-source", "1.7",
518					"-target", "1.7",
519					"-d", objPath,
520					"-bootclasspath", config.env.sdk.getPlatformLibrary(config.javaApi),
521					"-classpath", pathSep.join(classPaths),
522					"-sourcepath", pathSep.join(srcPaths),
523					srcFile
524				])
525
526		if not os.path.exists(os.path.dirname(jarPath)):
527			os.makedirs(os.path.dirname(jarPath))
528
529		try:
530			pushWorkingDir(objPath)
531			executeAndLog(config, [
532					"jar",
533					"cf",
534					jarPath,
535					"."
536				])
537		finally:
538			popWorkingDir()
539
540class BuildDex (BuildStep):
541	def __init__ (self, package, libraries):
542		self.package	= package
543		self.libraries	= libraries
544
545	def getInputs (self):
546		return [self.package.getClassesJarPath()] + [lib.getClassesJarPath() for lib in self.libraries]
547
548	def getOutputs (self):
549		return [self.package.getClassesDexPath()]
550
551	def update (self, config):
552		dxPath		= which("dx", [config.env.sdk.getBuildToolsPath()])
553		srcPaths	= resolvePaths(config, self.getInputs())
554		dexPath		= resolvePath(config, self.package.getClassesDexPath())
555		jarPaths	= [resolvePath(config, self.package.getClassesJarPath())]
556
557		for lib in self.libraries:
558			jarPaths.append(resolvePath(config, lib.getClassesJarPath()))
559
560		executeAndLog(config, [
561				dxPath,
562				"--dex",
563				"--output", dexPath
564			] + jarPaths)
565
566class CreateKeystore (BuildStep):
567	def __init__ (self):
568		self.keystorePath	= [BuildRoot(), "debug.keystore"]
569
570	def getOutputs (self):
571		return [self.keystorePath]
572
573	def isUpToDate (self, config):
574		return os.path.exists(resolvePath(config, self.keystorePath))
575
576	def update (self, config):
577		executeAndLog(config, [
578				"keytool",
579				"-genkey",
580				"-keystore", resolvePath(config, self.keystorePath),
581				"-storepass", "android",
582				"-alias", "androiddebugkey",
583				"-keypass", "android",
584				"-keyalg", "RSA",
585				"-keysize", "2048",
586				"-validity", "10000",
587				"-dname", "CN=, OU=, O=, L=, S=, C=",
588			])
589
590# Builds APK without code
591class BuildBaseAPK (BuildStep):
592	def __init__ (self, package, libraries = []):
593		self.package	= package
594		self.libraries	= libraries
595		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "base.apk"]
596
597	def getResPaths (self):
598		paths = []
599		for pkg in [self.package] + self.libraries:
600			if pkg.hasResources:
601				paths.append(pkg.getResPath())
602		return paths
603
604	def getInputs (self):
605		return [self.package.getManifestPath()] + self.getResPaths()
606
607	def getOutputs (self):
608		return [self.dstPath]
609
610	def update (self, config):
611		aaptPath	= which("aapt", [config.env.sdk.getBuildToolsPath()])
612		dstPath		= resolvePath(config, self.dstPath)
613
614		if not os.path.exists(os.path.dirname(dstPath)):
615			os.makedirs(os.path.dirname(dstPath))
616
617		args = [
618			aaptPath,
619			"package",
620			"-f",
621			"--min-sdk-version", str(config.minApi),
622			"--target-sdk-version", str(config.javaApi),
623			"-M", resolvePath(config, self.package.getManifestPath()),
624			"-I", config.env.sdk.getPlatformLibrary(config.javaApi),
625			"-F", dstPath,
626		]
627
628		for resPath in self.getResPaths():
629			args += ["-S", resolvePath(config, resPath)]
630
631		if config.verbose:
632			args.append("-v")
633
634		executeAndLog(config, args)
635
636def addFilesToAPK (config, apkPath, baseDir, relFilePaths):
637	aaptPath		= which("aapt", [config.env.sdk.getBuildToolsPath()])
638	maxBatchSize	= 25
639
640	pushWorkingDir(baseDir)
641	try:
642		workQueue = list(relFilePaths)
643
644		while len(workQueue) > 0:
645			batchSize	= min(len(workQueue), maxBatchSize)
646			items		= workQueue[0:batchSize]
647
648			executeAndLog(config, [
649					aaptPath,
650					"add",
651					"-f", apkPath,
652				] + items)
653
654			del workQueue[0:batchSize]
655	finally:
656		popWorkingDir()
657
658def addFileToAPK (config, apkPath, baseDir, relFilePath):
659	addFilesToAPK(config, apkPath, baseDir, [relFilePath])
660
661class AddJavaToAPK (BuildStep):
662	def __init__ (self, package):
663		self.package	= package
664		self.srcPath	= BuildBaseAPK(self.package).getOutputs()[0]
665		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-java.apk"]
666
667	def getInputs (self):
668		return [
669				self.srcPath,
670				self.package.getClassesDexPath(),
671			]
672
673	def getOutputs (self):
674		return [self.dstPath]
675
676	def update (self, config):
677		srcPath		= resolvePath(config, self.srcPath)
678		dstPath		= resolvePath(config, self.getOutputs()[0])
679		dexPath		= resolvePath(config, self.package.getClassesDexPath())
680
681		shutil.copyfile(srcPath, dstPath)
682		addFileToAPK(config, dstPath, os.path.dirname(dexPath), os.path.basename(dexPath))
683
684class AddAssetsToAPK (BuildStep):
685	def __init__ (self, package, abi):
686		self.package	= package
687		self.buildPath	= [NativeBuildPath(abi)]
688		self.srcPath	= AddJavaToAPK(self.package).getOutputs()[0]
689		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-assets.apk"]
690
691	def getInputs (self):
692		return [
693				self.srcPath,
694				self.buildPath + ["assets"]
695			]
696
697	def getOutputs (self):
698		return [self.dstPath]
699
700	@staticmethod
701	def getAssetFiles (buildPath):
702		allFiles = BuildStep.expandPathsToFiles([os.path.join(buildPath, "assets")])
703		return [os.path.relpath(p, buildPath) for p in allFiles]
704
705	def update (self, config):
706		srcPath		= resolvePath(config, self.srcPath)
707		dstPath		= resolvePath(config, self.getOutputs()[0])
708		buildPath	= resolvePath(config, self.buildPath)
709		assetFiles	= AddAssetsToAPK.getAssetFiles(buildPath)
710
711		shutil.copyfile(srcPath, dstPath)
712
713		addFilesToAPK(config, dstPath, buildPath, assetFiles)
714
715class AddNativeLibsToAPK (BuildStep):
716	def __init__ (self, package, abis):
717		self.package	= package
718		self.abis		= abis
719		self.srcPath	= AddAssetsToAPK(self.package, "").getOutputs()[0]
720		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-native-libs.apk"]
721
722	def getInputs (self):
723		paths = [self.srcPath]
724		for abi in self.abis:
725			paths.append([NativeBuildPath(abi), "libdeqp.so"])
726		return paths
727
728	def getOutputs (self):
729		return [self.dstPath]
730
731	def update (self, config):
732		srcPath		= resolvePath(config, self.srcPath)
733		dstPath		= resolvePath(config, self.getOutputs()[0])
734		pkgPath		= resolvePath(config, [BuildRoot(), self.package.getAppDirName()])
735		libFiles	= []
736
737		# Create right directory structure first
738		for abi in self.abis:
739			libSrcPath	= resolvePath(config, [NativeBuildPath(abi), "libdeqp.so"])
740			libRelPath	= os.path.join("lib", abi, "libdeqp.so")
741			libAbsPath	= os.path.join(pkgPath, libRelPath)
742
743			if not os.path.exists(os.path.dirname(libAbsPath)):
744				os.makedirs(os.path.dirname(libAbsPath))
745
746			shutil.copyfile(libSrcPath, libAbsPath)
747			libFiles.append(libRelPath)
748
749			if config.layers:
750				layersGlob = os.path.join(config.layers, abi, "libVkLayer_*.so")
751				libVkLayers = glob.glob(layersGlob)
752				for layer in libVkLayers:
753					layerFilename = os.path.basename(layer)
754					layerRelPath = os.path.join("lib", abi, layerFilename)
755					layerAbsPath = os.path.join(pkgPath, layerRelPath)
756					shutil.copyfile(layer, layerAbsPath)
757					libFiles.append(layerRelPath)
758					print("Adding layer binary: %s" % (layer,))
759
760			if config.angle:
761				angleGlob = os.path.join(config.angle, abi, "lib*_angle.so")
762				libAngle = glob.glob(angleGlob)
763				for lib in libAngle:
764					libFilename = os.path.basename(lib)
765					libRelPath = os.path.join("lib", abi, libFilename)
766					libAbsPath = os.path.join(pkgPath, libRelPath)
767					shutil.copyfile(lib, libAbsPath)
768					libFiles.append(libRelPath)
769					print("Adding ANGLE binary: %s" % (lib,))
770
771		shutil.copyfile(srcPath, dstPath)
772		addFilesToAPK(config, dstPath, pkgPath, libFiles)
773
774class SignAPK (BuildStep):
775	def __init__ (self, package):
776		self.package		= package
777		self.srcPath		= AddNativeLibsToAPK(self.package, []).getOutputs()[0]
778		self.dstPath		= [BuildRoot(), self.package.getAppDirName(), "tmp", "signed.apk"]
779		self.keystorePath	= CreateKeystore().getOutputs()[0]
780
781	def getInputs (self):
782		return [self.srcPath, self.keystorePath]
783
784	def getOutputs (self):
785		return [self.dstPath]
786
787	def update (self, config):
788		srcPath		= resolvePath(config, self.srcPath)
789		dstPath		= resolvePath(config, self.dstPath)
790
791		executeAndLog(config, [
792				"jarsigner",
793				"-keystore", resolvePath(config, self.keystorePath),
794				"-storepass", "android",
795				"-keypass", "android",
796				"-signedjar", dstPath,
797				srcPath,
798				"androiddebugkey"
799			])
800
801def getBuildRootRelativeAPKPath (package):
802	return os.path.join(package.getAppDirName(), package.getAppName() + ".apk")
803
804class FinalizeAPK (BuildStep):
805	def __init__ (self, package):
806		self.package		= package
807		self.srcPath		= SignAPK(self.package).getOutputs()[0]
808		self.dstPath		= [BuildRoot(), getBuildRootRelativeAPKPath(self.package)]
809		self.keystorePath	= CreateKeystore().getOutputs()[0]
810
811	def getInputs (self):
812		return [self.srcPath]
813
814	def getOutputs (self):
815		return [self.dstPath]
816
817	def update (self, config):
818		srcPath			= resolvePath(config, self.srcPath)
819		dstPath			= resolvePath(config, self.dstPath)
820		zipalignPath	= os.path.join(config.env.sdk.getBuildToolsPath(), "zipalign")
821
822		executeAndLog(config, [
823				zipalignPath,
824				"-f", "4",
825				srcPath,
826				dstPath
827			])
828
829def getBuildStepsForPackage (abis, package, libraries = []):
830	steps = []
831
832	assert len(abis) > 0
833
834	# Build native code first
835	for abi in abis:
836		steps += [BuildNativeLibrary(abi)]
837
838	# Build library packages
839	for library in libraries:
840		if library.hasResources:
841			steps.append(GenResourcesSrc(library))
842		steps.append(BuildJavaSource(library))
843
844	# Build main package .java sources
845	if package.hasResources:
846		steps.append(GenResourcesSrc(package))
847	steps.append(BuildJavaSource(package, libraries))
848	steps.append(BuildDex(package, libraries))
849
850	# Build base APK
851	steps.append(BuildBaseAPK(package, libraries))
852	steps.append(AddJavaToAPK(package))
853
854	# Add assets from first ABI
855	steps.append(AddAssetsToAPK(package, abis[0]))
856
857	# Add native libs to APK
858	steps.append(AddNativeLibsToAPK(package, abis))
859
860	# Finalize APK
861	steps.append(CreateKeystore())
862	steps.append(SignAPK(package))
863	steps.append(FinalizeAPK(package))
864
865	return steps
866
867def getPackageAndLibrariesForTarget (target):
868	deqpPackage	= PackageDescription("package", "dEQP")
869	ctsPackage	= PackageDescription("openglcts", "Khronos-CTS", hasResources = False)
870
871	if target == 'deqp':
872		return (deqpPackage, [])
873	elif target == 'openglcts':
874		return (ctsPackage, [deqpPackage])
875	else:
876		raise Exception("Uknown target '%s'" % target)
877
878def findNDK ():
879	ndkBuildPath = which('ndk-build')
880	if ndkBuildPath != None:
881		return os.path.dirname(ndkBuildPath)
882	else:
883		return None
884
885def findSDK ():
886	sdkBuildPath = which('android')
887	if sdkBuildPath != None:
888		return os.path.dirname(os.path.dirname(sdkBuildPath))
889	else:
890		return None
891
892def getDefaultBuildRoot ():
893	return os.path.join(tempfile.gettempdir(), "deqp-android-build")
894
895def parseArgs ():
896	nativeBuildTypes	= ['Release', 'Debug', 'MinSizeRel', 'RelWithAsserts', 'RelWithDebInfo']
897	defaultNDKPath		= findNDK()
898	defaultSDKPath		= findSDK()
899	defaultBuildRoot	= getDefaultBuildRoot()
900
901	parser = argparse.ArgumentParser(os.path.basename(__file__),
902		formatter_class=argparse.ArgumentDefaultsHelpFormatter)
903	parser.add_argument('--native-build-type',
904		dest='nativeBuildType',
905		default="RelWithAsserts",
906		choices=nativeBuildTypes,
907		help="Native code build type")
908	parser.add_argument('--build-root',
909		dest='buildRoot',
910		default=defaultBuildRoot,
911		help="Root build directory")
912	parser.add_argument('--abis',
913		dest='abis',
914		default=",".join(NDKEnv.getKnownAbis()),
915		help="ABIs to build")
916	parser.add_argument('--native-api',
917		type=int,
918		dest='nativeApi',
919		default=28,
920		help="Android API level to target in native code")
921	parser.add_argument('--min-api',
922		type=int,
923		dest='minApi',
924		default=22,
925		help="Minimum Android API level for which the APK can be installed")
926	parser.add_argument('--sdk',
927		dest='sdkPath',
928		default=defaultSDKPath,
929		help="Android SDK path",
930		required=(True if defaultSDKPath == None else False))
931	parser.add_argument('--ndk',
932		dest='ndkPath',
933		default=defaultNDKPath,
934		help="Android NDK path",
935		required=(True if defaultNDKPath == None else False))
936	parser.add_argument('-v', '--verbose',
937		dest='verbose',
938		help="Verbose output",
939		default=False,
940		action='store_true')
941	parser.add_argument('--target',
942		dest='target',
943		help='Build target',
944		choices=['deqp', 'openglcts'],
945		default='deqp')
946	parser.add_argument('--kc-cts-target',
947		dest='gtfTarget',
948		default='gles32',
949		choices=['gles32', 'gles31', 'gles3', 'gles2', 'gl'],
950		help="KC-CTS (GTF) target API (only used in openglcts target)")
951	parser.add_argument('--layers-path',
952		dest='layers',
953		default=None,
954		required=False)
955	parser.add_argument('--angle-path',
956		dest='angle',
957		default=None,
958		required=False)
959
960	args = parser.parse_args()
961
962	def parseAbis (abisStr):
963		knownAbis	= set(NDKEnv.getKnownAbis())
964		abis		= []
965
966		for abi in abisStr.split(','):
967			abi = abi.strip()
968			if not abi in knownAbis:
969				raise Exception("Unknown ABI: %s" % abi)
970			abis.append(abi)
971
972		return abis
973
974	# Custom parsing & checks
975	try:
976		args.abis = parseAbis(args.abis)
977		if len(args.abis) == 0:
978			raise Exception("--abis can't be empty")
979	except Exception as e:
980		print("ERROR: %s" % str(e))
981		parser.print_help()
982		sys.exit(-1)
983
984	return args
985
986if __name__ == "__main__":
987	args		= parseArgs()
988
989	ndk			= NDKEnv(os.path.realpath(args.ndkPath))
990	sdk			= SDKEnv(os.path.realpath(args.sdkPath))
991	buildPath	= os.path.realpath(args.buildRoot)
992	env			= Environment(sdk, ndk)
993	config		= Configuration(env, buildPath, abis=args.abis, nativeApi=args.nativeApi, minApi=args.minApi, nativeBuildType=args.nativeBuildType, gtfTarget=args.gtfTarget,
994						 verbose=args.verbose, layers=args.layers, angle=args.angle)
995
996	try:
997		config.check()
998	except Exception as e:
999		print("ERROR: %s" % str(e))
1000		print("")
1001		print("Please check your configuration:")
1002		print("  --sdk=%s" % args.sdkPath)
1003		print("  --ndk=%s" % args.ndkPath)
1004		sys.exit(-1)
1005
1006	pkg, libs	= getPackageAndLibrariesForTarget(args.target)
1007	steps		= getBuildStepsForPackage(config.abis, pkg, libs)
1008
1009	executeSteps(config, steps)
1010
1011	print("")
1012	print("Built %s" % os.path.join(buildPath, getBuildRootRelativeAPKPath(pkg)))
1013