xref: /aosp_15_r20/external/deqp/scripts/android/build_apk.py (revision 35238bce31c2a825756842865a792f8cf7f89930)
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                # Need to copy everything in the layer folder
773                layersGlob = os.path.join(config.layers, abi, "*")
774                libVkLayers = glob.glob(layersGlob)
775                for layer in libVkLayers:
776                    layerFilename = os.path.basename(layer)
777                    layerRelPath = os.path.join("lib", abi, layerFilename)
778                    layerAbsPath = os.path.join(pkgPath, layerRelPath)
779                    shutil.copyfile(layer, layerAbsPath)
780                    libFiles.append(layerRelPath)
781                    print("Adding layer binary: %s" % (layer,))
782
783            if config.angle:
784                angleGlob = os.path.join(config.angle, abi, "lib*_angle.so")
785                libAngle = glob.glob(angleGlob)
786                for lib in libAngle:
787                    libFilename = os.path.basename(lib)
788                    libRelPath = os.path.join("lib", abi, libFilename)
789                    libAbsPath = os.path.join(pkgPath, libRelPath)
790                    shutil.copyfile(lib, libAbsPath)
791                    libFiles.append(libRelPath)
792                    print("Adding ANGLE binary: %s" % (lib,))
793
794        shutil.copyfile(srcPath, dstPath)
795        addFilesToAPK(config, dstPath, pkgPath, libFiles)
796
797class SignAPK (BuildStep):
798    def __init__ (self, package):
799        self.package = package
800        self.srcPath = AlignAPK(self.package).getOutputs()[0]
801        self.dstPath = [BuildRoot(), getBuildRootRelativeAPKPath(self.package)]
802        self.keystorePath = CreateKeystore().getOutputs()[0]
803
804    def getInputs (self):
805        return [self.srcPath, self.keystorePath]
806
807    def getOutputs (self):
808        return [self.dstPath]
809
810    def update (self, config):
811        apksigner = which("apksigner", [config.env.sdk.getBuildToolsPath()])
812        srcPath = resolvePath(config, self.srcPath)
813        dstPath = resolvePath(config, self.dstPath)
814
815        executeAndLog(config, [
816                apksigner,
817                "sign",
818                "--ks", resolvePath(config, self.keystorePath),
819                "--ks-key-alias", "androiddebugkey",
820                "--ks-pass", "pass:android",
821                "--key-pass", "pass:android",
822                "--min-sdk-version", str(config.minApi),
823                "--max-sdk-version", str(config.javaApi),
824                "--out", dstPath,
825                srcPath
826            ])
827
828def getBuildRootRelativeAPKPath (package):
829    return os.path.join(package.getAppDirName(), package.getAppName() + ".apk")
830
831class AlignAPK (BuildStep):
832    def __init__ (self, package):
833        self.package = package
834        self.srcPath = AddNativeLibsToAPK(self.package, []).getOutputs()[0]
835        self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "aligned.apk"]
836        self.keystorePath = CreateKeystore().getOutputs()[0]
837
838    def getInputs (self):
839        return [self.srcPath]
840
841    def getOutputs (self):
842        return [self.dstPath]
843
844    def update (self, config):
845        srcPath = resolvePath(config, self.srcPath)
846        dstPath = resolvePath(config, self.dstPath)
847        zipalignPath = os.path.join(config.env.sdk.getBuildToolsPath(), "zipalign")
848
849        executeAndLog(config, [
850                zipalignPath,
851                "-f", "4",
852                srcPath,
853                dstPath
854            ])
855
856def getBuildStepsForPackage (abis, package, libraries = []):
857    steps = []
858
859    assert len(abis) > 0
860
861    # Build native code first
862    for abi in abis:
863        steps += [BuildNativeLibrary(abi)]
864
865    # Build library packages
866    for library in libraries:
867        if library.hasResources:
868            steps.append(GenResourcesSrc(library))
869        steps.append(BuildJavaSource(library))
870
871    # Build main package .java sources
872    if package.hasResources:
873        steps.append(GenResourcesSrc(package))
874    steps.append(BuildJavaSource(package, libraries))
875    steps.append(BuildDex(package, libraries))
876
877    # Build base APK
878    steps.append(BuildBaseAPK(package, libraries))
879    steps.append(AddJavaToAPK(package))
880
881    # Add assets from first ABI
882    steps.append(AddAssetsToAPK(package, abis[0]))
883
884    # Add native libs to APK
885    steps.append(AddNativeLibsToAPK(package, abis))
886
887    # Finalize APK
888    steps.append(CreateKeystore())
889    steps.append(AlignAPK(package))
890    steps.append(SignAPK(package))
891
892    return steps
893
894def getPackageAndLibrariesForTarget (target):
895    deqpPackage = PackageDescription("package", "dEQP")
896    ctsPackage = PackageDescription("openglcts", "Khronos-CTS", hasResources = False)
897
898    if target == 'deqp':
899        return (deqpPackage, [])
900    elif target == 'openglcts':
901        return (ctsPackage, [deqpPackage])
902    else:
903        raise Exception("Uknown target '%s'" % target)
904
905def findNDK ():
906    ndkBuildPath = which('ndk-build')
907    if ndkBuildPath != None:
908        return os.path.dirname(ndkBuildPath)
909    else:
910        return None
911
912def findSDK ():
913    sdkBuildPath = which('android')
914    if sdkBuildPath != None:
915        return os.path.dirname(os.path.dirname(sdkBuildPath))
916    else:
917        return None
918
919def getDefaultBuildRoot ():
920    return os.path.join(tempfile.gettempdir(), "deqp-android-build")
921
922def parseArgs ():
923    nativeBuildTypes = ['Release', 'Debug', 'MinSizeRel', 'RelWithAsserts', 'RelWithDebInfo']
924    defaultNDKPath = findNDK()
925    defaultSDKPath = findSDK()
926    defaultBuildRoot = getDefaultBuildRoot()
927
928    parser = argparse.ArgumentParser(os.path.basename(__file__),
929        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
930    parser.add_argument('--native-build-type',
931        dest='nativeBuildType',
932        default="RelWithAsserts",
933        choices=nativeBuildTypes,
934        help="Native code build type")
935    parser.add_argument('--build-root',
936        dest='buildRoot',
937        default=defaultBuildRoot,
938        help="Root build directory")
939    parser.add_argument('--abis',
940        dest='abis',
941        default=",".join(NDKEnv.getKnownAbis()),
942        help="ABIs to build")
943    parser.add_argument('--native-api',
944        type=int,
945        dest='nativeApi',
946        default=28,
947        help="Android API level to target in native code")
948    parser.add_argument('--java-api',
949        type=int,
950        dest='javaApi',
951        default=28,
952        help="Android API level to target in Java code")
953    parser.add_argument('--tool-api',
954        type=int,
955        dest='toolApi',
956        default=-1,
957        help="Android Tools level to target (-1 being maximum present)")
958    parser.add_argument('--min-api',
959        type=int,
960        dest='minApi',
961        default=22,
962        help="Minimum Android API level for which the APK can be installed")
963    parser.add_argument('--sdk',
964        dest='sdkPath',
965        default=defaultSDKPath,
966        help="Android SDK path",
967        required=(True if defaultSDKPath == None else False))
968    parser.add_argument('--ndk',
969        dest='ndkPath',
970        default=defaultNDKPath,
971        help="Android NDK path",
972        required=(True if defaultNDKPath == None else False))
973    parser.add_argument('-v', '--verbose',
974        dest='verbose',
975        help="Verbose output",
976        default=False,
977        action='store_true')
978    parser.add_argument('--target',
979        dest='target',
980        help='Build target',
981        choices=['deqp', 'openglcts'],
982        default='deqp')
983    parser.add_argument('--kc-cts-target',
984        dest='gtfTarget',
985        default='gles32',
986        choices=['gles32', 'gles31', 'gles3', 'gles2', 'gl'],
987        help="KC-CTS (GTF) target API (only used in openglcts target)")
988    parser.add_argument('--layers-path',
989        dest='layers',
990        default=None,
991        required=False)
992    parser.add_argument('--angle-path',
993        dest='angle',
994        default=None,
995        required=False)
996
997    args = parser.parse_args()
998
999    def parseAbis (abisStr):
1000        knownAbis = set(NDKEnv.getKnownAbis())
1001        abis = []
1002
1003        for abi in abisStr.split(','):
1004            abi = abi.strip()
1005            if not abi in knownAbis:
1006                raise Exception("Unknown ABI: %s" % abi)
1007            abis.append(abi)
1008
1009        return abis
1010
1011    # Custom parsing & checks
1012    try:
1013        args.abis = parseAbis(args.abis)
1014        if len(args.abis) == 0:
1015            raise Exception("--abis can't be empty")
1016    except Exception as e:
1017        print("ERROR: %s" % str(e))
1018        parser.print_help()
1019        sys.exit(-1)
1020
1021    return args
1022
1023if __name__ == "__main__":
1024    args = parseArgs()
1025
1026    ndk = NDKEnv(os.path.realpath(args.ndkPath))
1027    sdk = SDKEnv(os.path.realpath(args.sdkPath), args.toolApi)
1028    buildPath = os.path.realpath(args.buildRoot)
1029    env = Environment(sdk, ndk)
1030    config = Configuration(env, buildPath, abis=args.abis, nativeApi=args.nativeApi, javaApi=args.javaApi, minApi=args.minApi, nativeBuildType=args.nativeBuildType, gtfTarget=args.gtfTarget,
1031                         verbose=args.verbose, layers=args.layers, angle=args.angle)
1032
1033    try:
1034        config.check()
1035    except Exception as e:
1036        print("ERROR: %s" % str(e))
1037        print("")
1038        print("Please check your configuration:")
1039        print("  --sdk=%s" % args.sdkPath)
1040        print("  --ndk=%s" % args.ndkPath)
1041        sys.exit(-1)
1042
1043    pkg, libs = getPackageAndLibrariesForTarget(args.target)
1044    steps = getBuildStepsForPackage(config.abis, pkg, libs)
1045
1046    executeSteps(config, steps)
1047
1048    print("")
1049    print("Built %s" % os.path.join(buildPath, getBuildRootRelativeAPKPath(pkg)))
1050