• 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 = 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, javaApi, 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			= javaApi
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" % tool)
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		# Workaround for Windows.
644		if os.path.sep == "\\":
645			workQueue = [i.replace("\\", "/") for i in workQueue]
646
647		while len(workQueue) > 0:
648			batchSize	= min(len(workQueue), maxBatchSize)
649			items		= workQueue[0:batchSize]
650
651			executeAndLog(config, [
652					aaptPath,
653					"add",
654					"-f", apkPath,
655				] + items)
656
657			del workQueue[0:batchSize]
658	finally:
659		popWorkingDir()
660
661def addFileToAPK (config, apkPath, baseDir, relFilePath):
662	addFilesToAPK(config, apkPath, baseDir, [relFilePath])
663
664class AddJavaToAPK (BuildStep):
665	def __init__ (self, package):
666		self.package	= package
667		self.srcPath	= BuildBaseAPK(self.package).getOutputs()[0]
668		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-java.apk"]
669
670	def getInputs (self):
671		return [
672				self.srcPath,
673				self.package.getClassesDexPath(),
674			]
675
676	def getOutputs (self):
677		return [self.dstPath]
678
679	def update (self, config):
680		srcPath		= resolvePath(config, self.srcPath)
681		dstPath		= resolvePath(config, self.getOutputs()[0])
682		dexPath		= resolvePath(config, self.package.getClassesDexPath())
683
684		shutil.copyfile(srcPath, dstPath)
685		addFileToAPK(config, dstPath, os.path.dirname(dexPath), os.path.basename(dexPath))
686
687class AddAssetsToAPK (BuildStep):
688	def __init__ (self, package, abi):
689		self.package	= package
690		self.buildPath	= [NativeBuildPath(abi)]
691		self.srcPath	= AddJavaToAPK(self.package).getOutputs()[0]
692		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-assets.apk"]
693
694	def getInputs (self):
695		return [
696				self.srcPath,
697				self.buildPath + ["assets"]
698			]
699
700	def getOutputs (self):
701		return [self.dstPath]
702
703	@staticmethod
704	def getAssetFiles (buildPath):
705		allFiles = BuildStep.expandPathsToFiles([os.path.join(buildPath, "assets")])
706		return [os.path.relpath(p, buildPath) for p in allFiles]
707
708	def update (self, config):
709		srcPath		= resolvePath(config, self.srcPath)
710		dstPath		= resolvePath(config, self.getOutputs()[0])
711		buildPath	= resolvePath(config, self.buildPath)
712		assetFiles	= AddAssetsToAPK.getAssetFiles(buildPath)
713
714		shutil.copyfile(srcPath, dstPath)
715
716		addFilesToAPK(config, dstPath, buildPath, assetFiles)
717
718class AddNativeLibsToAPK (BuildStep):
719	def __init__ (self, package, abis):
720		self.package	= package
721		self.abis		= abis
722		self.srcPath	= AddAssetsToAPK(self.package, "").getOutputs()[0]
723		self.dstPath	= [BuildRoot(), self.package.getAppDirName(), "tmp", "with-native-libs.apk"]
724
725	def getInputs (self):
726		paths = [self.srcPath]
727		for abi in self.abis:
728			paths.append([NativeBuildPath(abi), "libdeqp.so"])
729		return paths
730
731	def getOutputs (self):
732		return [self.dstPath]
733
734	def update (self, config):
735		srcPath		= resolvePath(config, self.srcPath)
736		dstPath		= resolvePath(config, self.getOutputs()[0])
737		pkgPath		= resolvePath(config, [BuildRoot(), self.package.getAppDirName()])
738		libFiles	= []
739
740		# Create right directory structure first
741		for abi in self.abis:
742			libSrcPath	= resolvePath(config, [NativeBuildPath(abi), "libdeqp.so"])
743			libRelPath	= os.path.join("lib", abi, "libdeqp.so")
744			libAbsPath	= os.path.join(pkgPath, libRelPath)
745
746			if not os.path.exists(os.path.dirname(libAbsPath)):
747				os.makedirs(os.path.dirname(libAbsPath))
748
749			shutil.copyfile(libSrcPath, libAbsPath)
750			libFiles.append(libRelPath)
751
752			if config.layers:
753				layersGlob = os.path.join(config.layers, abi, "libVkLayer_*.so")
754				libVkLayers = glob.glob(layersGlob)
755				for layer in libVkLayers:
756					layerFilename = os.path.basename(layer)
757					layerRelPath = os.path.join("lib", abi, layerFilename)
758					layerAbsPath = os.path.join(pkgPath, layerRelPath)
759					shutil.copyfile(layer, layerAbsPath)
760					libFiles.append(layerRelPath)
761					print("Adding layer binary: %s" % (layer,))
762
763			if config.angle:
764				angleGlob = os.path.join(config.angle, abi, "lib*_angle.so")
765				libAngle = glob.glob(angleGlob)
766				for lib in libAngle:
767					libFilename = os.path.basename(lib)
768					libRelPath = os.path.join("lib", abi, libFilename)
769					libAbsPath = os.path.join(pkgPath, libRelPath)
770					shutil.copyfile(lib, libAbsPath)
771					libFiles.append(libRelPath)
772					print("Adding ANGLE binary: %s" % (lib,))
773
774		shutil.copyfile(srcPath, dstPath)
775		addFilesToAPK(config, dstPath, pkgPath, libFiles)
776
777class SignAPK (BuildStep):
778	def __init__ (self, package):
779		self.package		= package
780		self.srcPath		= AddNativeLibsToAPK(self.package, []).getOutputs()[0]
781		self.dstPath		= [BuildRoot(), self.package.getAppDirName(), "tmp", "signed.apk"]
782		self.keystorePath	= CreateKeystore().getOutputs()[0]
783
784	def getInputs (self):
785		return [self.srcPath, self.keystorePath]
786
787	def getOutputs (self):
788		return [self.dstPath]
789
790	def update (self, config):
791		srcPath		= resolvePath(config, self.srcPath)
792		dstPath		= resolvePath(config, self.dstPath)
793
794		executeAndLog(config, [
795				"jarsigner",
796				"-keystore", resolvePath(config, self.keystorePath),
797				"-storepass", "android",
798				"-keypass", "android",
799				"-signedjar", dstPath,
800				srcPath,
801				"androiddebugkey"
802			])
803
804def getBuildRootRelativeAPKPath (package):
805	return os.path.join(package.getAppDirName(), package.getAppName() + ".apk")
806
807class FinalizeAPK (BuildStep):
808	def __init__ (self, package):
809		self.package		= package
810		self.srcPath		= SignAPK(self.package).getOutputs()[0]
811		self.dstPath		= [BuildRoot(), getBuildRootRelativeAPKPath(self.package)]
812		self.keystorePath	= CreateKeystore().getOutputs()[0]
813
814	def getInputs (self):
815		return [self.srcPath]
816
817	def getOutputs (self):
818		return [self.dstPath]
819
820	def update (self, config):
821		srcPath			= resolvePath(config, self.srcPath)
822		dstPath			= resolvePath(config, self.dstPath)
823		zipalignPath	= os.path.join(config.env.sdk.getBuildToolsPath(), "zipalign")
824
825		executeAndLog(config, [
826				zipalignPath,
827				"-f", "4",
828				srcPath,
829				dstPath
830			])
831
832def getBuildStepsForPackage (abis, package, libraries = []):
833	steps = []
834
835	assert len(abis) > 0
836
837	# Build native code first
838	for abi in abis:
839		steps += [BuildNativeLibrary(abi)]
840
841	# Build library packages
842	for library in libraries:
843		if library.hasResources:
844			steps.append(GenResourcesSrc(library))
845		steps.append(BuildJavaSource(library))
846
847	# Build main package .java sources
848	if package.hasResources:
849		steps.append(GenResourcesSrc(package))
850	steps.append(BuildJavaSource(package, libraries))
851	steps.append(BuildDex(package, libraries))
852
853	# Build base APK
854	steps.append(BuildBaseAPK(package, libraries))
855	steps.append(AddJavaToAPK(package))
856
857	# Add assets from first ABI
858	steps.append(AddAssetsToAPK(package, abis[0]))
859
860	# Add native libs to APK
861	steps.append(AddNativeLibsToAPK(package, abis))
862
863	# Finalize APK
864	steps.append(CreateKeystore())
865	steps.append(SignAPK(package))
866	steps.append(FinalizeAPK(package))
867
868	return steps
869
870def getPackageAndLibrariesForTarget (target):
871	deqpPackage	= PackageDescription("package", "dEQP")
872	ctsPackage	= PackageDescription("openglcts", "Khronos-CTS", hasResources = False)
873
874	if target == 'deqp':
875		return (deqpPackage, [])
876	elif target == 'openglcts':
877		return (ctsPackage, [deqpPackage])
878	else:
879		raise Exception("Uknown target '%s'" % target)
880
881def findNDK ():
882	ndkBuildPath = which('ndk-build')
883	if ndkBuildPath != None:
884		return os.path.dirname(ndkBuildPath)
885	else:
886		return None
887
888def findSDK ():
889	sdkBuildPath = which('android')
890	if sdkBuildPath != None:
891		return os.path.dirname(os.path.dirname(sdkBuildPath))
892	else:
893		return None
894
895def getDefaultBuildRoot ():
896	return os.path.join(tempfile.gettempdir(), "deqp-android-build")
897
898def parseArgs ():
899	nativeBuildTypes	= ['Release', 'Debug', 'MinSizeRel', 'RelWithAsserts', 'RelWithDebInfo']
900	defaultNDKPath		= findNDK()
901	defaultSDKPath		= findSDK()
902	defaultBuildRoot	= getDefaultBuildRoot()
903
904	parser = argparse.ArgumentParser(os.path.basename(__file__),
905		formatter_class=argparse.ArgumentDefaultsHelpFormatter)
906	parser.add_argument('--native-build-type',
907		dest='nativeBuildType',
908		default="RelWithAsserts",
909		choices=nativeBuildTypes,
910		help="Native code build type")
911	parser.add_argument('--build-root',
912		dest='buildRoot',
913		default=defaultBuildRoot,
914		help="Root build directory")
915	parser.add_argument('--abis',
916		dest='abis',
917		default=",".join(NDKEnv.getKnownAbis()),
918		help="ABIs to build")
919	parser.add_argument('--native-api',
920		type=int,
921		dest='nativeApi',
922		default=28,
923		help="Android API level to target in native code")
924	parser.add_argument('--java-api',
925		type=int,
926		dest='javaApi',
927		default=28,
928		help="Android API level to target in Java code")
929	parser.add_argument('--min-api',
930		type=int,
931		dest='minApi',
932		default=22,
933		help="Minimum Android API level for which the APK can be installed")
934	parser.add_argument('--sdk',
935		dest='sdkPath',
936		default=defaultSDKPath,
937		help="Android SDK path",
938		required=(True if defaultSDKPath == None else False))
939	parser.add_argument('--ndk',
940		dest='ndkPath',
941		default=defaultNDKPath,
942		help="Android NDK path",
943		required=(True if defaultNDKPath == None else False))
944	parser.add_argument('-v', '--verbose',
945		dest='verbose',
946		help="Verbose output",
947		default=False,
948		action='store_true')
949	parser.add_argument('--target',
950		dest='target',
951		help='Build target',
952		choices=['deqp', 'openglcts'],
953		default='deqp')
954	parser.add_argument('--kc-cts-target',
955		dest='gtfTarget',
956		default='gles32',
957		choices=['gles32', 'gles31', 'gles3', 'gles2', 'gl'],
958		help="KC-CTS (GTF) target API (only used in openglcts target)")
959	parser.add_argument('--layers-path',
960		dest='layers',
961		default=None,
962		required=False)
963	parser.add_argument('--angle-path',
964		dest='angle',
965		default=None,
966		required=False)
967
968	args = parser.parse_args()
969
970	def parseAbis (abisStr):
971		knownAbis	= set(NDKEnv.getKnownAbis())
972		abis		= []
973
974		for abi in abisStr.split(','):
975			abi = abi.strip()
976			if not abi in knownAbis:
977				raise Exception("Unknown ABI: %s" % abi)
978			abis.append(abi)
979
980		return abis
981
982	# Custom parsing & checks
983	try:
984		args.abis = parseAbis(args.abis)
985		if len(args.abis) == 0:
986			raise Exception("--abis can't be empty")
987	except Exception as e:
988		print("ERROR: %s" % str(e))
989		parser.print_help()
990		sys.exit(-1)
991
992	return args
993
994if __name__ == "__main__":
995	args		= parseArgs()
996
997	ndk			= NDKEnv(os.path.realpath(args.ndkPath))
998	sdk			= SDKEnv(os.path.realpath(args.sdkPath))
999	buildPath	= os.path.realpath(args.buildRoot)
1000	env			= Environment(sdk, ndk)
1001	config		= Configuration(env, buildPath, abis=args.abis, nativeApi=args.nativeApi, javaApi=args.javaApi, minApi=args.minApi, nativeBuildType=args.nativeBuildType, gtfTarget=args.gtfTarget,
1002						 verbose=args.verbose, layers=args.layers, angle=args.angle)
1003
1004	try:
1005		config.check()
1006	except Exception as e:
1007		print("ERROR: %s" % str(e))
1008		print("")
1009		print("Please check your configuration:")
1010		print("  --sdk=%s" % args.sdkPath)
1011		print("  --ndk=%s" % args.ndkPath)
1012		sys.exit(-1)
1013
1014	pkg, libs	= getPackageAndLibrariesForTarget(args.target)
1015	steps		= getBuildStepsForPackage(config.abis, pkg, libs)
1016
1017	executeSteps(config, steps)
1018
1019	print("")
1020	print("Built %s" % os.path.join(buildPath, getBuildRootRelativeAPKPath(pkg)))
1021