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 subprocess # nosec 25import sys 26import threading 27import zipfile 28 29from WebcamoidDeployTools import DTDeployBase 30from WebcamoidDeployTools import DTQt5 31from WebcamoidDeployTools import DTBinaryPecoff 32 33 34class Deploy(DTDeployBase.DeployBase, DTQt5.Qt5Tools): 35 def __init__(self): 36 super().__init__() 37 rootDir = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..')) 38 self.setRootDir(rootDir) 39 self.targetSystem = 'posix_windows' 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.programName = 'webcamoid' 44 self.rootInstallDir = os.path.join(self.installDir, self.programName) 45 self.binaryInstallDir = os.path.join(self.rootInstallDir, 'bin') 46 self.libInstallDir = self.qmakeQuery(var='QT_INSTALL_LIBS') \ 47 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 48 self.rootInstallDir) 49 self.libQtInstallDir = self.qmakeQuery(var='QT_INSTALL_ARCHDATA') \ 50 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 51 self.rootInstallDir) 52 self.qmlInstallDir = self.qmakeQuery(var='QT_INSTALL_QML') \ 53 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 54 self.rootInstallDir) 55 self.pluginsInstallDir = self.qmakeQuery(var='QT_INSTALL_PLUGINS') \ 56 .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'), 57 self.rootInstallDir) 58 self.qtConf = os.path.join(self.binaryInstallDir, 'qt.conf') 59 self.qmlRootDirs = ['StandAlone/share/qml', 'libAvKys/Plugins'] 60 self.mainBinary = os.path.join(self.binaryInstallDir, self.programName + '.exe') 61 self.programName = os.path.splitext(os.path.basename(self.mainBinary))[0] 62 self.programVersion = self.detectVersion(os.path.join(self.rootDir, 'commons.pri')) 63 self.detectMake() 64 self.binarySolver = DTBinaryPecoff.PecoffBinaryTools() 65 self.binarySolver.readExcludes(os.name, sys.platform) 66 self.packageConfig = os.path.join(self.rootDir, 'ports/deploy/package_info.conf') 67 self.dependencies = [] 68 self.installerConfig = os.path.join(self.installDir, 'installer/config') 69 self.installerPackages = os.path.join(self.installDir, 'installer/packages') 70 self.installerIconSize = 256 71 self.appIcon = os.path.join(self.rootDir, 72 'StandAlone/share/icons/hicolor/{1}x{1}/{0}.ico'.format(self.programName, 73 self.installerIconSize)) 74 self.licenseFile = os.path.join(self.rootDir, 'COPYING') 75 self.installerRunProgram = '@TargetDir@/bin/' + self.programName + '.exe' 76 self.installerScript = os.path.join(self.rootDir, 'ports/deploy/installscript.windows.qs') 77 self.changeLog = os.path.join(self.rootDir, 'ChangeLog') 78 self.targetArch = '64bit' if 'x86_64' in self.qtInstallBins else '32bit' 79 80 @staticmethod 81 def removeUnneededFiles(path): 82 afiles = set() 83 84 for root, _, files in os.walk(path): 85 for f in files: 86 if f.endswith('.a') \ 87 or f.endswith('.static.prl') \ 88 or f.endswith('.pdb') \ 89 or f.endswith('.lib'): 90 afiles.add(os.path.join(root, f)) 91 92 for afile in afiles: 93 os.remove(afile) 94 95 def prepare(self): 96 print('Executing make install') 97 params = {'INSTALL_ROOT': self.installDir} 98 self.makeInstall(self.buildDir, params) 99 100 if self.targetArch == '32bit': 101 self.binarySolver.sysBinsPath = ['/usr/i686-w64-mingw32/bin'] 102 else: 103 self.binarySolver.sysBinsPath = ['/usr/x86_64-w64-mingw32/bin'] 104 105 self.binarySolver.detectStrip() 106 107 if self.qtIFWVersion == '' or int(self.qtIFWVersion.split('.')[0]) < 3: 108 appsDir = '@ApplicationsDir@' 109 else: 110 if self.targetArch == '32bit': 111 appsDir = '@ApplicationsDirX86@' 112 else: 113 appsDir = '@ApplicationsDirX64@' 114 115 self.installerTargetDir = appsDir + '/' + self.programName 116 arch = 'win32' if self.targetArch == '32bit' else 'win64' 117 self.outPackage = os.path.join(self.pkgsDir, 118 '{}-{}-{}.exe'.format(self.programName, 119 self.programVersion, 120 arch)) 121 122 print('Copying Qml modules\n') 123 self.solvedepsQml() 124 print('\nCopying required plugins\n') 125 self.solvedepsPlugins() 126 print('\nRemoving Qt debug libraries') 127 self.removeDebugs() 128 print('Copying required libs\n') 129 self.solvedepsLibs() 130 print('\nWritting qt.conf file') 131 self.writeQtConf() 132 print('Stripping symbols') 133 self.binarySolver.stripSymbols(self.installDir) 134 print('Writting launcher file') 135 self.createLauncher() 136 print('Removing unnecessary files') 137 self.removeUnneededFiles(self.installDir) 138 print('\nWritting build system information\n') 139 self.writeBuildInfo() 140 141 def solvedepsLibs(self): 142 deps = set(self.binarySolver.scanDependencies(self.installDir)) 143 extraDeps = ['libeay32.dll', 144 'ssleay32.dll', 145 'libEGL.dll', 146 'libGLESv2.dll', 147 'D3DCompiler_43.dll', 148 'D3DCompiler_46.dll', 149 'D3DCompiler_47.dll'] 150 151 for dep in extraDeps: 152 path = self.whereBin(dep) 153 154 if path != '': 155 deps.add(path) 156 157 for depPath in self.binarySolver.allDependencies(path): 158 deps.add(depPath) 159 160 deps = sorted(deps) 161 162 for dep in deps: 163 depPath = os.path.join(self.binaryInstallDir, os.path.basename(dep)) 164 165 if dep != depPath: 166 print(' {} -> {}'.format(dep, depPath)) 167 self.copy(dep, depPath) 168 self.dependencies.append(dep) 169 170 def removeDebugs(self): 171 dbgFiles = set() 172 173 for root, _, files in os.walk(self.libQtInstallDir): 174 for f in files: 175 if f.endswith('.dll'): 176 fname, ext = os.path.splitext(f) 177 dbgFile = os.path.join(root, '{}d{}'.format(fname, ext)) 178 179 if os.path.exists(dbgFile): 180 dbgFiles.add(dbgFile) 181 182 for f in dbgFiles: 183 os.remove(f) 184 185 def searchPackageFor(self, path): 186 os.environ['LC_ALL'] = 'C' 187 pacman = self.whereBin('pacman') 188 189 if len(pacman) > 0: 190 process = subprocess.Popen([pacman, '-Qo', path], # nosec 191 stdout=subprocess.PIPE, 192 stderr=subprocess.PIPE) 193 stdout, _ = process.communicate() 194 195 if process.returncode != 0: 196 return '' 197 198 info = stdout.decode(sys.getdefaultencoding()).split(' ') 199 200 if len(info) < 2: 201 return '' 202 203 package, version = info[-2:] 204 205 return ' '.join([package.strip(), version.strip()]) 206 207 dpkg = self.whereBin('dpkg') 208 209 if len(dpkg) > 0: 210 process = subprocess.Popen([dpkg, '-S', path], # nosec 211 stdout=subprocess.PIPE, 212 stderr=subprocess.PIPE) 213 stdout, _ = process.communicate() 214 215 if process.returncode != 0: 216 return '' 217 218 package = stdout.split(b':')[0].decode(sys.getdefaultencoding()).strip() 219 220 process = subprocess.Popen([dpkg, '-s', package], # nosec 221 stdout=subprocess.PIPE, 222 stderr=subprocess.PIPE) 223 stdout, _ = process.communicate() 224 225 if process.returncode != 0: 226 return '' 227 228 for line in stdout.decode(sys.getdefaultencoding()).split('\n'): 229 line = line.strip() 230 231 if line.startswith('Version:'): 232 return ' '.join([package, line.split()[1].strip()]) 233 234 return '' 235 236 rpm = self.whereBin('rpm') 237 238 if len(rpm) > 0: 239 process = subprocess.Popen([rpm, '-qf', path], # nosec 240 stdout=subprocess.PIPE, 241 stderr=subprocess.PIPE) 242 stdout, _ = process.communicate() 243 244 if process.returncode != 0: 245 return '' 246 247 return stdout.decode(sys.getdefaultencoding()).strip() 248 249 return '' 250 251 @staticmethod 252 def sysInfo(): 253 info = '' 254 255 for f in os.listdir('/etc'): 256 if f.endswith('-release'): 257 with open(os.path.join('/etc' , f)) as releaseFile: 258 info += releaseFile.read() 259 260 return info 261 262 def writeBuildInfo(self): 263 shareDir = os.path.join(self.rootInstallDir, 'share') 264 265 try: 266 os.makedirs(self.pkgsDir) 267 except: 268 pass 269 270 depsInfoFile = os.path.join(shareDir, 'build-info.txt') 271 272 # Write repository info. 273 274 with open(depsInfoFile, 'w') as f: 275 commitHash = self.gitCommitHash(self.rootDir) 276 277 if len(commitHash) < 1: 278 commitHash = 'Unknown' 279 280 print(' Commit hash: ' + commitHash) 281 f.write('Commit hash: ' + commitHash + '\n') 282 283 buildLogUrl = '' 284 285 if 'TRAVIS_BUILD_WEB_URL' in os.environ: 286 buildLogUrl = os.environ['TRAVIS_BUILD_WEB_URL'] 287 elif 'APPVEYOR_ACCOUNT_NAME' in os.environ and 'APPVEYOR_PROJECT_NAME' in os.environ and 'APPVEYOR_JOB_ID' in os.environ: 288 buildLogUrl = 'https://ci.appveyor.com/project/{}/{}/build/job/{}'.format(os.environ['APPVEYOR_ACCOUNT_NAME'], 289 os.environ['APPVEYOR_PROJECT_SLUG'], 290 os.environ['APPVEYOR_JOB_ID']) 291 292 if len(buildLogUrl) > 0: 293 print(' Build log URL: ' + buildLogUrl) 294 f.write('Build log URL: ' + buildLogUrl + '\n') 295 296 print() 297 f.write('\n') 298 299 # Write host info. 300 301 info = self.sysInfo() 302 303 with open(depsInfoFile, 'a') as f: 304 for line in info.split('\n'): 305 if len(line) > 0: 306 print(' ' + line) 307 f.write(line + '\n') 308 309 print() 310 f.write('\n') 311 312 # Write Wine version and emulated system info. 313 314 process = subprocess.Popen(['wine', '--version'], # nosec 315 stdout=subprocess.PIPE, 316 stderr=subprocess.PIPE) 317 stdout, _ = process.communicate() 318 wineVersion = stdout.decode(sys.getdefaultencoding()).strip() 319 320 with open(depsInfoFile, 'a') as f: 321 print(' Wine Version: {}'.format(wineVersion)) 322 f.write('Wine Version: {}\n'.format(wineVersion)) 323 324 process = subprocess.Popen(['wine', 'cmd', '/c', 'ver'], # nosec 325 stdout=subprocess.PIPE, 326 stderr=subprocess.PIPE) 327 stdout, _ = process.communicate() 328 fakeWindowsVersion = stdout.decode(sys.getdefaultencoding()).strip() 329 330 if len(fakeWindowsVersion) < 1: 331 fakeWindowsVersion = 'Unknown' 332 333 with open(depsInfoFile, 'a') as f: 334 print(' Windows Version: {}'.format(fakeWindowsVersion)) 335 f.write('Windows Version: {}\n'.format(fakeWindowsVersion)) 336 print() 337 f.write('\n') 338 339 # Write binary dependencies info. 340 341 packages = set() 342 343 for dep in self.dependencies: 344 packageInfo = self.searchPackageFor(dep) 345 346 if len(packageInfo) > 0: 347 packages.add(packageInfo) 348 349 packages = sorted(packages) 350 351 with open(depsInfoFile, 'a') as f: 352 for packge in packages: 353 print(' ' + packge) 354 f.write(packge + '\n') 355 356 def createLauncher(self): 357 path = os.path.join(self.rootInstallDir, self.programName) + '.bat' 358 libDir = os.path.relpath(self.libInstallDir, self.rootInstallDir) 359 qmlDir = os.path.relpath(self.qmlInstallDir, self.rootInstallDir).replace('/', '\\') 360 361 with open(path, 'w') as launcher: 362 launcher.write('@echo off\n') 363 launcher.write('\n') 364 launcher.write('rem Default values: desktop | angle | software\n') 365 launcher.write('rem set QT_OPENGL=angle\n') 366 launcher.write('\n') 367 launcher.write('rem Default values: d3d11 | d3d9 | warp\n') 368 launcher.write('rem set QT_ANGLE_PLATFORM=d3d11\n') 369 launcher.write('\n') 370 launcher.write('rem Default values: software | d3d12 | openvg\n') 371 launcher.write('rem set QT_QUICK_BACKEND=""\n') 372 launcher.write('\n') 373 launcher.write('rem Enable plugin debugging\n') 374 launcher.write('rem set QT_DEBUG_PLUGINS=1\n') 375 launcher.write('\n') 376 launcher.write('start /b "" ' 377 + '"%~dp0bin\\{}" '.format(self.programName) 378 + '-p "%~dp0{}\\avkys" '.format(libDir) 379 + '-q "%~dp0{}" '.format(qmlDir) 380 + '-c "%~dp0share\\config"\n') 381 382 @staticmethod 383 def hrSize(size): 384 i = int(math.log(size) // math.log(1024)) 385 386 if i < 1: 387 return '{} B'.format(size) 388 389 units = ['KiB', 'MiB', 'GiB', 'TiB'] 390 sizeKiB = size / (1024 ** i) 391 392 return '{:.2f} {}'.format(sizeKiB, units[i - 1]) 393 394 def printPackageInfo(self, path): 395 if os.path.exists(path): 396 print(' ', 397 os.path.basename(path), 398 self.hrSize(os.path.getsize(path))) 399 print(' sha256sum:', Deploy.sha256sum(path)) 400 else: 401 print(' ', 402 os.path.basename(path), 403 'FAILED') 404 405 def createPortable(self, mutex): 406 arch = 'win32' if self.targetArch == '32bit' else 'win64' 407 packagePath = \ 408 os.path.join(self.pkgsDir, 409 '{}-portable-{}-{}.zip'.format(self.programName, 410 self.programVersion, 411 arch)) 412 413 if not os.path.exists(self.pkgsDir): 414 os.makedirs(self.pkgsDir) 415 416 with zipfile.ZipFile(packagePath, 'w', zipfile.ZIP_DEFLATED, False) as zipFile: 417 for root, dirs, files in os.walk(self.rootInstallDir): 418 for f in dirs + files: 419 filePath = os.path.join(root, f) 420 dstPath = os.path.join(self.programName, 421 filePath.replace(self.rootInstallDir + os.sep, '')) 422 zipFile.write(filePath, dstPath) 423 424 mutex.acquire() 425 print('Created portable package:') 426 self.printPackageInfo(packagePath) 427 mutex.release() 428 429 def createAppInstaller(self, mutex): 430 packagePath = self.createInstaller() 431 432 if not packagePath: 433 return 434 435 mutex.acquire() 436 print('Created installable package:') 437 self.printPackageInfo(self.outPackage) 438 mutex.release() 439 440 def package(self): 441 mutex = threading.Lock() 442 443 threads = [threading.Thread(target=self.createPortable, args=(mutex,))] 444 packagingTools = ['zip'] 445 446 if self.qtIFW != '': 447 threads.append(threading.Thread(target=self.createAppInstaller, args=(mutex,))) 448 packagingTools += ['Qt Installer Framework'] 449 450 if len(packagingTools) > 0: 451 print('Detected packaging tools: {}\n'.format(', '.join(packagingTools))) 452 453 for thread in threads: 454 thread.start() 455 456 for thread in threads: 457 thread.join() 458