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