akvirtualcamera/ports/deploy/deploy_android.py
2020-06-05 19:09:17 -03:00

634 lines
22 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Webcamoid, webcam capture application.
# Copyright (C) 2019 Gonzalo Exequiel Pedone
#
# Webcamoid is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Webcamoid is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Webcamoid. If not, see <http://www.gnu.org/licenses/>.
#
# Web-Site: http://webcamoid.github.io/
import json
import math
import os
import re
import shutil
import subprocess # nosec
import sys
import threading
import zipfile
import deploy_base
import tools.android
import tools.binary_elf
import tools.qt5
class Deploy(deploy_base.DeployBase,
tools.qt5.DeployToolsQt,
tools.android.AndroidTools):
def __init__(self):
super().__init__()
self.targetSystem = 'android'
self.installDir = os.path.join(self.buildDir, 'ports/deploy/temp_priv')
self.pkgsDir = os.path.join(self.buildDir, 'ports/deploy/packages_auto/android')
self.standAloneDir = os.path.join(self.buildDir, 'StandAlone')
self.detectQt(self.standAloneDir)
self.programName = 'webcamoid'
self.binarySolver = tools.binary_elf.DeployToolsBinary()
self.detectAndroidPlatform(self.standAloneDir)
binary = self.detectTargetBinaryFromQt5Make(self.standAloneDir)
self.targetArch = self.binarySolver.machineEMCode(binary)
self.androidArchMap = {'AARCH64': 'arm64-v8a',
'ARM' : 'armeabi-v7a',
'386' : 'x86',
'X86_64' : 'x86_64'}
if self.targetArch in self.androidArchMap:
self.targetArch = self.androidArchMap[self.targetArch]
self.binarySolver.sysBinsPath = self.detectBinPaths() + self.binarySolver.sysBinsPath
self.binarySolver.libsSeachPaths = self.detectLibPaths()
self.rootInstallDir = os.path.join(self.installDir, self.programName)
self.libInstallDir = os.path.join(self.rootInstallDir,
'libs',
self.targetArch)
self.binaryInstallDir = self.libInstallDir
self.assetsIntallDir = os.path.join(self.rootInstallDir,
'assets',
'android_rcc_bundle')
self.qmlInstallDir = os.path.join(self.assetsIntallDir, 'qml')
self.pluginsInstallDir = os.path.join(self.assetsIntallDir, 'plugins')
self.qtConf = os.path.join(self.binaryInstallDir, 'qt.conf')
self.qmlRootDirs = ['StandAlone/share/qml', 'libAvKys/Plugins']
self.mainBinary = os.path.join(self.binaryInstallDir, os.path.basename(binary))
self.programVersion = self.detectVersion(os.path.join(self.rootDir, 'commons.pri'))
self.detectMake()
self.binarySolver.readExcludeList(os.path.join(self.rootDir, 'ports/deploy/exclude.android.txt'))
self.binarySolver.libsSeachPaths += [self.qmakeQuery(var='QT_INSTALL_LIBS')]
self.packageConfig = os.path.join(self.rootDir, 'ports/deploy/package_info.conf')
self.dependencies = []
@staticmethod
def removeUnneededFiles(path):
afiles = set()
for root, _, files in os.walk(path):
for f in files:
if f.endswith('.jar'):
afiles.add(os.path.join(root, f))
for afile in afiles:
os.remove(afile)
def prepare(self):
print('Executing make install')
self.makeInstall(self.buildDir, self.rootInstallDir)
self.binarySolver.detectStrip()
if 'PACKAGES_MERGE' in os.environ \
and len(os.environ['PACKAGES_MERGE']) > 0:
self.outPackage = \
os.path.join(self.pkgsDir,
'{}-{}.apk'.format(self.programName,
self.programVersion))
else:
self.outPackage = \
os.path.join(self.pkgsDir,
'{}-{}-{}.apk'.format(self.programName,
self.programVersion,
self.targetArch))
print('Copying Qml modules\n')
self.solvedepsQml()
print('\nCopying required plugins\n')
self.solvedepsPlugins()
print('\nRemoving unused architectures')
self.removeInvalidArchs()
print('Fixing Android libs\n')
self.fixQtLibs()
try:
shutil.rmtree(self.pluginsInstallDir)
except:
pass
print('\nCopying required libs\n')
self.solvedepsLibs()
print('\nSolving Android dependencies\n')
self.solvedepsAndroid()
print('\nCopying Android build templates')
self.copyAndroidTemplates()
print('Fixing libs.xml file')
self.fixLibsXml()
print('Creating .rcc bundle file')
self.createRccBundle()
print('Stripping symbols')
self.binarySolver.stripSymbols(self.rootInstallDir)
print('Removing unnecessary files')
self.removeUnneededFiles(self.libInstallDir)
print('Writting build system information\n')
self.writeBuildInfo()
def removeInvalidArchs(self):
suffix = '_{}.so'.format(self.targetArch)
if not self.mainBinary.endswith(suffix):
return
for root, dirs, files in os.walk(self.assetsIntallDir):
for f in files:
if f.endswith('.so') and not f.endswith(suffix):
os.remove(os.path.join(root, f))
def solvedepsLibs(self):
qtLibsPath = self.qmakeQuery(var='QT_INSTALL_LIBS')
self.binarySolver.ldLibraryPath.append(qtLibsPath)
self.qtLibs = sorted(self.binarySolver.scanDependencies(self.rootInstallDir))
for dep in self.qtLibs:
depPath = os.path.join(self.libInstallDir, os.path.basename(dep))
if dep != depPath:
print(' {} -> {}'.format(dep, depPath))
self.copy(dep, depPath, True)
self.dependencies.append(dep)
def searchPackageFor(self, path):
os.environ['LC_ALL'] = 'C'
pacman = self.whereBin('pacman')
if len(pacman) > 0:
process = subprocess.Popen([pacman, '-Qo', path], # nosec
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, _ = process.communicate()
if process.returncode != 0:
return ''
info = stdout.decode(sys.getdefaultencoding()).split(' ')
if len(info) < 2:
return ''
package, version = info[-2:]
return ' '.join([package.strip(), version.strip()])
dpkg = self.whereBin('dpkg')
if len(dpkg) > 0:
process = subprocess.Popen([dpkg, '-S', path], # nosec
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, _ = process.communicate()
if process.returncode != 0:
return ''
package = stdout.split(b':')[0].decode(sys.getdefaultencoding()).strip()
process = subprocess.Popen([dpkg, '-s', package], # nosec
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, _ = process.communicate()
if process.returncode != 0:
return ''
for line in stdout.decode(sys.getdefaultencoding()).split('\n'):
line = line.strip()
if line.startswith('Version:'):
return ' '.join([package, line.split()[1].strip()])
return ''
rpm = self.whereBin('rpm')
if len(rpm) > 0:
process = subprocess.Popen([rpm, '-qf', path], # nosec
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, _ = process.communicate()
if process.returncode != 0:
return ''
return stdout.decode(sys.getdefaultencoding()).strip()
return ''
def commitHash(self):
try:
process = subprocess.Popen(['git', 'rev-parse', 'HEAD'], # nosec
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.rootDir)
stdout, _ = process.communicate()
if process.returncode != 0:
return ''
return stdout.decode(sys.getdefaultencoding()).strip()
except:
return ''
@staticmethod
def sysInfo():
info = ''
for f in os.listdir('/etc'):
if f.endswith('-release'):
with open(os.path.join('/etc' , f)) as releaseFile:
info += releaseFile.read()
return info
def writeBuildInfo(self):
shareDir = os.path.join(self.rootInstallDir, 'assets')
try:
os.makedirs(self.pkgsDir)
except:
pass
depsInfoFile = os.path.join(shareDir, 'build-info.txt')
if not os.path.exists(shareDir):
os.makedirs(shareDir)
# Write repository info.
with open(depsInfoFile, 'w') as f:
commitHash = self.commitHash()
if len(commitHash) < 1:
commitHash = 'Unknown'
print(' Commit hash: ' + commitHash)
f.write('Commit hash: ' + commitHash + '\n')
buildLogUrl = ''
if 'TRAVIS_BUILD_WEB_URL' in os.environ:
buildLogUrl = os.environ['TRAVIS_BUILD_WEB_URL']
elif 'APPVEYOR_ACCOUNT_NAME' in os.environ and 'APPVEYOR_PROJECT_NAME' in os.environ and 'APPVEYOR_JOB_ID' in os.environ:
buildLogUrl = 'https://ci.appveyor.com/project/{}/{}/build/job/{}'.format(os.environ['APPVEYOR_ACCOUNT_NAME'],
os.environ['APPVEYOR_PROJECT_SLUG'],
os.environ['APPVEYOR_JOB_ID'])
if len(buildLogUrl) > 0:
print(' Build log URL: ' + buildLogUrl)
f.write('Build log URL: ' + buildLogUrl + '\n')
print()
f.write('\n')
# Write host info.
info = self.sysInfo()
with open(depsInfoFile, 'a') as f:
for line in info.split('\n'):
if len(line) > 0:
print(' ' + line)
f.write(line + '\n')
print()
f.write('\n')
# Write SDK and NDK info.
sdkInfoFile = os.path.join(self.androidSDK, 'tools', 'source.properties')
ndkInfoFile = os.path.join(self.androidNDK, 'source.properties')
with open(depsInfoFile, 'a') as f:
platform = self.androidPlatform
print(' Android Platform: {}'.format(platform))
f.write('Android Platform: {}\n'.format(platform))
print(' SDK Info: \n')
f.write('SDK Info: \n\n')
with open(sdkInfoFile) as sdkf:
for line in sdkf:
if len(line) > 0:
print(' ' + line.strip())
f.write(' ' + line)
print('\n NDK Info: \n')
f.write('\nNDK Info: \n\n')
with open(ndkInfoFile) as ndkf:
for line in ndkf:
if len(line) > 0:
print(' ' + line.strip())
f.write(' ' + line)
print()
f.write('\n')
# Write binary dependencies info.
packages = set()
for dep in self.dependencies:
packageInfo = self.searchPackageFor(dep)
if len(packageInfo) > 0:
packages.add(packageInfo)
packages = sorted(packages)
with open(depsInfoFile, 'a') as f:
for packge in packages:
print(' ' + packge)
f.write(packge + '\n')
@staticmethod
def hrSize(size):
i = int(math.log(size) // math.log(1024))
if i < 1:
return '{} B'.format(size)
units = ['KiB', 'MiB', 'GiB', 'TiB']
sizeKiB = size / (1024 ** i)
return '{:.2f} {}'.format(sizeKiB, units[i - 1])
def printPackageInfo(self, path):
if os.path.exists(path):
print(' ',
os.path.basename(path),
self.hrSize(os.path.getsize(path)))
print(' sha256sum:', Deploy.sha256sum(path))
else:
print(' ',
os.path.basename(path),
'FAILED')
def alignPackage(self, package):
deploymentSettingsPath = ''
for f in os.listdir(self.standAloneDir):
if re.match('^android-.+-deployment-settings.json$' , f):
deploymentSettingsPath = os.path.join(self.standAloneDir, f)
break
if len(deploymentSettingsPath) < 1:
return
with open(deploymentSettingsPath) as f:
deploymentSettings = json.load(f)
zipalign = os.path.join(self.androidSDK,
'build-tools',
deploymentSettings['sdkBuildToolsRevision'],
'zipalign')
if self.system == 'windows':
zipalign += '.exe'
alignedPackage = os.path.join(os.path.dirname(package),
'aligned-' + os.path.basename(package))
process = subprocess.Popen([zipalign, # nosec
'-v',
'-f', '4',
package,
alignedPackage],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
process.communicate()
if process.returncode != 0:
return False
self.move(alignedPackage, package)
return True
def apkSignPackage(self, package, keystore):
if not self.alignPackage(package):
return False
deploymentSettingsPath = ''
for f in os.listdir(self.standAloneDir):
if re.match('^android-.+-deployment-settings.json$' , f):
deploymentSettingsPath = os.path.join(self.standAloneDir, f)
break
if len(deploymentSettingsPath) < 1:
return
with open(deploymentSettingsPath) as f:
deploymentSettings = json.load(f)
apkSigner = os.path.join(self.androidSDK,
'build-tools',
deploymentSettings['sdkBuildToolsRevision'],
'apksigner')
if self.system == 'windows':
apkSigner += '.exe'
process = subprocess.Popen([apkSigner, # nosec
'sign',
'-v',
'--ks', keystore,
'--ks-pass', 'pass:android',
'--ks-key-alias', 'androiddebugkey',
'--key-pass', 'pass:android',
package],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
process.communicate()
return process.returncode == 0
def jarSignPackage(self, package, keystore):
jarSigner = 'jarsigner'
if self.system == 'windows':
jarSigner += '.exe'
jarSignerPath = ''
if 'JAVA_HOME' in os.environ:
jarSignerPath = os.path.join(os.environ['JAVA_HOME'],
'bin',
jarSigner)
if len(jarSignerPath) < 1 or not os.path.exists(jarSignerPath):
jarSignerPath = self.whereBin(jarSigner)
if len(jarSignerPath) < 1:
return False
signedPackage = os.path.join(os.path.dirname(package),
'signed-' + os.path.basename(package))
process = subprocess.Popen([jarSignerPath, # nosec
'-verbose',
'-keystore', keystore,
'-storepass', 'android',
'-keypass', 'android',
'-sigalg', 'SHA1withRSA',
'-digestalg', 'SHA1',
'-sigfile', 'CERT',
'-signedjar', signedPackage,
package,
'androiddebugkey'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
process.communicate()
if process.returncode != 0:
return False
self.move(signedPackage, package)
return self.alignPackage(package)
def signPackage(self, package):
keytool = 'keytool'
if self.system == 'windows':
keytool += '.exe'
keytoolPath = ''
if 'JAVA_HOME' in os.environ:
keytoolPath = os.path.join(os.environ['JAVA_HOME'], 'bin', keytool)
if len(keytoolPath) < 1 or not os.path.exists(keytoolPath):
keytoolPath = self.whereBin(keytool)
if len(keytoolPath) < 1:
return False
keystore = os.path.join(self.rootInstallDir, 'debug.keystore')
if 'KEYSTORE_PATH' in os.environ:
keystore = os.environ['KEYSTORE_PATH']
if not os.path.exists(keystore):
try:
os.makedirs(os.path.dirname(keystore))
except:
pass
process = subprocess.Popen([keytoolPath, # nosec
'-genkey',
'-v',
'-storetype', 'pkcs12',
'-keystore', keystore,
'-storepass', 'android',
'-alias', 'androiddebugkey',
'-keypass', 'android',
'-keyalg', 'RSA',
'-keysize', '2048',
'-validity', '10000',
'-dname', 'CN=Android Debug,O=Android,C=US'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
process.communicate()
if process.returncode != 0:
return False
if self.apkSignPackage(package, keystore):
return True
return self.jarSignPackage(package, keystore)
def createApk(self, mutex):
if 'PACKAGES_MERGE' in os.environ:
print('Merging package data:\n')
for path in os.environ['PACKAGES_MERGE'].split(':'):
path = path.strip()
if os.path.exists(path) and os.path.isdir(path):
if path == self.buildDir:
continue
standAlonePath = os.path.join(path,
os.path.relpath(self.standAloneDir,
self.buildDir))
binary = self.detectTargetBinaryFromQt5Make(standAlonePath)
targetArch = self.binarySolver.machineEMCode(binary)
if targetArch in self.androidArchMap:
targetArch = self.androidArchMap[targetArch]
libsPath = os.path.join(path,
os.path.relpath(self.rootInstallDir,
self.buildDir),
'libs',
targetArch)
dstLibPath = os.path.join(self.rootInstallDir,
'libs',
targetArch)
print(' {} -> {}'.format(libsPath, dstLibPath))
self.copy(libsPath, dstLibPath)
print()
if not os.path.exists(self.pkgsDir):
os.makedirs(self.pkgsDir)
gradleSript = os.path.join(self.rootInstallDir, 'gradlew')
if self.system == 'windows':
gradleSript += '.bat'
os.chmod(gradleSript, 0o744)
process = subprocess.Popen([gradleSript, # nosec
'--no-daemon',
'--info',
'assembleRelease'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.rootInstallDir)
process.communicate()
apk = os.path.join(self.rootInstallDir,
'build',
'outputs',
'apk',
'release',
'{}-release-unsigned.apk'.format(self.programName))
self.signPackage(apk)
self.copy(apk, self.outPackage)
print('Created APK package:')
self.printPackageInfo(self.outPackage)
def package(self):
mutex = threading.Lock()
threads = [threading.Thread(target=self.createApk, args=(mutex,))]
packagingTools = ['apk']
if len(packagingTools) > 0:
print('Detected packaging tools: {}\n'.format(', '.join(packagingTools)))
for thread in threads:
thread.start()
for thread in threads:
thread.join()