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