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