1050 lines
30 KiB
Python
1050 lines
30 KiB
Python
# -*- 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 glob
|
|
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, desired_version):
|
|
self.path = path
|
|
self.buildToolsVersion = SDKEnv.selectBuildToolsVersion(self.path, desired_version)
|
|
|
|
@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[int(m.group(1))] = (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
|
|
|
return versions
|
|
|
|
@staticmethod
|
|
def selectBuildToolsVersion (path, preferred):
|
|
versions = SDKEnv.getBuildToolsVersions(path)
|
|
|
|
if len(versions) == 0:
|
|
return (0,0,0)
|
|
|
|
if preferred == -1:
|
|
return max(versions.values())
|
|
|
|
if preferred in versions:
|
|
return versions[preferred]
|
|
|
|
# Pick newest
|
|
newest_version = max(versions.values())
|
|
print("Couldn't find Android Tool version %d, %d was selected." % (preferred, newest_version[0]))
|
|
return newest_version
|
|
|
|
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 = list(map(lambda x: x.strip(), 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, nativeApi, javaApi, minApi, nativeBuildType, gtfTarget, verbose, layers, angle):
|
|
self.env = env
|
|
self.sourcePath = DEQP_DIR
|
|
self.buildPath = buildPath
|
|
self.abis = abis
|
|
self.nativeApi = nativeApi
|
|
self.javaApi = javaApi
|
|
self.minApi = minApi
|
|
self.nativeBuildType = nativeBuildType
|
|
self.gtfTarget = gtfTarget
|
|
self.verbose = verbose
|
|
self.layers = layers
|
|
self.angle = angle
|
|
self.dCompilerName = "d8"
|
|
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)
|
|
|
|
if self.env.ndk.version[0] < 15:
|
|
raise Exception("Android NDK version %d is not supported; build requires NDK version >= 15" % (self.env.ndk.version[0]))
|
|
|
|
if not (self.minApi <= self.javaApi <= self.nativeApi):
|
|
raise Exception("Requires: min-api (%d) <= java-api (%d) <= native-api (%d)" % (self.minApi, self.javaApi, self.nativeApi))
|
|
|
|
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"))
|
|
|
|
if not os.path.exists(os.path.join(self.env.sdk.path, "platforms", "android-%d" % self.javaApi)):
|
|
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")))
|
|
|
|
# Try to find first d8 since dx was deprecated
|
|
if which(self.dCompilerName, [self.env.sdk.getBuildToolsPath()]) == None:
|
|
print("Couldn't find %s, will try to find dx", self.dCompilerName)
|
|
self.dCompilerName = "dx"
|
|
|
|
androidBuildTools = ["aapt", "zipalign", "apksigner", self.dCompilerName]
|
|
for tool in androidBuildTools:
|
|
if which(tool, [self.env.sdk.getBuildToolsPath()]) == None:
|
|
raise Exception("Missing Android build tool: %s in %s" % (tool, self.env.sdk.getBuildToolsPath()))
|
|
|
|
requiredToolsInPath = ["javac", "jar", "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 = list(filter(os.path.exists, expandedInputs))
|
|
existingOutputs = list(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 clearCMakeCacheVariables(args):
|
|
# New value, so clear the necessary cmake variables
|
|
args.append('-UANGLE_LIBS')
|
|
args.append('-UGLES1_LIBRARY')
|
|
args.append('-UGLES2_LIBRARY')
|
|
args.append('-UEGL_LIBRARY')
|
|
|
|
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):
|
|
args = ['-DDEQP_TARGET=android',
|
|
'-DDEQP_TARGET_TOOLCHAIN=ndk-modern',
|
|
'-DCMAKE_C_FLAGS=-Werror',
|
|
'-DCMAKE_CXX_FLAGS=-Werror',
|
|
'-DANDROID_NDK_PATH=%s' % config.env.ndk.path,
|
|
'-DANDROID_ABI=%s' % abiName,
|
|
'-DDE_ANDROID_API=%s' % config.nativeApi,
|
|
'-DGLCTS_GTF_TARGET=%s' % config.gtfTarget]
|
|
|
|
if config.angle is None:
|
|
# Find any previous builds that may have embedded ANGLE libs and clear the CMake cache
|
|
for abi in NDKEnv.getKnownAbis():
|
|
cMakeCachePath = os.path.join(getNativeBuildPath(config, abi), "CMakeCache.txt")
|
|
try:
|
|
if 'ANGLE_LIBS' in open(cMakeCachePath).read():
|
|
clearCMakeCacheVariables(args)
|
|
except IOError:
|
|
pass
|
|
else:
|
|
cMakeCachePath = os.path.join(getNativeBuildPath(config, abiName), "CMakeCache.txt")
|
|
angleLibsDir = os.path.join(config.angle, abiName)
|
|
# Check if the user changed where the ANGLE libs are being loaded from
|
|
try:
|
|
if angleLibsDir not in open(cMakeCachePath).read():
|
|
clearCMakeCacheVariables(args)
|
|
except IOError:
|
|
pass
|
|
args.append('-DANGLE_LIBS=%s' % angleLibsDir)
|
|
|
|
return args
|
|
|
|
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 getClassesDexDirectory (self):
|
|
return [BuildRoot(), self.appDirName, "bin",]
|
|
|
|
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(config.dCompilerName, [config.env.sdk.getBuildToolsPath()])
|
|
dexPath = resolvePath(config, self.package.getClassesDexDirectory())
|
|
jarPaths = [resolvePath(config, self.package.getClassesJarPath())]
|
|
|
|
for lib in self.libraries:
|
|
jarPaths.append(resolvePath(config, lib.getClassesJarPath()))
|
|
|
|
args = [ dxPath ]
|
|
if config.dCompilerName == "d8":
|
|
args.append("--lib")
|
|
args.append(config.env.sdk.getPlatformLibrary(config.javaApi))
|
|
else:
|
|
args.append("--dex")
|
|
args.append("--output")
|
|
args.append(dexPath)
|
|
|
|
executeAndLog(config, args + 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",
|
|
"-genkeypair",
|
|
"-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",
|
|
"--min-sdk-version", str(config.minApi),
|
|
"--target-sdk-version", str(config.javaApi),
|
|
"-M", resolvePath(config, self.package.getManifestPath()),
|
|
"-I", config.env.sdk.getPlatformLibrary(config.javaApi),
|
|
"-F", dstPath,
|
|
"-0", "arsc" # arsc files need to be uncompressed for SDK version 30 and up
|
|
]
|
|
|
|
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)
|
|
# Workaround for Windows.
|
|
if os.path.sep == "\\":
|
|
workQueue = [i.replace("\\", "/") for i in workQueue]
|
|
|
|
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)
|
|
|
|
if config.layers:
|
|
# Need to copy everything in the layer folder
|
|
layersGlob = os.path.join(config.layers, abi, "*")
|
|
libVkLayers = glob.glob(layersGlob)
|
|
for layer in libVkLayers:
|
|
layerFilename = os.path.basename(layer)
|
|
layerRelPath = os.path.join("lib", abi, layerFilename)
|
|
layerAbsPath = os.path.join(pkgPath, layerRelPath)
|
|
shutil.copyfile(layer, layerAbsPath)
|
|
libFiles.append(layerRelPath)
|
|
print("Adding layer binary: %s" % (layer,))
|
|
|
|
if config.angle:
|
|
angleGlob = os.path.join(config.angle, abi, "lib*_angle.so")
|
|
libAngle = glob.glob(angleGlob)
|
|
for lib in libAngle:
|
|
libFilename = os.path.basename(lib)
|
|
libRelPath = os.path.join("lib", abi, libFilename)
|
|
libAbsPath = os.path.join(pkgPath, libRelPath)
|
|
shutil.copyfile(lib, libAbsPath)
|
|
libFiles.append(libRelPath)
|
|
print("Adding ANGLE binary: %s" % (lib,))
|
|
|
|
shutil.copyfile(srcPath, dstPath)
|
|
addFilesToAPK(config, dstPath, pkgPath, libFiles)
|
|
|
|
class SignAPK (BuildStep):
|
|
def __init__ (self, package):
|
|
self.package = package
|
|
self.srcPath = AlignAPK(self.package).getOutputs()[0]
|
|
self.dstPath = [BuildRoot(), getBuildRootRelativeAPKPath(self.package)]
|
|
self.keystorePath = CreateKeystore().getOutputs()[0]
|
|
|
|
def getInputs (self):
|
|
return [self.srcPath, self.keystorePath]
|
|
|
|
def getOutputs (self):
|
|
return [self.dstPath]
|
|
|
|
def update (self, config):
|
|
apksigner = which("apksigner", [config.env.sdk.getBuildToolsPath()])
|
|
srcPath = resolvePath(config, self.srcPath)
|
|
dstPath = resolvePath(config, self.dstPath)
|
|
|
|
executeAndLog(config, [
|
|
apksigner,
|
|
"sign",
|
|
"--ks", resolvePath(config, self.keystorePath),
|
|
"--ks-key-alias", "androiddebugkey",
|
|
"--ks-pass", "pass:android",
|
|
"--key-pass", "pass:android",
|
|
"--min-sdk-version", str(config.minApi),
|
|
"--max-sdk-version", str(config.javaApi),
|
|
"--out", dstPath,
|
|
srcPath
|
|
])
|
|
|
|
def getBuildRootRelativeAPKPath (package):
|
|
return os.path.join(package.getAppDirName(), package.getAppName() + ".apk")
|
|
|
|
class AlignAPK (BuildStep):
|
|
def __init__ (self, package):
|
|
self.package = package
|
|
self.srcPath = AddNativeLibsToAPK(self.package, []).getOutputs()[0]
|
|
self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "aligned.apk"]
|
|
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(AlignAPK(package))
|
|
steps.append(SignAPK(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('--native-api',
|
|
type=int,
|
|
dest='nativeApi',
|
|
default=28,
|
|
help="Android API level to target in native code")
|
|
parser.add_argument('--java-api',
|
|
type=int,
|
|
dest='javaApi',
|
|
default=28,
|
|
help="Android API level to target in Java code")
|
|
parser.add_argument('--tool-api',
|
|
type=int,
|
|
dest='toolApi',
|
|
default=-1,
|
|
help="Android Tools level to target (-1 being maximum present)")
|
|
parser.add_argument('--min-api',
|
|
type=int,
|
|
dest='minApi',
|
|
default=22,
|
|
help="Minimum Android API level for which the APK can be installed")
|
|
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)")
|
|
parser.add_argument('--layers-path',
|
|
dest='layers',
|
|
default=None,
|
|
required=False)
|
|
parser.add_argument('--angle-path',
|
|
dest='angle',
|
|
default=None,
|
|
required=False)
|
|
|
|
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), args.toolApi)
|
|
buildPath = os.path.realpath(args.buildRoot)
|
|
env = Environment(sdk, ndk)
|
|
config = Configuration(env, buildPath, abis=args.abis, nativeApi=args.nativeApi, javaApi=args.javaApi, minApi=args.minApi, nativeBuildType=args.nativeBuildType, gtfTarget=args.gtfTarget,
|
|
verbose=args.verbose, layers=args.layers, angle=args.angle)
|
|
|
|
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)))
|