diff options
author | Pyry Haulos <phaulos@google.com> | 2017-03-27 11:21:37 -0700 |
---|---|---|
committer | Pyry Haulos <phaulos@google.com> | 2017-05-08 13:00:36 -0700 |
commit | 69bb2f6bcf261caa994273ee21b8e6111845d89e (patch) | |
tree | 7f29633005eb8b62e486a30d4a74eaf7a259344a /scripts/android | |
parent | e315fce5cddfef4bb27ae5a77e0910601655f006 (diff) | |
download | VK-GL-CTS-69bb2f6bcf261caa994273ee21b8e6111845d89e.tar.gz VK-GL-CTS-69bb2f6bcf261caa994273ee21b8e6111845d89e.tar.bz2 VK-GL-CTS-69bb2f6bcf261caa994273ee21b8e6111845d89e.zip |
Add new Android build and install scripts
This change adds new Android build and install scripts under
scripts/android. Key improvements over old ones are:
* Build no longer relies on ant or 'android project' tools.
* Native code build leverages scripts/build code which should fix
incremental builds and improve compatibility.
* Build script error reporting should be much better.
* Final APK is now built incrementally which should enable much faster
incremental builds once asset copy targets are fixed in main build.
This work required some changes to common code:
* Android cross-compile toolchain is set up by including
targets/android/ndk-r11.cmake before project() in the main
CMakeLists.txt instead of using -DCMAKE_TOOLCHAIN_FILE. CMake native
toolchain file support seems incredbly buggy and configuring
toolchain in regular build files seems to be much more robust.
* scripts/build/config.py now finds CMake automatically on OS X.
* New HostInfo class has been added into scripts/build/config.py.
Components: AOSP, Framework
Change-Id: I4b5b78c0d4d3aff248887ba5ced0c91081e24e6b
Diffstat (limited to 'scripts/android')
-rw-r--r-- | scripts/android/build_apk.py | 936 | ||||
-rw-r--r-- | scripts/android/install_apk.py | 248 |
2 files changed, 1184 insertions, 0 deletions
diff --git a/scripts/android/build_apk.py b/scripts/android/build_apk.py new file mode 100644 index 000000000..796dbee4b --- /dev/null +++ b/scripts/android/build_apk.py @@ -0,0 +1,936 @@ +# -*- coding: utf-8 -*- + +#------------------------------------------------------------------------- +# drawElements Quality Program utilities +# -------------------------------------- +# +# Copyright 2017 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#------------------------------------------------------------------------- + +# \todo [2017-04-10 pyry] +# * Use smarter asset copy in main build +# * cmake -E copy_directory doesn't copy timestamps which will cause +# assets to be always re-packaged +# * Consider adding an option for downloading SDK & NDK + +import os +import re +import sys +import string +import shutil +import argparse +import tempfile +import xml.etree.ElementTree + +# Import from <root>/scripts +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +from build.common import * +from build.config import * +from build.build import * + +class SDKEnv: + def __init__(self, path): + self.path = path + self.buildToolsVersion = SDKEnv.selectBuildToolsVersion(self.path) + + @staticmethod + def getBuildToolsVersions (path): + buildToolsPath = os.path.join(path, "build-tools") + versions = [] + + if os.path.exists(buildToolsPath): + for item in os.listdir(buildToolsPath): + m = re.match(r'^([0-9]+)\.([0-9]+)\.([0-9]+)$', item) + if m != None: + versions.append((int(m.group(1)), int(m.group(2)), int(m.group(3)))) + + return versions + + @staticmethod + def selectBuildToolsVersion (path): + preferred = [(25, 0, 2)] + versions = SDKEnv.getBuildToolsVersions(path) + + if len(versions) == 0: + return (0,0,0) + + for candidate in preferred: + if candidate in versions: + return candidate + + # Pick newest + versions.sort() + return versions[-1] + + def getPlatformLibrary (self, apiVersion): + return os.path.join(self.path, "platforms", "android-%d" % apiVersion, "android.jar") + + def getBuildToolsPath (self): + return os.path.join(self.path, "build-tools", "%d.%d.%d" % self.buildToolsVersion) + +class NDKEnv: + def __init__(self, path): + self.path = path + self.version = NDKEnv.detectVersion(self.path) + self.hostOsName = NDKEnv.detectHostOsName(self.path) + + @staticmethod + def getKnownAbis (): + return ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"] + + @staticmethod + def getAbiPrebuiltsName (abiName): + prebuilts = { + "armeabi-v7a": 'android-arm', + "arm64-v8a": 'android-arm64', + "x86": 'android-x86', + "x86_64": 'android-x86_64', + } + + if not abiName in prebuilts: + raise Exception("Unknown ABI: " + abiName) + + return prebuilts[abiName] + + @staticmethod + def detectVersion (path): + propFilePath = os.path.join(path, "source.properties") + try: + with open(propFilePath) as propFile: + for line in propFile: + keyValue = map(lambda x: string.strip(x), line.split("=")) + if keyValue[0] == "Pkg.Revision": + versionParts = keyValue[1].split(".") + return tuple(map(int, versionParts[0:2])) + except Exception as e: + raise Exception("Failed to read source prop file '%s': %s" % (propFilePath, str(e))) + except: + raise Exception("Failed to read source prop file '%s': unkown error") + + raise Exception("Failed to detect NDK version (does %s/source.properties have Pkg.Revision?)" % path) + + @staticmethod + def isHostOsSupported (hostOsName): + os = HostInfo.getOs() + bits = HostInfo.getArchBits() + hostOsParts = hostOsName.split('-') + + if len(hostOsParts) > 1: + assert(len(hostOsParts) == 2) + assert(hostOsParts[1] == "x86_64") + + if bits != 64: + return False + + if os == HostInfo.OS_WINDOWS: + return hostOsParts[0] == 'windows' + elif os == HostInfo.OS_LINUX: + return hostOsParts[0] == 'linux' + elif os == HostInfo.OS_OSX: + return hostOsParts[0] == 'darwin' + else: + raise Exception("Unhandled HostInfo.getOs() '%d'" % os) + + @staticmethod + def detectHostOsName (path): + hostOsNames = [ + "windows", + "windows-x86_64", + "darwin-x86", + "darwin-x86_64", + "linux-x86", + "linux-x86_64" + ] + + for name in hostOsNames: + if os.path.exists(os.path.join(path, "prebuilt", name)): + return name + + raise Exception("Failed to determine NDK host OS") + +class Environment: + def __init__(self, sdk, ndk): + self.sdk = sdk + self.ndk = ndk + +class Configuration: + def __init__(self, env, buildPath, abis, nativeBuildType, gtfTarget, verbose): + self.env = env + self.sourcePath = DEQP_DIR + self.buildPath = buildPath + self.abis = abis + self.nativeApi = 21 + self.javaApi = 22 + self.nativeBuildType = nativeBuildType + self.gtfTarget = gtfTarget + self.verbose = verbose + self.cmakeGenerator = selectFirstAvailableGenerator([NINJA_GENERATOR, MAKEFILE_GENERATOR, NMAKE_GENERATOR]) + + def check (self): + if self.cmakeGenerator == None: + raise Exception("Failed to find build tools for CMake") + + if not os.path.exists(self.env.ndk.path): + raise Exception("Android NDK not found at %s" % self.env.ndk.path) + + if not NDKEnv.isHostOsSupported(self.env.ndk.hostOsName): + raise Exception("NDK '%s' is not supported on this machine" % self.env.ndk.hostOsName) + + supportedNDKVersion = 11 + if self.env.ndk.version[0] != supportedNDKVersion: + raise Exception("Android NDK version %d is not supported; build requires NDK version %d" % (self.env.ndk.version[0], supportedNDKVersion)) + + if self.env.sdk.buildToolsVersion == (0,0,0): + raise Exception("No build tools directory found at %s" % os.path.join(self.env.sdk.path, "build-tools")) + + androidBuildTools = ["aapt", "zipalign", "dx"] + for tool in androidBuildTools: + if which(tool, [self.env.sdk.getBuildToolsPath()]) == None: + raise Exception("Missing Android build tool: %s" % toolPath) + + requiredToolsInPath = ["javac", "jar", "jarsigner", "keytool"] + for tool in requiredToolsInPath: + if which(tool) == None: + raise Exception("%s not in PATH" % tool) + +def log (config, msg): + if config.verbose: + print msg + +def executeAndLog (config, args): + if config.verbose: + print " ".join(args) + execute(args) + +# Path components + +class ResolvablePathComponent: + def __init__ (self): + pass + +class SourceRoot (ResolvablePathComponent): + def resolve (self, config): + return config.sourcePath + +class BuildRoot (ResolvablePathComponent): + def resolve (self, config): + return config.buildPath + +class NativeBuildPath (ResolvablePathComponent): + def __init__ (self, abiName): + self.abiName = abiName + + def resolve (self, config): + return getNativeBuildPath(config, self.abiName) + +class GeneratedResSourcePath (ResolvablePathComponent): + def __init__ (self, package): + self.package = package + + def resolve (self, config): + packageComps = self.package.getPackageName(config).split('.') + packageDir = os.path.join(*packageComps) + + return os.path.join(config.buildPath, self.package.getAppDirName(), "src", packageDir, "R.java") + +def resolvePath (config, path): + resolvedComps = [] + + for component in path: + if isinstance(component, ResolvablePathComponent): + resolvedComps.append(component.resolve(config)) + else: + resolvedComps.append(str(component)) + + return os.path.join(*resolvedComps) + +def resolvePaths (config, paths): + return list(map(lambda p: resolvePath(config, p), paths)) + +class BuildStep: + def __init__ (self): + pass + + def getInputs (self): + return [] + + def getOutputs (self): + return [] + + @staticmethod + def expandPathsToFiles (paths): + """ + Expand mixed list of file and directory paths into a flattened list + of files. Any non-existent input paths are preserved as is. + """ + + def getFiles (dirPath): + for root, dirs, files in os.walk(dirPath): + for file in files: + yield os.path.join(root, file) + + files = [] + for path in paths: + if os.path.isdir(path): + files += list(getFiles(path)) + else: + files.append(path) + + return files + + def isUpToDate (self, config): + inputs = resolvePaths(config, self.getInputs()) + outputs = resolvePaths(config, self.getOutputs()) + + assert len(inputs) > 0 and len(outputs) > 0 + + expandedInputs = BuildStep.expandPathsToFiles(inputs) + expandedOutputs = BuildStep.expandPathsToFiles(outputs) + + existingInputs = filter(os.path.exists, expandedInputs) + existingOutputs = filter(os.path.exists, expandedOutputs) + + if len(existingInputs) != len(expandedInputs): + for file in expandedInputs: + if file not in existingInputs: + print "ERROR: Missing input file: %s" % file + die("Missing input files") + + if len(existingOutputs) != len(expandedOutputs): + return False # One or more output files are missing + + lastInputChange = max(map(os.path.getmtime, existingInputs)) + firstOutputChange = min(map(os.path.getmtime, existingOutputs)) + + return lastInputChange <= firstOutputChange + + def update (config): + die("BuildStep.update() not implemented") + +def getNativeBuildPath (config, abiName): + return os.path.join(config.buildPath, "%s-%s-%d" % (abiName, config.nativeBuildType, config.nativeApi)) + +def buildNativeLibrary (config, abiName): + def makeNDKVersionString (version): + minorVersionString = (chr(ord('a') + version[1]) if version[1] > 0 else "") + return "r%d%s" % (version[0], minorVersionString) + + def getBuildArgs (config, abiName): + toolchain = 'ndk-%s' % makeNDKVersionString((config.env.ndk.version[0], 0)) + return ['-DDEQP_TARGET=android', + '-DDEQP_TARGET_TOOLCHAIN=%s' % toolchain, + '-DCMAKE_C_FLAGS=-Werror', + '-DCMAKE_CXX_FLAGS=-Werror', + '-DANDROID_NDK_HOST_OS=%s' % config.env.ndk.hostOsName, + '-DANDROID_NDK_PATH=%s' % config.env.ndk.path, + '-DANDROID_ABI=%s' % abiName, + '-DDE_ANDROID_API=%s' % config.nativeApi, + '-DGLCTS_GTF_TARGET=%s' % config.gtfTarget] + + nativeBuildPath = getNativeBuildPath(config, abiName) + buildConfig = BuildConfig(nativeBuildPath, config.nativeBuildType, getBuildArgs(config, abiName)) + + build(buildConfig, config.cmakeGenerator, ["deqp"]) + +def executeSteps (config, steps): + for step in steps: + if not step.isUpToDate(config): + step.update(config) + +def parsePackageName (manifestPath): + tree = xml.etree.ElementTree.parse(manifestPath) + + if not 'package' in tree.getroot().attrib: + raise Exception("'package' attribute missing from root element in %s" % manifestPath) + + return tree.getroot().attrib['package'] + +class PackageDescription: + def __init__ (self, appDirName, appName, hasResources = True): + self.appDirName = appDirName + self.appName = appName + self.hasResources = hasResources + + def getAppName (self): + return self.appName + + def getAppDirName (self): + return self.appDirName + + def getPackageName (self, config): + manifestPath = resolvePath(config, self.getManifestPath()) + + return parsePackageName(manifestPath) + + def getManifestPath (self): + return [SourceRoot(), "android", self.appDirName, "AndroidManifest.xml"] + + def getResPath (self): + return [SourceRoot(), "android", self.appDirName, "res"] + + def getSourcePaths (self): + return [ + [SourceRoot(), "android", self.appDirName, "src"] + ] + + def getAssetsPath (self): + return [BuildRoot(), self.appDirName, "assets"] + + def getClassesJarPath (self): + return [BuildRoot(), self.appDirName, "bin", "classes.jar"] + + def getClassesDexPath (self): + return [BuildRoot(), self.appDirName, "bin", "classes.dex"] + + def getAPKPath (self): + return [BuildRoot(), self.appDirName, "bin", self.appName + ".apk"] + +# Build step implementations + +class BuildNativeLibrary (BuildStep): + def __init__ (self, abi): + self.abi = abi + + def isUpToDate (self, config): + return False + + def update (self, config): + log(config, "BuildNativeLibrary: %s" % self.abi) + buildNativeLibrary(config, self.abi) + +class GenResourcesSrc (BuildStep): + def __init__ (self, package): + self.package = package + + def getInputs (self): + return [self.package.getResPath(), self.package.getManifestPath()] + + def getOutputs (self): + return [[GeneratedResSourcePath(self.package)]] + + def update (self, config): + aaptPath = which("aapt", [config.env.sdk.getBuildToolsPath()]) + dstDir = os.path.dirname(resolvePath(config, [GeneratedResSourcePath(self.package)])) + + if not os.path.exists(dstDir): + os.makedirs(dstDir) + + executeAndLog(config, [ + aaptPath, + "package", + "-f", + "-m", + "-S", resolvePath(config, self.package.getResPath()), + "-M", resolvePath(config, self.package.getManifestPath()), + "-J", resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "src"]), + "-I", config.env.sdk.getPlatformLibrary(config.javaApi) + ]) + +# Builds classes.jar from *.java files +class BuildJavaSource (BuildStep): + def __init__ (self, package, libraries = []): + self.package = package + self.libraries = libraries + + def getSourcePaths (self): + srcPaths = self.package.getSourcePaths() + + if self.package.hasResources: + srcPaths.append([BuildRoot(), self.package.getAppDirName(), "src"]) # Generated sources + + return srcPaths + + def getInputs (self): + inputs = self.getSourcePaths() + + for lib in self.libraries: + inputs.append(lib.getClassesJarPath()) + + return inputs + + def getOutputs (self): + return [self.package.getClassesJarPath()] + + def update (self, config): + srcPaths = resolvePaths(config, self.getSourcePaths()) + srcFiles = BuildStep.expandPathsToFiles(srcPaths) + jarPath = resolvePath(config, self.package.getClassesJarPath()) + objPath = resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "obj"]) + classPaths = [objPath] + [resolvePath(config, lib.getClassesJarPath()) for lib in self.libraries] + pathSep = ";" if HostInfo.getOs() == HostInfo.OS_WINDOWS else ":" + + if os.path.exists(objPath): + shutil.rmtree(objPath) + + os.makedirs(objPath) + + for srcFile in srcFiles: + executeAndLog(config, [ + "javac", + "-source", "1.7", + "-target", "1.7", + "-d", objPath, + "-bootclasspath", config.env.sdk.getPlatformLibrary(config.javaApi), + "-classpath", pathSep.join(classPaths), + "-sourcepath", pathSep.join(srcPaths), + srcFile + ]) + + if not os.path.exists(os.path.dirname(jarPath)): + os.makedirs(os.path.dirname(jarPath)) + + try: + pushWorkingDir(objPath) + executeAndLog(config, [ + "jar", + "cf", + jarPath, + "." + ]) + finally: + popWorkingDir() + +class BuildDex (BuildStep): + def __init__ (self, package, libraries): + self.package = package + self.libraries = libraries + + def getInputs (self): + return [self.package.getClassesJarPath()] + [lib.getClassesJarPath() for lib in self.libraries] + + def getOutputs (self): + return [self.package.getClassesDexPath()] + + def update (self, config): + dxPath = which("dx", [config.env.sdk.getBuildToolsPath()]) + srcPaths = resolvePaths(config, self.getInputs()) + dexPath = resolvePath(config, self.package.getClassesDexPath()) + jarPaths = [resolvePath(config, self.package.getClassesJarPath())] + + for lib in self.libraries: + jarPaths.append(resolvePath(config, lib.getClassesJarPath())) + + executeAndLog(config, [ + dxPath, + "--dex", + "--output", dexPath + ] + jarPaths) + +class CreateKeystore (BuildStep): + def __init__ (self): + self.keystorePath = [BuildRoot(), "debug.keystore"] + + def getOutputs (self): + return [self.keystorePath] + + def isUpToDate (self, config): + return os.path.exists(resolvePath(config, self.keystorePath)) + + def update (self, config): + executeAndLog(config, [ + "keytool", + "-genkey", + "-keystore", resolvePath(config, self.keystorePath), + "-storepass", "android", + "-alias", "androiddebugkey", + "-keypass", "android", + "-keyalg", "RSA", + "-keysize", "2048", + "-validity", "10000", + "-dname", "CN=, OU=, O=, L=, S=, C=", + ]) + +# Builds APK without code +class BuildBaseAPK (BuildStep): + def __init__ (self, package, libraries = []): + self.package = package + self.libraries = libraries + self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "base.apk"] + + def getResPaths (self): + paths = [] + for pkg in [self.package] + self.libraries: + if pkg.hasResources: + paths.append(pkg.getResPath()) + return paths + + def getInputs (self): + return [self.package.getManifestPath()] + self.getResPaths() + + def getOutputs (self): + return [self.dstPath] + + def update (self, config): + aaptPath = which("aapt", [config.env.sdk.getBuildToolsPath()]) + dstPath = resolvePath(config, self.dstPath) + + if not os.path.exists(os.path.dirname(dstPath)): + os.makedirs(os.path.dirname(dstPath)) + + args = [ + aaptPath, + "package", + "-f", + "-M", resolvePath(config, self.package.getManifestPath()), + "-I", config.env.sdk.getPlatformLibrary(config.javaApi), + "-F", dstPath, + ] + + for resPath in self.getResPaths(): + args += ["-S", resolvePath(config, resPath)] + + if config.verbose: + args.append("-v") + + executeAndLog(config, args) + +def addFilesToAPK (config, apkPath, baseDir, relFilePaths): + aaptPath = which("aapt", [config.env.sdk.getBuildToolsPath()]) + maxBatchSize = 25 + + pushWorkingDir(baseDir) + try: + workQueue = list(relFilePaths) + + while len(workQueue) > 0: + batchSize = min(len(workQueue), maxBatchSize) + items = workQueue[0:batchSize] + + executeAndLog(config, [ + aaptPath, + "add", + "-f", apkPath, + ] + items) + + del workQueue[0:batchSize] + finally: + popWorkingDir() + +def addFileToAPK (config, apkPath, baseDir, relFilePath): + addFilesToAPK(config, apkPath, baseDir, [relFilePath]) + +class AddJavaToAPK (BuildStep): + def __init__ (self, package): + self.package = package + self.srcPath = BuildBaseAPK(self.package).getOutputs()[0] + self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "with-java.apk"] + + def getInputs (self): + return [ + self.srcPath, + self.package.getClassesDexPath(), + ] + + def getOutputs (self): + return [self.dstPath] + + def update (self, config): + srcPath = resolvePath(config, self.srcPath) + dstPath = resolvePath(config, self.getOutputs()[0]) + dexPath = resolvePath(config, self.package.getClassesDexPath()) + + shutil.copyfile(srcPath, dstPath) + addFileToAPK(config, dstPath, os.path.dirname(dexPath), os.path.basename(dexPath)) + +class AddAssetsToAPK (BuildStep): + def __init__ (self, package, abi): + self.package = package + self.buildPath = [NativeBuildPath(abi)] + self.srcPath = AddJavaToAPK(self.package).getOutputs()[0] + self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "with-assets.apk"] + + def getInputs (self): + return [ + self.srcPath, + self.buildPath + ["assets"] + ] + + def getOutputs (self): + return [self.dstPath] + + @staticmethod + def getAssetFiles (buildPath): + allFiles = BuildStep.expandPathsToFiles([os.path.join(buildPath, "assets")]) + return [os.path.relpath(p, buildPath) for p in allFiles] + + def update (self, config): + srcPath = resolvePath(config, self.srcPath) + dstPath = resolvePath(config, self.getOutputs()[0]) + buildPath = resolvePath(config, self.buildPath) + assetFiles = AddAssetsToAPK.getAssetFiles(buildPath) + + shutil.copyfile(srcPath, dstPath) + + addFilesToAPK(config, dstPath, buildPath, assetFiles) + +class AddNativeLibsToAPK (BuildStep): + def __init__ (self, package, abis): + self.package = package + self.abis = abis + self.srcPath = AddAssetsToAPK(self.package, "").getOutputs()[0] + self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "with-native-libs.apk"] + + def getInputs (self): + paths = [self.srcPath] + for abi in self.abis: + paths.append([NativeBuildPath(abi), "libdeqp.so"]) + return paths + + def getOutputs (self): + return [self.dstPath] + + def update (self, config): + srcPath = resolvePath(config, self.srcPath) + dstPath = resolvePath(config, self.getOutputs()[0]) + pkgPath = resolvePath(config, [BuildRoot(), self.package.getAppDirName()]) + libFiles = [] + + # Create right directory structure first + for abi in self.abis: + libSrcPath = resolvePath(config, [NativeBuildPath(abi), "libdeqp.so"]) + libRelPath = os.path.join("lib", abi, "libdeqp.so") + libAbsPath = os.path.join(pkgPath, libRelPath) + + if not os.path.exists(os.path.dirname(libAbsPath)): + os.makedirs(os.path.dirname(libAbsPath)) + + shutil.copyfile(libSrcPath, libAbsPath) + libFiles.append(libRelPath) + + shutil.copyfile(srcPath, dstPath) + addFilesToAPK(config, dstPath, pkgPath, libFiles) + +class SignAPK (BuildStep): + def __init__ (self, package): + self.package = package + self.srcPath = AddNativeLibsToAPK(self.package, []).getOutputs()[0] + self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "signed.apk"] + self.keystorePath = CreateKeystore().getOutputs()[0] + + def getInputs (self): + return [self.srcPath, self.keystorePath] + + def getOutputs (self): + return [self.dstPath] + + def update (self, config): + srcPath = resolvePath(config, self.srcPath) + dstPath = resolvePath(config, self.dstPath) + + executeAndLog(config, [ + "jarsigner", + "-keystore", resolvePath(config, self.keystorePath), + "-storepass", "android", + "-keypass", "android", + "-signedjar", dstPath, + srcPath, + "androiddebugkey" + ]) + +def getBuildRootRelativeAPKPath (package): + return os.path.join(package.getAppDirName(), package.getAppName() + ".apk") + +class FinalizeAPK (BuildStep): + def __init__ (self, package): + self.package = package + self.srcPath = SignAPK(self.package).getOutputs()[0] + self.dstPath = [BuildRoot(), getBuildRootRelativeAPKPath(self.package)] + self.keystorePath = CreateKeystore().getOutputs()[0] + + def getInputs (self): + return [self.srcPath] + + def getOutputs (self): + return [self.dstPath] + + def update (self, config): + srcPath = resolvePath(config, self.srcPath) + dstPath = resolvePath(config, self.dstPath) + zipalignPath = os.path.join(config.env.sdk.getBuildToolsPath(), "zipalign") + + executeAndLog(config, [ + zipalignPath, + "-f", "4", + srcPath, + dstPath + ]) + +def getBuildStepsForPackage (abis, package, libraries = []): + steps = [] + + assert len(abis) > 0 + + # Build native code first + for abi in abis: + steps += [BuildNativeLibrary(abi)] + + # Build library packages + for library in libraries: + if library.hasResources: + steps.append(GenResourcesSrc(library)) + steps.append(BuildJavaSource(library)) + + # Build main package .java sources + if package.hasResources: + steps.append(GenResourcesSrc(package)) + steps.append(BuildJavaSource(package, libraries)) + steps.append(BuildDex(package, libraries)) + + # Build base APK + steps.append(BuildBaseAPK(package, libraries)) + steps.append(AddJavaToAPK(package)) + + # Add assets from first ABI + steps.append(AddAssetsToAPK(package, abis[0])) + + # Add native libs to APK + steps.append(AddNativeLibsToAPK(package, abis)) + + # Finalize APK + steps.append(CreateKeystore()) + steps.append(SignAPK(package)) + steps.append(FinalizeAPK(package)) + + return steps + +def getPackageAndLibrariesForTarget (target): + deqpPackage = PackageDescription("package", "dEQP") + ctsPackage = PackageDescription("openglcts", "Khronos-CTS", hasResources = False) + + if target == 'deqp': + return (deqpPackage, []) + elif target == 'openglcts': + return (ctsPackage, [deqpPackage]) + else: + raise Exception("Uknown target '%s'" % target) + +def findNDK (): + ndkBuildPath = which('ndk-build') + if ndkBuildPath != None: + return os.path.dirname(ndkBuildPath) + else: + return None + +def findSDK (): + sdkBuildPath = which('android') + if sdkBuildPath != None: + return os.path.dirname(os.path.dirname(sdkBuildPath)) + else: + return None + +def getDefaultBuildRoot (): + return os.path.join(tempfile.gettempdir(), "deqp-android-build") + +def parseArgs (): + nativeBuildTypes = ['Release', 'Debug', 'MinSizeRel', 'RelWithAsserts', 'RelWithDebInfo'] + defaultNDKPath = findNDK() + defaultSDKPath = findSDK() + defaultBuildRoot = getDefaultBuildRoot() + + parser = argparse.ArgumentParser(os.path.basename(__file__), + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--native-build-type', + dest='nativeBuildType', + default="RelWithAsserts", + choices=nativeBuildTypes, + help="Native code build type") + parser.add_argument('--build-root', + dest='buildRoot', + default=defaultBuildRoot, + help="Root build directory") + parser.add_argument('--abis', + dest='abis', + default=",".join(NDKEnv.getKnownAbis()), + help="ABIs to build") + parser.add_argument('--sdk', + dest='sdkPath', + default=defaultSDKPath, + help="Android SDK path", + required=(True if defaultSDKPath == None else False)) + parser.add_argument('--ndk', + dest='ndkPath', + default=defaultNDKPath, + help="Android NDK path", + required=(True if defaultNDKPath == None else False)) + parser.add_argument('-v', '--verbose', + dest='verbose', + help="Verbose output", + default=False, + action='store_true') + parser.add_argument('--target', + dest='target', + help='Build target', + choices=['deqp', 'openglcts'], + default='deqp') + parser.add_argument('--kc-cts-target', + dest='gtfTarget', + default='gles32', + choices=['gles32', 'gles31', 'gles3', 'gles2', 'gl'], + help="KC-CTS (GTF) target API (only used in openglcts target)") + + args = parser.parse_args() + + def parseAbis (abisStr): + knownAbis = set(NDKEnv.getKnownAbis()) + abis = [] + + for abi in abisStr.split(','): + abi = abi.strip() + if not abi in knownAbis: + raise Exception("Unknown ABI: %s" % abi) + abis.append(abi) + + return abis + + # Custom parsing & checks + try: + args.abis = parseAbis(args.abis) + if len(args.abis) == 0: + raise Exception("--abis can't be empty") + except Exception as e: + print "ERROR: %s" % str(e) + parser.print_help() + sys.exit(-1) + + return args + +if __name__ == "__main__": + args = parseArgs() + + ndk = NDKEnv(os.path.realpath(args.ndkPath)) + sdk = SDKEnv(os.path.realpath(args.sdkPath)) + buildPath = os.path.realpath(args.buildRoot) + env = Environment(sdk, ndk) + config = Configuration(env, buildPath, abis=args.abis, nativeBuildType=args.nativeBuildType, gtfTarget=args.gtfTarget, verbose=args.verbose) + + try: + config.check() + except Exception as e: + print "ERROR: %s" % str(e) + print "" + print "Please check your configuration:" + print " --sdk=%s" % args.sdkPath + print " --ndk=%s" % args.ndkPath + sys.exit(-1) + + pkg, libs = getPackageAndLibrariesForTarget(args.target) + steps = getBuildStepsForPackage(config.abis, pkg, libs) + + executeSteps(config, steps) + + print "" + print "Built %s" % os.path.join(buildPath, getBuildRootRelativeAPKPath(pkg)) diff --git a/scripts/android/install_apk.py b/scripts/android/install_apk.py new file mode 100644 index 000000000..a3558ee89 --- /dev/null +++ b/scripts/android/install_apk.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +#------------------------------------------------------------------------- +# drawElements Quality Program utilities +# -------------------------------------- +# +# Copyright 2017 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#------------------------------------------------------------------------- + +import os +import re +import sys +import argparse +import threading +import subprocess + +from build_apk import findSDK +from build_apk import getDefaultBuildRoot +from build_apk import getPackageAndLibrariesForTarget +from build_apk import getBuildRootRelativeAPKPath +from build_apk import parsePackageName + +# Import from <root>/scripts +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + +from build.common import * + +class Device: + def __init__(self, serial, product, model, device): + self.serial = serial + self.product = product + self.model = model + self.device = device + + def __str__ (self): + return "%s: {product: %s, model: %s, device: %s}" % (self.serial, self.product, self.model, self.device) + +def getDevices (adbPath): + proc = subprocess.Popen([adbPath, 'devices', '-l'], stdout=subprocess.PIPE) + (stdout, stderr) = proc.communicate() + + if proc.returncode != 0: + raise Exception("adb devices -l failed, got %d" % proc.returncode) + + ptrn = re.compile(r'^([a-zA-Z0-9\.:]+)\s+.*product:([^\s]+)\s+model:([^\s]+)\s+device:([^\s]+)') + devices = [] + for line in stdout.splitlines()[1:]: + if len(line.strip()) == 0: + continue + + m = ptrn.match(line) + if m == None: + print "WARNING: Failed to parse device info '%s'" % line + continue + + devices.append(Device(m.group(1), m.group(2), m.group(3), m.group(4))) + + return devices + +def execWithPrintPrefix (args, linePrefix="", failOnNonZeroExit=True): + + def readApplyPrefixAndPrint (source, prefix, sink): + while True: + line = source.readline() + if len(line) == 0: # EOF + break; + sink.write(prefix + line) + + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdoutJob = threading.Thread(target=readApplyPrefixAndPrint, args=(process.stdout, linePrefix, sys.stdout)) + stderrJob = threading.Thread(target=readApplyPrefixAndPrint, args=(process.stderr, linePrefix, sys.stderr)) + stdoutJob.start() + stderrJob.start() + retcode = process.wait() + if failOnNonZeroExit and retcode != 0: + raise Exception("Failed to execute '%s', got %d" % (str(args), retcode)) + +def serialApply (f, argsList): + for args in argsList: + f(*args) + +def parallelApply (f, argsList): + class ErrorCode: + def __init__ (self): + self.error = None; + + def applyAndCaptureError (func, args, errorCode): + try: + func(*args) + except: + errorCode.error = sys.exc_info() + + errorCode = ErrorCode() + jobs = [] + for args in argsList: + job = threading.Thread(target=applyAndCaptureError, args=(f, args, errorCode)) + job.start() + jobs.append(job) + + for job in jobs: + job.join() + + if errorCode.error: + raise errorCode.error[0], errorCode.error[1], errorCode.error[2] + +def uninstall (adbPath, packageName, extraArgs = [], printPrefix=""): + print printPrefix + "Removing existing %s...\n" % packageName, + execWithPrintPrefix([adbPath] + extraArgs + [ + 'uninstall', + packageName + ], printPrefix, failOnNonZeroExit=False) + print printPrefix + "Remove complete\n", + +def install (adbPath, apkPath, extraArgs = [], printPrefix=""): + print printPrefix + "Installing %s...\n" % apkPath, + execWithPrintPrefix([adbPath] + extraArgs + [ + 'install', + apkPath + ], printPrefix) + print printPrefix + "Install complete\n", + +def installToDevice (device, adbPath, packageName, apkPath, printPrefix=""): + if len(printPrefix) == 0: + print "Installing to %s (%s)...\n" % (device.serial, device.model), + else: + print printPrefix + "Installing to %s\n" % device.serial, + + uninstall(adbPath, packageName, ['-s', device.serial], printPrefix) + install(adbPath, apkPath, ['-s', device.serial], printPrefix) + +def installToDevices (devices, doParallel, adbPath, packageName, apkPath): + padLen = max([len(device.model) for device in devices])+1 + if doParallel: + parallelApply(installToDevice, [(device, adbPath, packageName, apkPath, ("(%s):%s" % (device.model, ' ' * (padLen - len(device.model))))) for device in devices]); + else: + serialApply(installToDevice, [(device, adbPath, packageName, apkPath) for device in devices]); + +def installToAllDevices (doParallel, adbPath, packageName, apkPath): + devices = getDevices(adbPath) + installToDevices(devices, doParallel, adbPath, packageName, apkPath) + +def getAPKPath (buildRootPath, target): + package = getPackageAndLibrariesForTarget(target)[0] + return os.path.join(buildRootPath, getBuildRootRelativeAPKPath(package)) + +def getPackageName (target): + package = getPackageAndLibrariesForTarget(target)[0] + manifestPath = os.path.join(DEQP_DIR, "android", package.appDirName, "AndroidManifest.xml") + + return parsePackageName(manifestPath) + +def findADB (): + adbInPath = which("adb") + if adbInPath != None: + return adbInPath + + sdkPath = findSDK() + if sdkPath != None: + adbInSDK = os.path.join(sdkPath, "platform-tools", "adb") + if os.path.isfile(adbInSDK): + return adbInSDK + + return None + +def parseArgs (): + defaultADBPath = findADB() + defaultBuildRoot = getDefaultBuildRoot() + + parser = argparse.ArgumentParser(os.path.basename(__file__), + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--build-root', + dest='buildRoot', + default=defaultBuildRoot, + help="Root build directory") + parser.add_argument('--adb', + dest='adbPath', + default=defaultADBPath, + help="ADB binary path", + required=(True if defaultADBPath == None else False)) + parser.add_argument('--target', + dest='target', + help='Build target', + choices=['deqp', 'openglcts'], + default='deqp') + parser.add_argument('-p', '--parallel', + dest='doParallel', + action="store_true", + help="Install package in parallel") + parser.add_argument('-s', '--serial', + dest='serial', + type=str, + nargs='+', + help="Install package to device with serial number") + parser.add_argument('-a', '--all', + dest='all', + action="store_true", + help="Install to all devices") + + return parser.parse_args() + +if __name__ == "__main__": + args = parseArgs() + packageName = getPackageName(args.target) + apkPath = getAPKPath(args.buildRoot, args.target) + + if not os.path.isfile(apkPath): + die("%s does not exist" % apkPath) + + if args.all: + installToAllDevices(args.doParallel, args.adbPath, packageName, apkPath) + else: + if args.serial == None: + devices = getDevices(args.adbPath) + if len(devices) == 0: + die('No devices connected') + elif len(devices) == 1: + installToDevice(devices[0], args.adbPath, packageName, apkPath) + else: + print "More than one device connected:" + for i in range(0, len(devices)): + print "%3d: %16s %s" % ((i+1), devices[i].serial, devices[i].model) + + deviceNdx = int(raw_input("Choose device (1-%d): " % len(devices))) + installToDevice(devices[deviceNdx-1], args.adbPath, packageName, apkPath) + else: + devices = getDevices(args.adbPath) + + devices = [dev for dev in devices if dev.serial in args.serial] + devSerials = [dev.serial for dev in devices] + notFounds = [serial for serial in args.serial if not serial in devSerials] + + for notFound in notFounds: + print("Couldn't find device matching serial '%s'" % notFound) + + installToDevices(devices, args.doParallel, args.adbPath, packageName, apkPath) |