1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3 4# Webcamoid, webcam capture application. 5# Copyright (C) 2017 Gonzalo Exequiel Pedone 6# 7# Webcamoid is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# Webcamoid is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with Webcamoid. If not, see <http://www.gnu.org/licenses/>. 19# 20# Web-Site: http://webcamoid.github.io/ 21 22import math 23import os 24import platform 25import subprocess # nosec 26import sys 27import threading 28import zipfile 29 30from WebcamoidDeployTools import DTDeployBase 31from WebcamoidDeployTools import DTQt5 32from WebcamoidDeployTools import DTBinaryPecoff 33 34 35class Deploy(DTDeployBase.DeployBase, DTQt5.Qt5Tools): 36 def __init__(self): 37 super().__init__() 38 rootDir = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..')) 39 self.setRootDir(rootDir) 40 self.installDir = os.path.join(self.buildDir, 'ports/deploy/temp_priv') 41 self.pkgsDir = os.path.join(self.buildDir, 'ports/deploy/packages_auto/windows') 42 self.detectQt(os.path.join(self.buildDir, 'StandAlone')) 43 self.rootInstallDir = os.path.join(self.installDir, 'usr') 44 self.binaryInstallDir = os.path.join(self.rootInstallDir, 'bin') 45 self.libInstallDir = self.qmakeQuery(var='QT_INSTALL_LIBS') \ 46 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 47 self.rootInstallDir) 48 self.libQtInstallDir = self.qmakeQuery(var='QT_INSTALL_ARCHDATA') \ 49 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 50 self.rootInstallDir) 51 self.qmlInstallDir = self.qmakeQuery(var='QT_INSTALL_QML') \ 52 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 53 self.rootInstallDir) 54 self.pluginsInstallDir = self.qmakeQuery(var='QT_INSTALL_PLUGINS') \ 55 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 56 self.rootInstallDir) 57 self.qtConf = os.path.join(self.binaryInstallDir, 'qt.conf') 58 self.qmlRootDirs = ['StandAlone/share/qml', 'libAvKys/Plugins'] 59 self.mainBinary = os.path.join(self.binaryInstallDir, 'webcamoid.exe') 60 self.programName = os.path.splitext(os.path.basename(self.mainBinary))[0] 61 self.programVersion = self.detectVersion(os.path.join(self.rootDir, 'commons.pri')) 62 self.detectMake() 63 xspec = self.qmakeQuery(var='QMAKE_XSPEC') 64 65 if 'android' in xspec: 66 self.targetSystem = 'android' 67 68 self.binarySolver = DTBinaryPecoff.PecoffBinaryTools() 69 self.binarySolver.readExcludes(os.name, sys.platform) 70 self.packageConfig = os.path.join(self.rootDir, 'ports/deploy/package_info.conf') 71 self.dependencies = [] 72 self.installerConfig = os.path.join(self.installDir, 'installer/config') 73 self.installerPackages = os.path.join(self.installDir, 'installer/packages') 74 self.installerIconSize = 256 75 self.appIcon = os.path.join(self.rootDir, 76 'StandAlone/share/icons/hicolor/{1}x{1}/{0}.ico'.format(self.programName, 77 self.installerIconSize)) 78 self.licenseFile = os.path.join(self.rootDir, 'COPYING') 79 self.installerRunProgram = '@TargetDir@/bin/' + self.programName + '.exe' 80 self.installerScript = os.path.join(self.rootDir, 'ports/deploy/installscript.windows.qs') 81 self.changeLog = os.path.join(self.rootDir, 'ChangeLog') 82 83 def prepare(self): 84 print('Executing make install') 85 self.makeInstall(self.buildDir) 86 self.detectTargetArch() 87 88 if self.qtIFWVersion == '' or int(self.qtIFWVersion.split('.')[0]) < 3: 89 appsDir = '@ApplicationsDir@' 90 else: 91 if self.targetArch == '32bit': 92 appsDir = '@ApplicationsDirX86@' 93 else: 94 appsDir = '@ApplicationsDirX64@' 95 96 self.installerTargetDir = appsDir + '/' + self.programName 97 arch = 'win32' if self.targetArch == '32bit' else 'win64' 98 self.outPackage = os.path.join(self.pkgsDir, 99 'webcamoid-{}-{}.exe'.format(self.programVersion, 100 arch)) 101 102 print('Copying Qml modules\n') 103 self.solvedepsQml() 104 print('\nCopying required plugins\n') 105 self.solvedepsPlugins() 106 print('\nRemoving Qt debug libraries') 107 self.removeDebugs() 108 print('Copying required libs\n') 109 self.solvedepsLibs() 110 print('\nWritting qt.conf file') 111 self.writeQtConf() 112 print('Stripping symbols') 113 self.binarySolver.stripSymbols(self.installDir) 114 print('Writting launcher file') 115 self.createLauncher() 116 print('Removing unnecessary files') 117 self.removeUnneededFiles(self.installDir) 118 print('\nWritting build system information\n') 119 self.writeBuildInfo() 120 121 def solvedepsLibs(self): 122 deps = set(self.binarySolver.scanDependencies(self.installDir)) 123 extraDeps = ['libeay32.dll', 124 'ssleay32.dll', 125 'libEGL.dll', 126 'libGLESv2.dll', 127 'D3DCompiler_43.dll', 128 'D3DCompiler_46.dll', 129 'D3DCompiler_47.dll'] 130 131 for dep in extraDeps: 132 path = self.whereBin(dep) 133 134 if path != '': 135 deps.add(path) 136 137 for depPath in self.binarySolver.allDependencies(path): 138 deps.add(depPath) 139 140 deps = sorted(deps) 141 142 for dep in deps: 143 dep = dep.replace('\\', '/') 144 depPath = os.path.join(self.binaryInstallDir, os.path.basename(dep)) 145 depPath = depPath.replace('\\', '/') 146 147 if dep != depPath: 148 print(' {} -> {}'.format(dep, depPath)) 149 self.copy(dep, depPath) 150 self.dependencies.append(dep) 151 152 def removeDebugs(self): 153 dbgFiles = set() 154 155 for root, _, files in os.walk(self.libQtInstallDir): 156 for f in files: 157 if f.endswith('.dll'): 158 fname, ext = os.path.splitext(f) 159 dbgFile = os.path.join(root, '{}d{}'.format(fname, ext)) 160 161 if os.path.exists(dbgFile): 162 dbgFiles.add(dbgFile) 163 164 for f in dbgFiles: 165 os.remove(f) 166 167 def createLauncher(self): 168 path = os.path.join(self.rootInstallDir, self.programName) + '.bat' 169 libDir = os.path.relpath(self.libInstallDir, self.rootInstallDir) 170 qmlDir = os.path.relpath(self.qmlInstallDir, self.rootInstallDir).replace('/', '\\') 171 172 with open(path, 'w') as launcher: 173 launcher.write('@echo off\n') 174 launcher.write('\n') 175 launcher.write('rem Default values: desktop | angle | software\n') 176 launcher.write('rem set QT_OPENGL=angle\n') 177 launcher.write('\n') 178 launcher.write('rem Default values: d3d11 | d3d9 | warp\n') 179 launcher.write('rem set QT_ANGLE_PLATFORM=d3d11\n') 180 launcher.write('\n') 181 launcher.write('rem Default values: software | d3d12 | openvg\n') 182 launcher.write('rem set QT_QUICK_BACKEND=""\n') 183 launcher.write('\n') 184 launcher.write('rem Enable plugin debugging\n') 185 launcher.write('rem set QT_DEBUG_PLUGINS=1\n') 186 launcher.write('\n') 187 launcher.write('start /b "" ' 188 + '"%~dp0bin\\{}" '.format(self.programName) 189 + '-p "%~dp0{}\\avkys" '.format(libDir) 190 + '-q "%~dp0{}" '.format(qmlDir) 191 + '-c "%~dp0share\\config"\n') 192 193 @staticmethod 194 def removeUnneededFiles(path): 195 afiles = set() 196 197 for root, _, files in os.walk(path): 198 for f in files: 199 if f.endswith('.a') \ 200 or f.endswith('.static.prl') \ 201 or f.endswith('.pdb') \ 202 or f.endswith('.lib'): 203 afiles.add(os.path.join(root, f)) 204 205 for afile in afiles: 206 os.remove(afile) 207 208 def searchPackageFor(self, path): 209 path = path.replace('C:/', '/c/') 210 os.environ['LC_ALL'] = 'C' 211 pacman = self.whereBin('pacman.exe') 212 213 if len(pacman) > 0: 214 process = subprocess.Popen([pacman, '-Qo', path], # nosec 215 stdout=subprocess.PIPE, 216 stderr=subprocess.PIPE) 217 stdout, _ = process.communicate() 218 219 if process.returncode != 0: 220 prefix = '/c/msys32' if self.targetArch == '32bit' else '/c/msys64' 221 path = path[len(prefix):] 222 process = subprocess.Popen([pacman, '-Qo', path], # nosec 223 stdout=subprocess.PIPE, 224 stderr=subprocess.PIPE) 225 stdout, _ = process.communicate() 226 227 if process.returncode != 0: 228 return '' 229 230 info = stdout.decode(sys.getdefaultencoding()).split(' ') 231 232 if len(info) < 2: 233 return '' 234 235 package, version = info[-2:] 236 237 return ' '.join([package.strip(), version.strip()]) 238 239 return '' 240 241 @staticmethod 242 def sysInfo(): 243 try: 244 process = subprocess.Popen(['cmd', '/c', 'ver'], # nosec 245 stdout=subprocess.PIPE, 246 stderr=subprocess.PIPE) 247 stdout, _ = process.communicate() 248 windowsVersion = stdout.decode(sys.getdefaultencoding()).strip() 249 250 return 'Windows Version: {}\n'.format(windowsVersion) 251 except: 252 pass 253 254 return ' '.join(platform.uname()) 255 256 def writeBuildInfo(self): 257 shareDir = os.path.join(self.rootInstallDir, 'share') 258 259 try: 260 os.makedirs(shareDir) 261 except: 262 pass 263 264 depsInfoFile = os.path.join(shareDir, 'build-info.txt') 265 266 # Write repository info. 267 268 with open(depsInfoFile, 'w') as f: 269 commitHash = self.gitCommitHash(self.rootDir) 270 271 if len(commitHash) < 1: 272 commitHash = 'Unknown' 273 274 print(' Commit hash: ' + commitHash) 275 f.write('Commit hash: ' + commitHash + '\n') 276 277 buildLogUrl = '' 278 279 if 'TRAVIS_BUILD_WEB_URL' in os.environ: 280 buildLogUrl = os.environ['TRAVIS_BUILD_WEB_URL'] 281 elif 'APPVEYOR_ACCOUNT_NAME' in os.environ and 'APPVEYOR_PROJECT_NAME' in os.environ and 'APPVEYOR_JOB_ID' in os.environ: 282 buildLogUrl = 'https://ci.appveyor.com/project/{}/{}/build/job/{}'.format(os.environ['APPVEYOR_ACCOUNT_NAME'], 283 os.environ['APPVEYOR_PROJECT_SLUG'], 284 os.environ['APPVEYOR_JOB_ID']) 285 286 if len(buildLogUrl) > 0: 287 print(' Build log URL: ' + buildLogUrl) 288 f.write('Build log URL: ' + buildLogUrl + '\n') 289 290 print() 291 f.write('\n') 292 293 # Write binary dependencies info. 294 295 packages = set() 296 297 for dep in self.dependencies: 298 packageInfo = self.searchPackageFor(dep) 299 300 if len(packageInfo) > 0: 301 packages.add(packageInfo) 302 303 packages = sorted(packages) 304 305 with open(depsInfoFile, 'a') as f: 306 for packge in packages: 307 print(' ' + packge) 308 f.write(packge + '\n') 309 310 @staticmethod 311 def hrSize(size): 312 i = int(math.log(size) // math.log(1024)) 313 314 if i < 1: 315 return '{} B'.format(size) 316 317 units = ['KiB', 'MiB', 'GiB', 'TiB'] 318 sizeKiB = size / (1024 ** i) 319 320 return '{:.2f} {}'.format(sizeKiB, units[i - 1]) 321 322 def printPackageInfo(self, path): 323 if os.path.exists(path): 324 print(' ', 325 os.path.basename(path), 326 self.hrSize(os.path.getsize(path))) 327 print(' sha256sum:', Deploy.sha256sum(path)) 328 else: 329 print(' ', 330 os.path.basename(path), 331 'FAILED') 332 333 def createPortable(self, mutex): 334 arch = 'win32' if self.targetArch == '32bit' else 'win64' 335 packagePath = \ 336 os.path.join(self.pkgsDir, 337 '{}-portable-{}-{}.zip'.format(self.programName, 338 self.programVersion, 339 arch)) 340 341 if not os.path.exists(self.pkgsDir): 342 os.makedirs(self.pkgsDir) 343 344 with zipfile.ZipFile(packagePath, 'w', zipfile.ZIP_DEFLATED, False) as zipFile: 345 for root, dirs, files in os.walk(self.rootInstallDir): 346 for f in dirs + files: 347 filePath = os.path.join(root, f) 348 dstPath = os.path.join(self.programName, 349 filePath.replace(self.rootInstallDir + os.sep, '')) 350 zipFile.write(filePath, dstPath) 351 352 mutex.acquire() 353 print('Created portable package:') 354 self.printPackageInfo(packagePath) 355 mutex.release() 356 357 def createAppInstaller(self, mutex): 358 packagePath = self.createInstaller() 359 360 if not packagePath: 361 return 362 363 mutex.acquire() 364 print('Created installable package:') 365 self.printPackageInfo(self.outPackage) 366 mutex.release() 367 368 def package(self): 369 mutex = threading.Lock() 370 371 threads = [threading.Thread(target=self.createPortable, args=(mutex,))] 372 packagingTools = ['zip'] 373 374 if self.qtIFW != '': 375 threads.append(threading.Thread(target=self.createAppInstaller, args=(mutex,))) 376 packagingTools += ['Qt Installer Framework'] 377 378 if len(packagingTools) > 0: 379 print('Detected packaging tools: {}\n'.format(', '.join(packagingTools))) 380 381 for thread in threads: 382 thread.start() 383 384 for thread in threads: 385 thread.join() 386