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