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