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