diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f3e4972
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,112 @@
+language: cpp
+
+cache:
+ ccache: true
+ timeout: 1000
+ directories:
+ - keystores
+
+env:
+ global:
+ - QTVER=5.14.1
+ - QTIFWVER=3.2.2
+ - ARCH_ROOT_DATE=2020.04.01
+ - ARCH_ROOT_URL=https://mirror.rackspace.com/archlinux
+ # BT_KEY
+ - secure: "RiR23dM8ft06+5ezNAlfpJXNRQhFH2Cy3hTY9oWD6nyw0qsydgOA9qlogBP0o1/kikyqt2uxv/VRcqZnBV3nkwEg9AuiCRgOuKqMtFdxwNnF8wqxUUz4pun9FQGLNnKkIznJkV61vsuRs8GSO95X3KEwgsoGPD60WQDDuFj3wXcI1kIN7HEP1t3Kq8lbhsYhbMPPeZBUGYcEjuWMz/GcfdcEUQLD6F0bkfEQgLZ+/d5Nqxk3NTJGM/LUYLKuWOthqsK7qNp/BCTRnOCg44NV3XO0DXti4lNkbtbfbLZIEMfD5EeTnbznBd+AHH2OTnw2Gn5ViVz7Z2C9mZgcN+/XyEJi7K80ab9DR9s+ZLMFO2yyF6MVf7/qiOzL0RT7lGFHt2HPLBpTji3qmgPN4JXaXKfF3nIzOjaoUibhTzwh5NlMpkAFlydmYwhBS3argsKQgeck3YTWUUnQVFvkmhFHVZzFBGtqy+0dPr9WGyay0mKKYpnLHqGk74K23dGnBKzk+iJs9o9zVQaSdvS5XnSeRsQCAYlqEKy/sSdrZnLYAOaLv1f291CIm76+e5B2sIkZaGcox7d3rcYw/tqn7ozgsKcyT1BJq8/ZyYSXsIT826ZeDiZd4MFjSQTeas5c9V85btDq2W9LvA66KqzTq0dH5pfJ67+BJJ7ebxUf4HNw6ME="
+
+matrix:
+ include:
+ - os: linux
+ dist: bionic
+ compiler: gcc
+ env:
+ - ARCH_ROOT_MINGW=x86_64
+ - DAILY_BUILD=1
+ - os: linux
+ dist: bionic
+ compiler: gcc
+ env:
+ - ARCH_ROOT_MINGW=i686
+ - DAILY_BUILD=1
+ - os: linux
+ dist: bionic
+ compiler: gcc
+ env:
+ - ARCH_ROOT_MINGW=x86_64
+ - RELEASE_BUILD=1
+ - os: linux
+ dist: bionic
+ compiler: gcc
+ env:
+ - ARCH_ROOT_MINGW=i686
+ - RELEASE_BUILD=1
+ - os: osx
+ osx_image: xcode10
+ compiler: clang
+ env:
+ - DAILY_BUILD=1
+ - os: osx
+ osx_image: xcode10
+ compiler: clang
+ env:
+ - RELEASE_BUILD=1
+ - os: osx
+ osx_image: xcode10.1
+ compiler: clang
+ - os: osx
+ osx_image: xcode10.2
+ compiler: clang
+ - os: osx
+ osx_image: xcode10.3
+ compiler: clang
+ - os: osx
+ osx_image: xcode11
+ compiler: clang
+ - os: osx
+ osx_image: xcode11.1
+ compiler: clang
+ - os: osx
+ osx_image: xcode11.2
+ compiler: clang
+ - os: osx
+ osx_image: xcode11.3
+ compiler: clang
+
+install:
+ - chmod +x ports/ci/travis/install_deps.sh
+ - ports/ci/travis/install_deps.sh
+
+before_script: |
+ if [ "${TRAVIS_OS_NAME}" = linux ]; then
+ export CXX=${ARCH_ROOT_MINGW}-w64-mingw32-g++
+ export COMPILESPEC=win32-g++
+ elif [ "${TRAVIS_OS_NAME}" = osx ]; then
+ brew link --force qt5
+ if [ "${CXX}" = g++ ]; then
+ export COMPILESPEC=macx-g++
+ elif [ "${CXX}" = clang++ ]; then
+ export COMPILESPEC=macx-clang
+ fi
+ fi
+
+script:
+ - chmod +x ports/ci/travis/build.sh
+ - ports/ci/travis/build.sh
+
+#after_success:
+# - chmod +x ports/ci/travis/deploy.sh
+# - ports/ci/travis/deploy.sh
+# - chmod +x ports/ci/travis/upload.sh
+# - ports/ci/travis/upload.sh
+
+branches:
+ only:
+ - master
+
+notifications:
+ recipients:
+ - hipersayan.x@gmail.com
+ email:
+ on_success: change
+ on_failure: change
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..f33cc53
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,82 @@
+version: 9.0.0.{build}-{branch}
+os: MinGW
+
+image: Visual Studio 2019
+
+platform:
+ - x64
+
+build:
+ parallel: true
+ verbosity: detailed
+
+configuration:
+ - release
+# - debug
+
+branches:
+ only:
+ - master
+
+environment:
+ global:
+ PYTHON_VERSION: Python38
+ QMAKESPEC: win32-g++
+ MAKETOOL: mingw32-make
+ INSTALL_PREFIX: C:/projects/akvirtualcamera/ports/deploy/temp_priv/usr
+ # api key from https://bintray.com/profile/edit
+ # encrypted in https://ci.appveyor.com/tools/encrypt
+ BT_KEY:
+ secure: seneRk4ppI4bquIsdweI8pd8FT0RXUvU2LOUNGSBEA28IhFQijypil2CfC3WtJxa
+
+ matrix:
+ - MSYS2_BUILD: 1
+ - QTDIR: C:\Qt\5.14\mingw73_64
+ TOOLSDIR: C:\Qt\Tools\mingw730_64
+ - QTDIR: C:\Qt\5.14\mingw73_32
+ TOOLSDIR: C:\Qt\Tools\mingw730_32
+ PLATFORM: x86
+ - QTDIR: C:\Qt\5.14\msvc2017
+ TOOLSDIR: C:\Qt\Tools\QtCreator
+ QMAKESPEC: win32-msvc
+ VSPATH: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build
+ MAKETOOL: jom
+ PLATFORM: x86
+ - QTDIR: C:\Qt\5.14\msvc2017_64
+ TOOLSDIR: C:\Qt\Tools\QtCreator
+ QMAKESPEC: win32-msvc
+ VSPATH: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build
+ MAKETOOL: jom
+
+install:
+ - if "%MSYS2_BUILD%" == "" (
+ ports\ci\appveyor\install_deps.bat
+ ) else (
+ C:\msys64\usr\bin\bash -lc "cd /c/projects/akvirtualcamera && ./ports/ci/appveyor/install_deps.sh"
+ )
+
+build_script:
+ - if "%MSYS2_BUILD%" == "" (
+ ports\ci\appveyor\build.bat
+ ) else (
+ C:\msys64\usr\bin\bash -lc "cd /c/projects/akvirtualcamera && ./ports/ci/appveyor/build.sh '%INSTALL_PREFIX%'"
+ )
+
+#after_build:
+# - if "%MSYS2_BUILD%" == "" (
+# ports\ci\appveyor\deploy.bat
+# ) else (
+# C:\msys64\usr\bin\bash -lc "cd /c/projects/akvirtualcamera && ./ports/ci/appveyor/deploy.sh"
+# )
+# - ports\ci\appveyor\push_artifacts.bat
+#
+#deploy_script:
+# - ports\ci\appveyor\upload.bat
+
+notifications:
+ - provider: Email
+ to:
+ - hipersayan.x@gmail.com
+ on_build_success: false
+ on_build_failure: false
+ on_build_status_changed: true
diff --git a/ports/ci/appveyor/build.bat b/ports/ci/appveyor/build.bat
new file mode 100644
index 0000000..45d86ca
--- /dev/null
+++ b/ports/ci/appveyor/build.bat
@@ -0,0 +1,68 @@
+REM Webcamoid, webcam capture application.
+REM Copyright (C) 2016 Gonzalo Exequiel Pedone
+REM
+REM Webcamoid is free software: you can redistribute it and/or modify
+REM it under the terms of the GNU General Public License as published by
+REM the Free Software Foundation, either version 3 of the License, or
+REM (at your option) any later version.
+REM
+REM Webcamoid is distributed in the hope that it will be useful,
+REM but WITHOUT ANY WARRANTY; without even the implied warranty of
+REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+REM GNU General Public License for more details.
+REM
+REM You should have received a copy of the GNU General Public License
+REM along with Webcamoid. If not, see .
+REM
+REM Web-Site: http://webcamoid.github.io/
+
+if "%PLATFORM%" == "x86" (
+ set VC_ARGS=x86
+) else (
+ set VC_ARGS=amd64
+)
+
+rem Visual Studio init
+if not "%VSPATH%" == "" call "%VSPATH%\vcvarsall" %VC_ARGS%
+
+set PATH_ORIG=%PATH%
+set PATH=%QTDIR%\bin;%TOOLSDIR%\bin;%PATH%
+
+qmake -query
+qmake akvirtualcamera.pro ^
+ CONFIG+=%CONFIGURATION% ^
+ CONFIG+=silent ^
+ PREFIX="%INSTALL_PREFIX%"
+
+%MAKETOOL% -j4
+
+if "%DAILY_BUILD%" == "" goto EndScript
+
+if "%PLATFORM%" == "x86" (
+ set DRV_ARCH=x64
+) else (
+ set DRV_ARCH=x86
+)
+
+echo.
+echo Building %DRV_ARCH% virtual camera driver
+echo.
+
+mkdir akvcam
+cd akvcam
+
+set PATH=%QTDIR_ALT%\bin;%TOOLSDIR_ALT%\bin;%PATH_ORIG%
+qmake -query
+qmake ^
+ CONFIG+=silent ^
+ ..\akvirtualcamera.pro
+%MAKETOOL% -j4
+
+cd ..
+mkdir AkVirtualCamera.plugin\%DRV_ARCH%
+xcopy ^
+ akvcam\AkVirtualCamera.plugin\%DRV_ARCH%\* ^
+ AkVirtualCamera.plugin\%DRV_ARCH% ^
+ /i /y
+
+:EndScript
diff --git a/ports/ci/appveyor/build.sh b/ports/ci/appveyor/build.sh
new file mode 100755
index 0000000..5a805bd
--- /dev/null
+++ b/ports/ci/appveyor/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+export PATH=/mingw64/bin:$PATH
+qmake -query
+qmake akvirtualcamera.pro \
+ CONFIG+=silent \
+ PREFIX="$1"
+make -j4
diff --git a/ports/ci/appveyor/deploy.bat b/ports/ci/appveyor/deploy.bat
new file mode 100644
index 0000000..d034fec
--- /dev/null
+++ b/ports/ci/appveyor/deploy.bat
@@ -0,0 +1,35 @@
+REM Webcamoid, webcam capture application.
+REM Copyright (C) 2017 Gonzalo Exequiel Pedone
+REM
+REM Webcamoid is free software: you can redistribute it and/or modify
+REM it under the terms of the GNU General Public License as published by
+REM the Free Software Foundation, either version 3 of the License, or
+REM (at your option) any later version.
+REM
+REM Webcamoid is distributed in the hope that it will be useful,
+REM but WITHOUT ANY WARRANTY; without even the implied warranty of
+REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+REM GNU General Public License for more details.
+REM
+REM You should have received a copy of the GNU General Public License
+REM along with Webcamoid. If not, see .
+REM
+REM Web-Site: http://webcamoid.github.io/
+
+if "%PLATFORM%" == "x86" (
+ set FF_ARCH=win32
+ set GST_ARCH=x86
+ set VC_ARGS=x86
+ set PYTHON_PATH=C:\%PYTHON_VERSION%
+) else (
+ set FF_ARCH=win64
+ set GST_ARCH=x86_64
+ set VC_ARGS=amd64
+ set PYTHON_PATH=C:\%PYTHON_VERSION%-x64
+)
+
+set MAKE_PATH=%TOOLSDIR%\bin\%MAKETOOL%.exe
+set GSTREAMER_DEV_PATH=C:\gstreamer\1.0\%GST_ARCH%
+set PATH=%QTDIR%\bin;%TOOLSDIR%\bin;%CD%\ffmpeg-%FFMPEG_VERSION%-%FF_ARCH%-shared\bin;%GSTREAMER_DEV_PATH%\bin;%PATH%
+
+%PYTHON_PATH%\python.exe ports\deploy\deploy.py
diff --git a/ports/ci/appveyor/deploy.sh b/ports/ci/appveyor/deploy.sh
new file mode 100755
index 0000000..37d1b15
--- /dev/null
+++ b/ports/ci/appveyor/deploy.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+export PATH=/mingw64/bin:$PATH
+python3 ports/deploy/deploy.py
diff --git a/ports/ci/appveyor/install_deps.bat b/ports/ci/appveyor/install_deps.bat
new file mode 100644
index 0000000..bbf6ae6
--- /dev/null
+++ b/ports/ci/appveyor/install_deps.bat
@@ -0,0 +1,20 @@
+REM Webcamoid, webcam capture application.
+REM Copyright (C) 2017 Gonzalo Exequiel Pedone
+REM
+REM Webcamoid is free software: you can redistribute it and/or modify
+REM it under the terms of the GNU General Public License as published by
+REM the Free Software Foundation, either version 3 of the License, or
+REM (at your option) any later version.
+REM
+REM Webcamoid is distributed in the hope that it will be useful,
+REM but WITHOUT ANY WARRANTY; without even the implied warranty of
+REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+REM GNU General Public License for more details.
+REM
+REM You should have received a copy of the GNU General Public License
+REM along with Webcamoid. If not, see .
+REM
+REM Web-Site: http://webcamoid.github.io/
+
+rem Installing various utilities
+choco install -y jfrog-cli
diff --git a/ports/ci/appveyor/install_deps.sh b/ports/ci/appveyor/install_deps.sh
new file mode 100755
index 0000000..e224f68
--- /dev/null
+++ b/ports/ci/appveyor/install_deps.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+pacman -Syy
+pacman --noconfirm --needed -S \
+ git \
+ make \
+ pkg-config \
+ python3 \
+ mingw-w64-x86_64-pkg-config \
+ mingw-w64-x86_64-qt5
diff --git a/ports/ci/appveyor/push_artifacts.bat b/ports/ci/appveyor/push_artifacts.bat
new file mode 100644
index 0000000..8892ebd
--- /dev/null
+++ b/ports/ci/appveyor/push_artifacts.bat
@@ -0,0 +1,23 @@
+REM Webcamoid, webcam capture application.
+REM Copyright (C) 2019 Gonzalo Exequiel Pedone
+REM
+REM Webcamoid is free software: you can redistribute it and/or modify
+REM it under the terms of the GNU General Public License as published by
+REM the Free Software Foundation, either version 3 of the License, or
+REM (at your option) any later version.
+REM
+REM Webcamoid is distributed in the hope that it will be useful,
+REM but WITHOUT ANY WARRANTY; without even the implied warranty of
+REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+REM GNU General Public License for more details.
+REM
+REM You should have received a copy of the GNU General Public License
+REM along with Webcamoid. If not, see .
+REM
+REM Web-Site: http://webcamoid.github.io/
+
+if not "%DAILY_BUILD%" == "" (
+ for %%f in (ports\deploy\packages_auto\windows\*.exe) do (
+ appveyor PushArtifact %%f
+ )
+)
diff --git a/ports/ci/appveyor/upload.bat b/ports/ci/appveyor/upload.bat
new file mode 100644
index 0000000..5c0e66c
--- /dev/null
+++ b/ports/ci/appveyor/upload.bat
@@ -0,0 +1,35 @@
+REM Webcamoid, webcam capture application.
+REM Copyright (C) 2019 Gonzalo Exequiel Pedone
+REM
+REM Webcamoid is free software: you can redistribute it and/or modify
+REM it under the terms of the GNU General Public License as published by
+REM the Free Software Foundation, either version 3 of the License, or
+REM (at your option) any later version.
+REM
+REM Webcamoid is distributed in the hope that it will be useful,
+REM but WITHOUT ANY WARRANTY; without even the implied warranty of
+REM MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+REM GNU General Public License for more details.
+REM
+REM You should have received a copy of the GNU General Public License
+REM along with Webcamoid. If not, see .
+REM
+REM Web-Site: http://webcamoid.github.io/
+
+if not "%DAILY_BUILD%" == "" if "%APPVEYOR_REPO_BRANCH%" == "master" (
+ jfrog bt config ^
+ --user=hipersayanx ^
+ --key=%BT_KEY% ^
+ --licenses=GPL-3.0-or-later
+
+ for %%f in (ports\deploy\packages_auto\windows\*.exe) do (
+ jfrog bt upload ^
+ --user=hipersayanx ^
+ --key=%BT_KEY% ^
+ --override=true ^
+ --publish=true ^
+ %%f ^
+ webcamoid/webcamoid/webcamoid/daily ^
+ windows/
+ )
+)
diff --git a/ports/ci/travis/build.sh b/ports/ci/travis/build.sh
new file mode 100755
index 0000000..af6b147
--- /dev/null
+++ b/ports/ci/travis/build.sh
@@ -0,0 +1,147 @@
+#!/bin/bash
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+if [ -z "${DISABLE_CCACHE}" ]; then
+ if [ "${CXX}" = clang++ ]; then
+ UNUSEDARGS="-Qunused-arguments"
+ fi
+
+ COMPILER="ccache ${CXX} ${UNUSEDARGS}"
+else
+ COMPILER=${CXX}
+fi
+
+if [ "${TRAVIS_OS_NAME}" = linux ]; then
+ EXEC='sudo ./root.x86_64/bin/arch-chroot root.x86_64'
+fi
+
+if [ "${TRAVIS_OS_NAME}" = linux ]; then
+ sudo mount --bind root.x86_64 root.x86_64
+ sudo mount --bind $HOME root.x86_64/$HOME
+
+ QMAKE_CMD=/usr/${ARCH_ROOT_MINGW}-w64-mingw32/lib/qt/bin/qmake
+
+ cat << EOF > ${BUILDSCRIPT}
+#!/bin/sh
+
+export LC_ALL=C
+export HOME=$HOME
+EOF
+
+ if [ ! -z "${DAILY_BUILD}" ]; then
+ cat << EOF >> ${BUILDSCRIPT}
+export DAILY_BUILD=1
+EOF
+ fi
+
+ cat << EOF >> ${BUILDSCRIPT}
+cd $TRAVIS_BUILD_DIR
+${QMAKE_CMD} -query
+${QMAKE_CMD} -spec ${COMPILESPEC} akvirtualcamera.pro \
+ CONFIG+=silent \
+ QMAKE_CXX="${COMPILER}"
+EOF
+ chmod +x ${BUILDSCRIPT}
+ sudo cp -vf ${BUILDSCRIPT} root.x86_64/$HOME/
+
+ ${EXEC} bash $HOME/${BUILDSCRIPT}
+elif [ "${TRAVIS_OS_NAME}" = osx ]; then
+ ${EXEC} qmake -query
+ ${EXEC} qmake -spec ${COMPILESPEC} akvirtualcamera.pro \
+ CONFIG+=silent \
+ QMAKE_CXX="${COMPILER}"
+fi
+
+if [ -z "${NJOBS}" ]; then
+ NJOBS=4
+fi
+
+if [ "${TRAVIS_OS_NAME}" = linux ]; then
+ cat << EOF > ${BUILDSCRIPT}
+#!/bin/sh
+
+export LC_ALL=C
+export HOME=$HOME
+EOF
+
+ if [ ! -z "${DAILY_BUILD}" ]; then
+ cat << EOF >> ${BUILDSCRIPT}
+export DAILY_BUILD=1
+EOF
+ fi
+
+ cat << EOF >> ${BUILDSCRIPT}
+cd $TRAVIS_BUILD_DIR
+make -j${NJOBS}
+EOF
+ chmod +x ${BUILDSCRIPT}
+ sudo cp -vf ${BUILDSCRIPT} root.x86_64/$HOME/
+
+ ${EXEC} bash $HOME/${BUILDSCRIPT}
+ sudo umount root.x86_64/$HOME
+ sudo umount root.x86_64
+else
+ ${EXEC} make -j${NJOBS}
+fi
+
+if [ "${TRAVIS_OS_NAME}" = linux ]; then
+ if [ "$ARCH_ROOT_MINGW" = x86_64 ]; then
+ mingw_arch=i686
+ mingw_compiler=${COMPILER/x86_64/i686}
+ mingw_dstdir=x86
+ else
+ mingw_arch=x86_64
+ mingw_compiler=${COMPILER/i686/x86_64}
+ mingw_dstdir=x64
+ fi
+
+ echo
+ echo "Building $mingw_arch virtual camera driver"
+ echo
+ sudo mount --bind root.x86_64 root.x86_64
+ sudo mount --bind $HOME root.x86_64/$HOME
+
+ cat << EOF > ${BUILDSCRIPT}
+#!/bin/sh
+
+export LC_ALL=C
+export HOME=$HOME
+mkdir -p $TRAVIS_BUILD_DIR/akvcam
+cd $TRAVIS_BUILD_DIR/akvcam
+/usr/${mingw_arch}-w64-mingw32/lib/qt/bin/qmake \
+ -spec ${COMPILESPEC} \
+ ../akvirtualcamera.pro \
+ CONFIG+=silent \
+ QMAKE_CXX="${mingw_compiler}"
+make -j${NJOBS}
+EOF
+ chmod +x ${BUILDSCRIPT}
+ sudo cp -vf ${BUILDSCRIPT} root.x86_64/$HOME/
+
+ ${EXEC} bash $HOME/${BUILDSCRIPT}
+
+ sudo mkdir -p AkVirtualCamera.plugin/${mingw_dstdir}
+ sudo cp -rvf \
+ akvcam/AkVirtualCamera.plugin/${mingw_dstdir}/* \
+ AkVirtualCamera.plugin/${mingw_dstdir}/
+
+ sudo umount root.x86_64/$HOME
+ sudo umount root.x86_64
+fi
diff --git a/ports/ci/travis/deploy.sh b/ports/ci/travis/deploy.sh
new file mode 100755
index 0000000..6e35cbb
--- /dev/null
+++ b/ports/ci/travis/deploy.sh
@@ -0,0 +1,129 @@
+#!/bin/bash
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+if [ "${ARCH_ROOT_BUILD}" = 1 ]; then
+ EXEC='sudo ./root.x86_64/bin/arch-chroot root.x86_64'
+elif [ "${TRAVIS_OS_NAME}" = linux ] && [ -z "${ANDROID_BUILD}" ]; then
+ if [ -z "${DAILY_BUILD}" ]; then
+ EXEC="docker exec ${DOCKERSYS}"
+ else
+ EXEC="docker exec -e DAILY_BUILD=1 ${DOCKERSYS}"
+ fi
+fi
+
+DEPLOYSCRIPT=deployscript.sh
+
+if [ "${ANDROID_BUILD}" = 1 ]; then
+ export JAVA_HOME=$(readlink -f /usr/bin/java | sed 's:bin/java::')
+ export ANDROID_HOME="${PWD}/build/android-sdk"
+ export ANDROID_NDK="${PWD}/build/android-ndk"
+ export ANDROID_NDK_HOME=${ANDROID_NDK}
+ export ANDROID_NDK_PLATFORM=android-${ANDROID_PLATFORM}
+ export ANDROID_NDK_ROOT=${ANDROID_NDK}
+ export ANDROID_SDK_ROOT=${ANDROID_HOME}
+ export PATH="${JAVA_HOME}/bin/java:${PATH}"
+ export PATH="$PATH:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin"
+ export PATH="${PATH}:${ANDROID_HOME}/platform-tools"
+ export PATH="${PATH}:${ANDROID_HOME}/emulator"
+ export PATH="${PATH}:${ANDROID_NDK}"
+ export ORIG_PATH="${PATH}"
+ export KEYSTORE_PATH="${PWD}/keystores/debug.keystore"
+ nArchs=$(echo "${TARGET_ARCH}" | tr ':' ' ' | wc -w)
+ lastArch=$(echo "${TARGET_ARCH}" | awk -F: '{print $NF}')
+
+ if [ "${nArchs}" = 1 ]; then
+ export PATH="${PWD}/build/Qt/${QTVER}/android/bin:${PWD}/.local/bin:${ORIG_PATH}"
+ export BUILD_PATH=${PWD}/build-webcamoid-${lastArch}
+
+ python3 ports/deploy/deploy.py
+ else
+ pkgMerge=
+
+ for arch_ in $(echo "${TARGET_ARCH}" | tr ":" "\n"); do
+ if [ ! -z "${pkgMerge}" ]; then
+ pkgMerge=${pkgMerge}:
+ fi
+
+ pkgMerge=${pkgMerge}${PWD}/build-webcamoid-${arch_}
+ done
+
+ for arch_ in $(echo "${TARGET_ARCH}" | tr ":" "\n"); do
+ export PATH="${PWD}/build/Qt/${QTVER}/android/bin:${PWD}/.local/bin:${ORIG_PATH}"
+ export BUILD_PATH=${PWD}/build-webcamoid-${arch_}
+
+ if [ "${arch_}" = "${lastArch}" ]; then
+ export PACKAGES_PREPARE_ONLY=0
+ export PACKAGES_MERGE="${pkgMerge}"
+ else
+ export PACKAGES_PREPARE_ONLY=1
+ fi
+
+ export NO_SHOW_PKG_DATA_INFO=1
+
+ python3 ports/deploy/deploy.py
+ done
+ fi
+
+ mkdir -p "${PWD}/ports/deploy/packages_auto"
+ cp -rvf "${PWD}/build-webcamoid-${lastArch}/ports/deploy/packages_auto"/* \
+ "${PWD}/ports/deploy/packages_auto"
+elif [ "${ARCH_ROOT_BUILD}" = 1 ]; then
+ sudo mount --bind root.x86_64 root.x86_64
+ sudo mount --bind $HOME root.x86_64/$HOME
+
+ cat << EOF > ${DEPLOYSCRIPT}
+#!/bin/sh
+
+export LC_ALL=C
+export HOME=$HOME
+export PATH="$TRAVIS_BUILD_DIR/.local/bin:\$PATH"
+export WINEPREFIX=/opt/.wine
+cd $TRAVIS_BUILD_DIR
+EOF
+
+ if [ ! -z "${DAILY_BUILD}" ]; then
+ cat << EOF >> ${DEPLOYSCRIPT}
+export DAILY_BUILD=1
+EOF
+ fi
+
+ cat << EOF >> ${DEPLOYSCRIPT}
+python ports/deploy/deploy.py
+EOF
+ chmod +x ${DEPLOYSCRIPT}
+ sudo cp -vf ${DEPLOYSCRIPT} root.x86_64/$HOME/
+
+ ${EXEC} bash $HOME/${DEPLOYSCRIPT}
+ sudo umount root.x86_64/$HOME
+ sudo umount root.x86_64
+elif [ "${TRAVIS_OS_NAME}" = linux ]; then
+ cat << EOF > ${DEPLOYSCRIPT}
+#!/bin/sh
+
+export PATH="\$PWD/.local/bin:\$PATH"
+xvfb-run --auto-servernum python3 ports/deploy/deploy.py
+EOF
+
+ chmod +x ${DEPLOYSCRIPT}
+
+ ${EXEC} bash ${DEPLOYSCRIPT}
+elif [ "${TRAVIS_OS_NAME}" = osx ]; then
+ ${EXEC} python3 ports/deploy/deploy.py
+fi
diff --git a/ports/ci/travis/install_deps.sh b/ports/ci/travis/install_deps.sh
new file mode 100755
index 0000000..79392ae
--- /dev/null
+++ b/ports/ci/travis/install_deps.sh
@@ -0,0 +1,157 @@
+#!/bin/bash
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+#qtIinstallerVerbose=-v
+
+if [ ! -z "${USE_WGET}" ]; then
+ export DOWNLOAD_CMD="wget -nv -c"
+else
+ export DOWNLOAD_CMD="curl --retry 10 -sS -kLOC -"
+fi
+
+if [ "${TRAVIS_OS_NAME}" = linux ] && [ "${ANDROID_BUILD}" != 1 ]; then
+ if [ "${ARCH_ROOT_BUILD}" = 1 ]; then
+ EXEC='sudo ./root.x86_64/bin/arch-chroot root.x86_64'
+ else
+ EXEC="docker exec ${DOCKERSYS}"
+ fi
+fi
+
+if [ "${ARCH_ROOT_BUILD}" = 1 ]; then
+ # Download chroot image
+ archImage=archlinux-bootstrap-${ARCH_ROOT_DATE}-x86_64.tar.gz
+ ${DOWNLOAD_CMD} ${ARCH_ROOT_URL}/iso/${ARCH_ROOT_DATE}/$archImage
+ sudo tar xzf $archImage
+
+ # Configure mirrors
+ cp -vf root.x86_64/etc/pacman.conf .
+ cat << EOF >> pacman.conf
+
+[multilib]
+Include = /etc/pacman.d/mirrorlist
+
+[ownstuff]
+Server = https://ftp.f3l.de/~martchus/\$repo/os/\$arch
+Server = http://martchus.no-ip.biz/repo/arch/\$repo/os/\$arch
+EOF
+ sed -i 's/Required DatabaseOptional/Never/g' pacman.conf
+ sed -i 's/#TotalDownload/TotalDownload/g' pacman.conf
+ sudo cp -vf pacman.conf root.x86_64/etc/pacman.conf
+
+ cp -vf root.x86_64/etc/pacman.d/mirrorlist .
+ cat << EOF >> mirrorlist
+
+Server = ${ARCH_ROOT_URL}/\$repo/os/\$arch
+EOF
+ sudo cp -vf mirrorlist root.x86_64/etc/pacman.d/mirrorlist
+
+ # Install packages
+ sudo mkdir -pv root.x86_64/$HOME
+ sudo mount --bind root.x86_64 root.x86_64
+ sudo mount --bind $HOME root.x86_64/$HOME
+
+ ${EXEC} pacman-key --init
+ ${EXEC} pacman-key --populate archlinux
+ ${EXEC} pacman -Syu \
+ --noconfirm \
+ --ignore linux,linux-api-headers,linux-docs,linux-firmware,linux-headers,pacman
+
+ ${EXEC} pacman --noconfirm --needed -S \
+ ccache \
+ clang \
+ file \
+ git \
+ make \
+ pkgconf \
+ python \
+ sed \
+ xorg-server-xvfb \
+ wine \
+ mingw-w64-pkg-config \
+ mingw-w64-gcc \
+ mingw-w64-qt5-base
+
+ for mingw_arch in i686 x86_64; do
+ if [ "$mingw_arch" = x86_64 ]; then
+ ff_arch=win64
+ else
+ ff_arch=win32
+ fi
+
+ qtIFW=QtInstallerFramework-win-x86.exe
+
+ # Install Qt Installer Framework
+ ${DOWNLOAD_CMD} http://download.qt.io/official_releases/qt-installer-framework/${QTIFWVER}/${qtIFW} || true
+
+ if [ -e ${qtIFW} ]; then
+ INSTALLSCRIPT=installscript.sh
+
+ cat << EOF > ${INSTALLSCRIPT}
+#!/bin/sh
+
+export LC_ALL=C
+export HOME=$HOME
+export WINEPREFIX=/opt/.wine
+cd $TRAVIS_BUILD_DIR
+
+wine ./${qtIFW} \
+ ${qtIinstallerVerbose} \
+ --script "ports/ci/travis/qtifw_non_interactive_install.qs" \
+ --no-force-installations
+EOF
+
+ chmod +x ${INSTALLSCRIPT}
+ sudo cp -vf ${INSTALLSCRIPT} root.x86_64/$HOME/
+ ${EXEC} bash $HOME/${INSTALLSCRIPT}
+ fi
+
+ # Finish
+ sudo umount root.x86_64/$HOME
+ sudo umount root.x86_64
+elif [ "${TRAVIS_OS_NAME}" = osx ]; then
+ brew update
+ brew upgrade
+ brew link --overwrite numpy
+ brew install \
+ p7zip \
+ python \
+ ccache \
+ pkg-config \
+ qt5 \
+ brew link --overwrite python
+
+ brew link python
+ qtIFW=QtInstallerFramework-mac-x64.dmg
+
+ # Install Qt Installer Framework
+ ${DOWNLOAD_CMD} http://download.qt.io/official_releases/qt-installer-framework/${QTIFWVER}/${qtIFW} || true
+
+ if [ -e "${qtIFW}" ]; then
+ hdiutil convert ${qtIFW} -format UDZO -o qtifw
+ 7z x -oqtifw qtifw.dmg -bb
+ 7z x -oqtifw qtifw/5.hfsx -bb
+ chmod +x qtifw/QtInstallerFramework-mac-x64/QtInstallerFramework-mac-x64.app/Contents/MacOS/QtInstallerFramework-mac-x64
+
+ qtifw/QtInstallerFramework-mac-x64/QtInstallerFramework-mac-x64.app/Contents/MacOS/QtInstallerFramework-mac-x64 \
+ ${qtIinstallerVerbose} \
+ --script "$PWD/ports/ci/travis/qtifw_non_interactive_install.qs" \
+ --no-force-installations
+ fi
+fi
diff --git a/ports/ci/travis/qt_non_interactive_install.qs b/ports/ci/travis/qt_non_interactive_install.qs
new file mode 100644
index 0000000..5c0043d
--- /dev/null
+++ b/ports/ci/travis/qt_non_interactive_install.qs
@@ -0,0 +1,62 @@
+function Controller()
+{
+ installer.autoRejectMessageBoxes();
+ installer.setMessageBoxAutomaticAnswer("OverwriteTargetDirectory", QMessageBox.Yes);
+ installer.setMessageBoxAutomaticAnswer("stopProcessesForUpdates", QMessageBox.Ignore);
+ installer.installationFinished.connect(function() {
+ gui.clickButton(buttons.NextButton);
+ })
+}
+
+Controller.prototype.WelcomePageCallback = function()
+{
+ gui.clickButton(buttons.NextButton, 10000);
+}
+
+Controller.prototype.CredentialsPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.IntroductionPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.TargetDirectoryPageCallback = function()
+{
+ //gui.currentPageWidget().TargetDirectoryLineEdit.setText(installer.value("HomeDir") + "/Qt");
+ gui.currentPageWidget().TargetDirectoryLineEdit.setText(installer.value("InstallerDirPath") + "/Qt");
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.ComponentSelectionPageCallback = function()
+{
+ var widget = gui.currentPageWidget();
+
+ widget.deselectAll();
+ widget.selectComponent("qt.qt5.5141.android");
+
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.LicenseAgreementPageCallback = function()
+{
+ gui.currentPageWidget().AcceptLicenseRadioButton.setChecked(true);
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.ReadyForInstallationPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.FinishedPageCallback = function()
+{
+ var checkBoxForm = gui.currentPageWidget().LaunchQtCreatorCheckBoxForm
+
+ if (checkBoxForm && checkBoxForm.launchQtCreatorCheckBox)
+ checkBoxForm.launchQtCreatorCheckBox.checked = false;
+
+ gui.clickButton(buttons.FinishButton);
+}
diff --git a/ports/ci/travis/qtifw_non_interactive_install.qs b/ports/ci/travis/qtifw_non_interactive_install.qs
new file mode 100644
index 0000000..55d8e67
--- /dev/null
+++ b/ports/ci/travis/qtifw_non_interactive_install.qs
@@ -0,0 +1,50 @@
+function Controller()
+{
+ installer.autoRejectMessageBoxes();
+ installer.setMessageBoxAutomaticAnswer("OverwriteTargetDirectory", QMessageBox.Yes);
+ installer.setMessageBoxAutomaticAnswer("stopProcessesForUpdates", QMessageBox.Ignore);
+ installer.installationFinished.connect(function() {
+ gui.clickButton(buttons.NextButton);
+ })
+}
+
+Controller.prototype.IntroductionPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.TargetDirectoryPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.ComponentSelectionPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.LicenseAgreementPageCallback = function()
+{
+ gui.currentPageWidget().AcceptLicenseRadioButton.setChecked(true);
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.StartMenuDirectoryPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.ReadyForInstallationPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.PerformInstallationPageCallback = function()
+{
+ gui.clickButton(buttons.NextButton);
+}
+
+Controller.prototype.FinishedPageCallback = function()
+{
+ gui.clickButton(buttons.FinishButton);
+}
diff --git a/ports/ci/travis/upload.sh b/ports/ci/travis/upload.sh
new file mode 100755
index 0000000..41fa876
--- /dev/null
+++ b/ports/ci/travis/upload.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+if [ ! -z "${USE_WGET}" ]; then
+ export DOWNLOAD_CMD="wget -nv -c"
+else
+ export DOWNLOAD_CMD="curl --retry 10 -sS -kLOC -"
+fi
+
+if [[ ( ! -z "$DAILY_BUILD" || ! -z "$RELEASE_BUILD" ) && "$TRAVIS_BRANCH" == "master" ]]; then
+ if [ -z "$DAILY_BUILD" ]; then
+ VER_MAJ=$(grep -re '^VER_MAJ[[:space:]]*=[[:space:]]*' commons.pri | awk '{print $3}')
+ VER_MIN=$(grep -re '^VER_MIN[[:space:]]*=[[:space:]]*' commons.pri | awk '{print $3}')
+ VER_PAT=$(grep -re '^VER_PAT[[:space:]]*=[[:space:]]*' commons.pri | awk '{print $3}')
+ version=$VER_MAJ.$VER_MIN.$VER_PAT
+ publish=false
+ else
+ version=daily
+ publish=true
+ fi
+
+ # Upload to Bintray
+
+ curl -fL https://getcli.jfrog.io | sh
+
+ ./jfrog bt config \
+ --user=hipersayanx \
+ --key=$BT_KEY \
+ --licenses=GPL-3.0-or-later
+
+ path=ports/deploy/packages_auto
+
+ for f in $(find $path -type f); do
+ packagePath=${f#$path/}
+ folder=$(dirname $packagePath)
+
+ ./jfrog bt upload \
+ --user=hipersayanx \
+ --key=$BT_KEY \
+ --override=true \
+ --publish=$publish \
+ $f \
+ webcamoid/webcamoid/webcamoid/$version \
+ $folder/
+ done
+
+ # Upload to Github Releases
+ upload=false
+
+ if [[ ! -z "$DAILY_BUILD" && "$TRAVIS_BRANCH" == master && "$upload" == true ]]; then
+ hub=''
+
+ if [ "${TRAVIS_OS_NAME}" = linux ]; then
+ hub=hub-linux-amd64-${GITHUB_HUBVER}
+ else
+ hub=hub-darwin-amd64-${GITHUB_HUBVER}
+ fi
+
+ cd ${TRAVIS_BUILD_DIR}
+ ${DOWNLOAD_CMD} https://github.com/github/hub/releases/download/v${GITHUB_HUBVER}/${hub}.tgz || true
+ tar xzf ${hub}.tgz
+ mkdir -p .local
+ cp -rf ${hub}/* .local/
+
+ export PATH="${PWD}/.local/bin:${PATH}"
+
+ hubTag=$(hub release -df '%T %t%n' | grep 'Daily Build' | awk '{print $1}' | sed 's/.*://')
+
+ if [ -z "$hubTag" ]; then
+ hub release create -p -m 'Daily Build' daily
+ hubTag=$(hub release -df '%T %t%n' | grep 'Daily Build' | awk '{print $1}' | sed 's/.*://')
+ fi
+
+ if [ ! -z "$hubTag" ]; then
+ path=ports/deploy/packages_auto
+
+ for f in $(find $path -type f); do
+ hubTag=$(hub release -df '%T %t%n' | grep 'Daily Build' | awk '{print $1}' | sed 's/.*://')
+ hub release edit -m 'Daily Build' -a "$f" "$hubTag"
+ done
+ fi
+ fi
+fi
diff --git a/ports/deploy/deploy.py b/ports/deploy/deploy.py
new file mode 100755
index 0000000..45a54e9
--- /dev/null
+++ b/ports/deploy/deploy.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import tools.utils
+
+
+if __name__ =='__main__':
+ system = tools.utils.DeployToolsUtils().system
+
+ while True:
+ try:
+ deploy = __import__('deploy_' + system).Deploy()
+ except:
+ print('No valid deploy script found.')
+
+ exit()
+
+ if system == deploy.targetSystem:
+ deploy.run()
+
+ exit()
+
+ system = deploy.targetSystem
diff --git a/ports/deploy/deploy_android.py b/ports/deploy/deploy_android.py
new file mode 100644
index 0000000..9e8ed63
--- /dev/null
+++ b/ports/deploy/deploy_android.py
@@ -0,0 +1,634 @@
+#!/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 .
+#
+# 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()
diff --git a/ports/deploy/deploy_base.py b/ports/deploy/deploy_base.py
new file mode 100644
index 0000000..7a149d6
--- /dev/null
+++ b/ports/deploy/deploy_base.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import os
+import sys
+import platform
+import shutil
+
+import tools.utils
+
+class DeployBase(tools.utils.DeployToolsUtils):
+ def __init__(self):
+ super().__init__()
+ self.rootDir = os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..'))
+ self.buildDir = os.environ['BUILD_PATH'] if 'BUILD_PATH' in os.environ else self.rootDir
+ self.installDir = os.path.join(self.rootDir, 'ports/deploy/temp_priv/root')
+ self.rootInstallDir = ''
+ self.pkgsDir = os.path.join(self.rootDir,
+ 'ports/deploy/packages_auto',
+ sys.platform if os.name == 'posix' else os.name)
+ self.programVersion = ''
+ self.qmake = ''
+
+ def __str__(self):
+ deployInfo = 'Python version: {}\n' \
+ 'Root directory: {}\n' \
+ 'Build directory: {}\n' \
+ 'Install directory: {}\n' \
+ 'Packages directory: {}\n' \
+ 'System: {}\n' \
+ 'Architecture: {}\n' \
+ 'Target system: {}\n' \
+ 'Target architecture: {}\n' \
+ 'Number of threads: {}\n' \
+ 'Program version: {}\n' \
+ 'Make executable: {}\n' \
+ 'Qmake executable: {}'. \
+ format(platform.python_version(),
+ self.rootDir,
+ self.buildDir,
+ self.installDir,
+ self.pkgsDir,
+ self.system,
+ self.arch,
+ self.targetSystem,
+ self.targetArch,
+ self.njobs,
+ self.programVersion,
+ self.make,
+ self.qmake)
+
+ return deployInfo
+
+ def run(self):
+ print('Deploy info\n')
+ print(self)
+ print('\nPreparing for software packaging\n')
+ self.prepare()
+
+ if not 'NO_SHOW_PKG_DATA_INFO' in os.environ \
+ or os.environ['NO_SHOW_PKG_DATA_INFO'] != '1':
+ print('\nPackaged data info\n')
+ self.printPackageDataInfo()
+
+ if 'PACKAGES_PREPARE_ONLY' in os.environ \
+ and os.environ['PACKAGES_PREPARE_ONLY'] == '1':
+ print('\nPackage data is ready for merging\n')
+ else:
+ print('\nCreating packages\n')
+ self.package()
+ print('\nCleaning up')
+ self.cleanup()
+ print('Deploy finnished\n')
+
+ def printPackageDataInfo(self):
+ packagedFiles = []
+
+ for root, _, files in os.walk(self.rootInstallDir):
+ for f in files:
+ packagedFiles.append(os.path.join(root, f))
+
+ packagedFiles = sorted(packagedFiles)
+
+ for f in packagedFiles:
+ print(' ' + f)
+
+ def prepare(self):
+ pass
+
+ def package(self):
+ pass
+
+ def cleanup(self):
+ shutil.rmtree(self.installDir, True)
diff --git a/ports/deploy/deploy_mac.py b/ports/deploy/deploy_mac.py
new file mode 100644
index 0000000..b675e7d
--- /dev/null
+++ b/ports/deploy/deploy_mac.py
@@ -0,0 +1,499 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import math
+import os
+import platform
+import shutil
+import subprocess # nosec
+import sys
+import threading
+import time
+
+import deploy_base
+import tools.binary_mach
+import tools.qt5
+
+
+class Deploy(deploy_base.DeployBase, tools.qt5.DeployToolsQt):
+ def __init__(self):
+ super().__init__()
+ self.installDir = os.path.join(self.buildDir, 'ports/deploy/temp_priv')
+ self.pkgsDir = os.path.join(self.buildDir, 'ports/deploy/packages_auto', self.targetSystem)
+ self.detectQt(os.path.join(self.buildDir, 'StandAlone'))
+ self.rootInstallDir = os.path.join(self.installDir, 'Applications')
+ self.programName = 'webcamoid'
+ self.appBundleDir = os.path.join(self.rootInstallDir, self.programName + '.app')
+ self.execPrefixDir = os.path.join(self.appBundleDir, 'Contents')
+ self.binaryInstallDir = os.path.join(self.execPrefixDir, 'MacOS')
+ self.libInstallDir = os.path.join(self.execPrefixDir, 'Frameworks')
+ self.qmlInstallDir = os.path.join(self.execPrefixDir, 'Resources/qml')
+ self.pluginsInstallDir = os.path.join(self.execPrefixDir, 'Plugins')
+ self.qtConf = os.path.join(self.execPrefixDir, 'Resources/qt.conf')
+ self.qmlRootDirs = ['StandAlone/share/qml', 'libAvKys/Plugins']
+ self.mainBinary = os.path.join(self.binaryInstallDir, self.programName)
+ self.programVersion = self.detectVersion(os.path.join(self.rootDir, 'commons.pri'))
+ self.detectMake()
+ xspec = self.qmakeQuery(var='QMAKE_XSPEC')
+
+ if 'android' in xspec:
+ self.targetSystem = 'android'
+
+ self.binarySolver = tools.binary_mach.DeployToolsBinary()
+ self.binarySolver.readExcludeList(os.path.join(self.rootDir, 'ports/deploy/exclude.{}.{}.txt'.format(os.name, sys.platform)))
+ self.packageConfig = os.path.join(self.rootDir, 'ports/deploy/package_info.conf')
+ self.dependencies = []
+ self.installerConfig = os.path.join(self.installDir, 'installer/config')
+ self.installerPackages = os.path.join(self.installDir, 'installer/packages')
+ self.appIcon = os.path.join(self.execPrefixDir, 'Resources/{0}.icns'.format(self.programName))
+ self.licenseFile = os.path.join(self.rootDir, 'COPYING')
+ self.installerRunProgram = '@TargetDir@/{0}.app/Contents/MacOS/{0}'.format(self.programName)
+ self.installerTargetDir = '@ApplicationsDir@/' + self.programName
+ self.installerScript = os.path.join(self.rootDir, 'ports/deploy/installscript.mac.qs')
+ self.changeLog = os.path.join(self.rootDir, 'ChangeLog')
+ self.outPackage = os.path.join(self.pkgsDir,
+ '{}-{}.dmg'.format(self.programName,
+ self.programVersion))
+
+ def prepare(self):
+ print('Executing make install')
+ self.makeInstall(self.buildDir, self.installDir)
+ self.detectTargetArch()
+
+ print('Copying Qml modules\n')
+ self.solvedepsQml()
+ print('\nCopying required plugins\n')
+ self.solvedepsPlugins()
+ print('\nCopying required libs\n')
+ self.solvedepsLibs()
+ print('\nWritting qt.conf file')
+ self.writeQtConf()
+ print('Stripping symbols')
+ self.binarySolver.stripSymbols(self.installDir)
+ print('Resetting file permissions')
+ self.binarySolver.resetFilePermissions(self.rootInstallDir,
+ self.binaryInstallDir)
+ print('Removing unnecessary files')
+ self.removeUnneededFiles(self.libInstallDir)
+ print('Fixing rpaths\n')
+ self.fixRpaths()
+ print('\nWritting build system information\n')
+ self.writeBuildInfo()
+
+ def solvedepsLibs(self):
+ deps = sorted(self.binarySolver.scanDependencies(self.installDir))
+
+ for dep in deps:
+ depPath = os.path.join(self.libInstallDir, os.path.basename(dep))
+
+ if dep != depPath:
+ print(' {} -> {}'.format(dep, depPath))
+ self.copy(dep, depPath, not dep.endswith('.framework'))
+ self.dependencies.append(dep)
+
+ @staticmethod
+ def removeUnneededFiles(path):
+ adirs = set()
+ afiles = set()
+
+ for root, dirs, files in os.walk(path):
+ for d in dirs:
+ if d == 'Headers':
+ adirs.add(os.path.join(root, d))
+
+ for f in files:
+ if f == 'Headers' or f.endswith('.prl'):
+ afiles.add(os.path.join(root, f))
+
+ for adir in adirs:
+ try:
+ shutil.rmtree(adir, True)
+ except:
+ pass
+
+ for afile in afiles:
+ try:
+ if os.path.islink(afile):
+ os.unlink(afile)
+ else:
+ os.remove(afile)
+ except:
+ pass
+
+ def fixLibRpath(self, mutex, mach):
+ rpath = os.path.join('@executable_path',
+ os.path.relpath(self.libInstallDir,
+ self.binaryInstallDir))
+ log = '\tFixed {}\n\n'.format(mach)
+ machInfo = self.binarySolver.dump(mach)
+
+ # Change rpath
+ if mach.startswith(self.binaryInstallDir):
+ log += '\t\tChanging rpath to {}\n'.format(rpath)
+
+ for oldRpath in machInfo['rpaths']:
+ process = subprocess.Popen(['install_name_tool', # nosec
+ '-delete_rpath', oldRpath, mach],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ process = subprocess.Popen(['install_name_tool', # nosec
+ '-add_rpath', rpath, mach],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ # Change ID
+ if mach.startswith(self.binaryInstallDir):
+ newMachId = machInfo['id']
+ elif mach.startswith(self.libInstallDir):
+ newMachId = mach.replace(self.libInstallDir, rpath)
+ else:
+ newMachId = os.path.basename(mach)
+
+ if newMachId != machInfo['id']:
+ log += '\t\tChanging ID to {}\n'.format(newMachId)
+
+ process = subprocess.Popen(['install_name_tool', # nosec
+ '-id', newMachId, mach],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ # Change library links
+ for dep in machInfo['imports']:
+ if dep.startswith(rpath):
+ continue
+
+ if self.binarySolver.isExcluded(dep):
+ continue
+
+ basename = os.path.basename(dep)
+ framework = ''
+ inFrameworkPath = ''
+
+ if not basename.endswith('.dylib'):
+ frameworkPath = dep[: dep.rfind('.framework')] + '.framework'
+ framework = os.path.basename(frameworkPath)
+ inFrameworkPath = os.path.join(framework, dep.replace(frameworkPath + '/', ''))
+
+ newDepPath = os.path.join(rpath, basename if len(framework) < 1 else inFrameworkPath)
+
+ if dep != newDepPath:
+ log += '\t\t{} -> {}\n'.format(dep, newDepPath)
+
+ process = subprocess.Popen(['install_name_tool', # nosec
+ '-change', dep, newDepPath, mach],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ mutex.acquire()
+ print(log)
+ mutex.release()
+
+ def fixRpaths(self):
+ path = os.path.join(self.execPrefixDir)
+ mutex = threading.Lock()
+ threads = []
+
+ for mach in self.binarySolver.find(path):
+ thread = threading.Thread(target=self.fixLibRpath, args=(mutex, mach,))
+ threads.append(thread)
+
+ while threading.active_count() >= self.njobs:
+ time.sleep(0.25)
+
+ thread.start()
+
+ for thread in threads:
+ thread.join()
+
+ @staticmethod
+ def searchPackageFor(cellarPath, path):
+ if not path.startswith(cellarPath):
+ return ''
+
+ return ' '.join(path.replace(cellarPath + os.sep, '').split(os.sep)[0: 2])
+
+ 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():
+ process = subprocess.Popen(['sw_vers'], # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+
+ return stdout.decode(sys.getdefaultencoding()).strip()
+
+ def writeBuildInfo(self):
+ resourcesDir = os.path.join(self.execPrefixDir, 'Resources')
+ os.makedirs(self.pkgsDir)
+ depsInfoFile = os.path.join(resourcesDir, 'build-info.txt')
+
+ # 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')
+
+ os.environ['LC_ALL'] = 'C'
+ brew = self.whereBin('brew')
+
+ if len(brew) < 1:
+ return
+
+ process = subprocess.Popen([brew, '--cellar'], # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+ cellarPath = stdout.decode(sys.getdefaultencoding()).strip()
+
+ # Write binary dependencies info.
+
+ packages = set()
+
+ for dep in self.dependencies:
+ packageInfo = self.searchPackageFor(cellarPath, 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')
+
+ @staticmethod
+ def dirSize(path):
+ size = 0
+
+ for root, _, files in os.walk(path):
+ for f in files:
+ fpath = os.path.join(root, f)
+
+ if not os.path.islink(fpath):
+ size += os.path.getsize(fpath)
+
+ return size
+
+ # https://asmaloney.com/2013/07/howto/packaging-a-mac-os-x-application-using-a-dmg/
+ def createPortable(self, mutex):
+ staggingDir = os.path.join(self.installDir, 'stagging')
+
+ if not os.path.exists(staggingDir):
+ os.makedirs(staggingDir)
+
+ self.copy(self.appBundleDir,
+ os.path.join(staggingDir, self.programName + '.app'))
+ imageSize = self.dirSize(staggingDir)
+ tmpDmg = os.path.join(self.installDir, self.programName + '_tmp.dmg')
+ volumeName = "{}-portable-{}".format(self.programName,
+ self.programVersion)
+
+ process = subprocess.Popen(['hdiutil', 'create', # nosec
+ '-srcfolder', staggingDir,
+ '-volname', volumeName,
+ '-fs', 'HFS+',
+ '-fsargs', '-c c=64,a=16,e=16',
+ '-format', 'UDRW',
+ '-size', str(math.ceil(imageSize * 1.1)),
+ tmpDmg],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ process = subprocess.Popen(['hdiutil', # nosec
+ 'attach',
+ '-readwrite',
+ '-noverify',
+ tmpDmg],
+ stdout=subprocess.PIPE)
+ stdout, _ = process.communicate()
+ device = ''
+
+ for line in stdout.split(b'\n'):
+ line = line.strip()
+
+ if len(line) < 1:
+ continue
+
+ dev = line.split()
+
+ if len(dev) > 2:
+ device = dev[0].decode(sys.getdefaultencoding())
+
+ break
+
+ time.sleep(2)
+ volumePath = os.path.join('/Volumes', volumeName)
+ volumeIcon = os.path.join(volumePath, '.VolumeIcon.icns')
+ self.copy(self.appIcon, volumeIcon)
+
+ process = subprocess.Popen(['SetFile', # nosec
+ '-c', 'icnC',
+ volumeIcon],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ process = subprocess.Popen(['SetFile', # nosec
+ '-a', 'C',
+ volumePath],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ appsShortcut = os.path.join(volumePath, 'Applications')
+
+ if not os.path.exists(appsShortcut):
+ os.symlink('/Applications', appsShortcut)
+
+ os.sync()
+
+ process = subprocess.Popen(['hdiutil', # nosec
+ 'detach',
+ device],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ packagePath = \
+ os.path.join(self.pkgsDir,
+ '{}-portable-{}-{}.dmg'.format(self.programName,
+ self.programVersion,
+ platform.machine()))
+
+ if not os.path.exists(self.pkgsDir):
+ os.makedirs(self.pkgsDir)
+
+ if os.path.exists(packagePath):
+ os.remove(packagePath)
+
+ process = subprocess.Popen(['hdiutil', # nosec
+ 'convert',
+ tmpDmg,
+ '-format', 'UDZO',
+ '-imagekey', 'zlib-level=9',
+ '-o', packagePath],
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ mutex.acquire()
+ print('Created portable package:')
+ self.printPackageInfo(packagePath)
+ mutex.release()
+
+ def createAppInstaller(self, mutex):
+ packagePath = self.createInstaller()
+
+ if not packagePath:
+ return
+
+ mutex.acquire()
+ print('Created installable package:')
+ self.printPackageInfo(self.outPackage)
+ mutex.release()
+
+ def package(self):
+ mutex = threading.Lock()
+
+ threads = [threading.Thread(target=self.createPortable, args=(mutex,))]
+ packagingTools = ['dmg']
+
+ if self.qtIFW != '':
+ threads.append(threading.Thread(target=self.createAppInstaller, args=(mutex,)))
+ packagingTools += ['Qt Installer Framework']
+
+ if len(packagingTools) > 0:
+ print('Detected packaging tools: {}\n'.format(', '.join(packagingTools)))
+
+ for thread in threads:
+ thread.start()
+
+ for thread in threads:
+ thread.join()
diff --git a/ports/deploy/deploy_posix.py b/ports/deploy/deploy_posix.py
new file mode 100644
index 0000000..6ee44b7
--- /dev/null
+++ b/ports/deploy/deploy_posix.py
@@ -0,0 +1,497 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import configparser
+import math
+import os
+import platform
+import subprocess # nosec
+import sys
+import tarfile
+import threading
+
+import deploy_base
+import tools.binary_elf
+import tools.qt5
+
+
+class Deploy(deploy_base.DeployBase, tools.qt5.DeployToolsQt):
+ def __init__(self):
+ super().__init__()
+ self.installDir = os.path.join(self.buildDir, 'ports/deploy/temp_priv')
+ self.pkgsDir = os.path.join(self.buildDir, 'ports/deploy/packages_auto', sys.platform)
+ self.detectQt(os.path.join(self.buildDir, 'StandAlone'))
+ self.rootInstallDir = os.path.join(self.installDir, self.qmakeQuery(var='QT_INSTALL_PREFIX')[1:])
+ self.binaryInstallDir = os.path.join(self.rootInstallDir, 'bin')
+ self.libInstallDir = self.qmakeQuery(var='QT_INSTALL_LIBS') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.libQtInstallDir = self.qmakeQuery(var='QT_INSTALL_ARCHDATA') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.qmlInstallDir = self.qmakeQuery(var='QT_INSTALL_QML') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.pluginsInstallDir = self.qmakeQuery(var='QT_INSTALL_PLUGINS') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.qtConf = os.path.join(self.binaryInstallDir, 'qt.conf')
+ self.qmlRootDirs = ['StandAlone/share/qml', 'libAvKys/Plugins']
+ self.mainBinary = os.path.join(self.binaryInstallDir, 'webcamoid')
+ self.programName = os.path.basename(self.mainBinary)
+ self.programVersion = self.detectVersion(os.path.join(self.rootDir, 'commons.pri'))
+ self.detectMake()
+ xspec = self.qmakeQuery(var='QMAKE_XSPEC')
+
+ if 'win32' in xspec:
+ self.targetSystem = 'posix_windows'
+ elif 'android' in xspec:
+ self.targetSystem = 'android'
+
+ self.binarySolver = tools.binary_elf.DeployToolsBinary()
+ self.binarySolver.readExcludeList(os.path.join(self.rootDir, 'ports/deploy/exclude.{}.{}.txt'.format(os.name, sys.platform)))
+ self.packageConfig = os.path.join(self.rootDir, 'ports/deploy/package_info.conf')
+ self.dependencies = []
+ self.installerConfig = os.path.join(self.installDir, 'installer/config')
+ self.installerPackages = os.path.join(self.installDir, 'installer/packages')
+ self.installerIconSize = 128
+ self.appIcon = os.path.join(self.installDir,
+ 'usr/share/icons/hicolor/{1}x{1}/apps/{0}.png'.format(self.programName,
+ self.installerIconSize))
+ self.licenseFile = os.path.join(self.rootDir, 'COPYING')
+ self.installerRunProgram = '@TargetDir@/' + self.programName + '.sh'
+ self.installerTargetDir = '@HomeDir@/' + self.programName
+ self.installerScript = os.path.join(self.rootDir, 'ports/deploy/installscript.posix.qs')
+ self.changeLog = os.path.join(self.rootDir, 'ChangeLog')
+ self.outPackage = os.path.join(self.pkgsDir,
+ 'webcamoid-installer-{}-{}.run'.format(self.programVersion,
+ platform.machine()))
+
+ def detectAppImage(self):
+ if 'APPIMAGETOOL' in os.environ:
+ return os.environ['APPIMAGETOOL']
+
+ appimagetool = self.whereBin('appimagetool')
+
+ if len(appimagetool) > 0:
+ return appimagetool
+
+ if self.targetArch == '32bit':
+ return self.whereBin('appimagetool-i686.AppImage')
+
+ return self.whereBin('appimagetool-x86_64.AppImage')
+
+ def solvedepsLibs(self):
+ qtLibsPath = self.qmakeQuery(var='QT_INSTALL_LIBS')
+ self.binarySolver.ldLibraryPath.append(qtLibsPath)
+ deps = sorted(self.binarySolver.scanDependencies(self.installDir))
+
+ for dep in deps:
+ 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 prepare(self):
+ print('Executing make install')
+ self.makeInstall(self.buildDir, self.installDir)
+ self.detectTargetArch()
+ self.appImage = self.detectAppImage()
+ print('Copying Qml modules\n')
+ self.solvedepsQml()
+ print('\nCopying required plugins\n')
+ self.solvedepsPlugins()
+ print('\nCopying required libs\n')
+ self.solvedepsLibs()
+ print('\nWritting qt.conf file')
+ self.writeQtConf()
+ print('Stripping symbols')
+ self.binarySolver.stripSymbols(self.installDir)
+ print('Resetting file permissions')
+ self.binarySolver.resetFilePermissions(self.rootInstallDir,
+ self.binaryInstallDir)
+ print('Writting launcher file')
+ self.createLauncher()
+ print('\nWritting build system information\n')
+ self.writeBuildInfo()
+
+ 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()
+
+ pkg = self.whereBin('pkg')
+
+ if len(pkg) > 0:
+ process = subprocess.Popen([pkg, 'which', '-q', 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()
+
+ if len(info) < 1:
+ info = ' '.join(platform.uname())
+
+ return info
+
+ def writeBuildInfo(self):
+ shareDir = os.path.join(self.rootInstallDir, 'share')
+
+ 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 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')
+
+ def createLauncher(self):
+ path = os.path.join(self.rootInstallDir, self.programName) + '.sh'
+ libDir = self.qmakeQuery(var='QT_INSTALL_LIBS') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ '${ROOTDIR}')
+
+ with open(path, 'w') as launcher:
+ launcher.write('#!/bin/sh\n')
+ launcher.write('\n')
+ launcher.write('path=$(realpath "$0")\n')
+ launcher.write('ROOTDIR=$(dirname "$path")\n')
+ launcher.write('export PATH="${ROOTDIR}/bin:$PATH"\n')
+ launcher.write('export LD_LIBRARY_PATH="{}:$LD_LIBRARY_PATH"\n'.format(libDir))
+ launcher.write('#export QT_DEBUG_PLUGINS=1\n')
+ launcher.write('{} "$@"\n'.format(self.programName))
+
+ os.chmod(path, 0o744)
+
+ @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 createPortable(self, mutex):
+ packagePath = \
+ os.path.join(self.pkgsDir,
+ '{}-portable-{}-{}.tar.xz'.format(self.programName,
+ self.programVersion,
+ platform.machine()))
+
+ if not os.path.exists(self.pkgsDir):
+ os.makedirs(self.pkgsDir)
+
+ with tarfile.open(packagePath, 'w:xz') as tar:
+ tar.add(self.rootInstallDir, self.programName)
+
+ mutex.acquire()
+ print('Created portable package:')
+ self.printPackageInfo(packagePath)
+ mutex.release()
+
+ def createAppInstaller(self, mutex):
+ packagePath = self.createInstaller()
+
+ if not packagePath:
+ return
+
+ mutex.acquire()
+ print('Created installable package:')
+ self.printPackageInfo(self.outPackage)
+ mutex.release()
+
+ def createAppImage(self, mutex):
+ if not os.path.exists(self.appImage):
+ return
+
+ appDir = \
+ os.path.join(self.installDir,
+ '{}-{}-{}.AppDir'.format(self.programName,
+ self.programVersion,
+ platform.machine()))
+
+ usrDir = os.path.join(appDir, 'usr')
+
+ if not os.path.exists(usrDir):
+ os.makedirs(usrDir)
+
+ self.copy(self.rootInstallDir, usrDir)
+ launcher = os.path.join(appDir, 'AppRun')
+
+ if not os.path.exists(launcher):
+ os.symlink('./usr/webcamoid.sh', launcher)
+
+ desktopFile = os.path.join(appDir, 'webcamoid.desktop')
+
+ if os.path.exists(desktopFile):
+ os.remove(desktopFile)
+
+ self.copy(os.path.join(usrDir, 'share/applications/webcamoid.desktop'), desktopFile)
+ config = configparser.ConfigParser()
+ config.optionxform=str
+ config.read(desktopFile, 'utf-8')
+ config['Desktop Entry']['Exec'] = 'webcamoid.sh'
+ del config['Desktop Entry']['Keywords']
+
+ with open(desktopFile, 'w', encoding='utf-8') as configFile:
+ config.write(configFile, space_around_delimiters=False)
+
+ icons = {'webcamoid.png': '256x256', '.DirIcon': '48x48'}
+
+ for icon in icons:
+ iconPath = os.path.join(appDir, icon)
+
+ if not os.path.exists(icon):
+ sourceIcon = os.path.join(usrDir,
+ 'share/icons/hicolor/{}/apps/webcamoid.png'.format(icons[icon]))
+ self.copy(sourceIcon, iconPath)
+
+ # Remove old file
+ packagePath = \
+ os.path.join(self.pkgsDir,
+ '{}-{}-{}.AppImage'.format(self.programName,
+ self.programVersion,
+ platform.machine()))
+
+ if not os.path.exists(self.pkgsDir):
+ os.makedirs(self.pkgsDir)
+
+ if os.path.exists(packagePath):
+ os.remove(packagePath)
+
+ penv = os.environ.copy()
+ penv['ARCH'] = platform.machine()
+
+ libPaths = []
+
+ if 'LD_LIBRARY_PATH' in penv:
+ libPaths = penv['LD_LIBRARY_PATH'].split(':')
+
+ libDir = os.path.relpath(self.qmakeQuery(var='QT_INSTALL_LIBS'),
+ self.qmakeQuery(var='QT_INSTALL_PREFIX'))
+ penv['LD_LIBRARY_PATH'] = \
+ ':'.join([os.path.abspath(os.path.join(self.appImage, '../..', libDir)),
+ os.path.abspath(os.path.join(self.appImage, '../../lib'))]
+ + libPaths)
+
+ process = subprocess.Popen([self.appImage, # nosec
+ '-v',
+ '--no-appstream',
+ '--comp', 'xz',
+ appDir,
+ packagePath],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=penv)
+ process.communicate()
+
+ mutex.acquire()
+ print('Created AppImage package:')
+ self.printPackageInfo(packagePath)
+ mutex.release()
+
+ def package(self):
+ mutex = threading.Lock()
+
+ threads = [threading.Thread(target=self.createPortable, args=(mutex,))]
+ packagingTools = ['tar.xz']
+
+ if self.qtIFW != '':
+ threads.append(threading.Thread(target=self.createAppInstaller, args=(mutex,)))
+ packagingTools += ['Qt Installer Framework']
+
+ if self.appImage != '':
+ threads.append(threading.Thread(target=self.createAppImage, args=(mutex,)))
+ packagingTools += ['AppImage']
+
+ if len(packagingTools) > 0:
+ print('Detected packaging tools: {}\n'.format(', '.join(packagingTools)))
+
+ for thread in threads:
+ thread.start()
+
+ for thread in threads:
+ thread.join()
diff --git a/ports/deploy/deploy_posix_windows.py b/ports/deploy/deploy_posix_windows.py
new file mode 100644
index 0000000..8ef462e
--- /dev/null
+++ b/ports/deploy/deploy_posix_windows.py
@@ -0,0 +1,464 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import math
+import os
+import subprocess # nosec
+import sys
+import threading
+import zipfile
+
+import deploy_base
+import tools.binary_pecoff
+import tools.qt5
+
+
+class Deploy(deploy_base.DeployBase, tools.qt5.DeployToolsQt):
+ def __init__(self):
+ super().__init__()
+ self.targetSystem = 'posix_windows'
+ self.installDir = os.path.join(self.buildDir, 'ports/deploy/temp_priv')
+ self.pkgsDir = os.path.join(self.buildDir, 'ports/deploy/packages_auto/windows')
+ self.detectQt(os.path.join(self.buildDir, 'StandAlone'))
+ self.programName = 'webcamoid'
+ self.rootInstallDir = os.path.join(self.installDir, self.programName)
+ self.binaryInstallDir = os.path.join(self.rootInstallDir, 'bin')
+ self.libInstallDir = self.qmakeQuery(var='QT_INSTALL_LIBS') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.libQtInstallDir = self.qmakeQuery(var='QT_INSTALL_ARCHDATA') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.qmlInstallDir = self.qmakeQuery(var='QT_INSTALL_QML') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.pluginsInstallDir = self.qmakeQuery(var='QT_INSTALL_PLUGINS') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.qtConf = os.path.join(self.binaryInstallDir, 'qt.conf')
+ self.qmlRootDirs = ['StandAlone/share/qml', 'libAvKys/Plugins']
+ self.mainBinary = os.path.join(self.binaryInstallDir, self.programName + '.exe')
+ self.programName = os.path.splitext(os.path.basename(self.mainBinary))[0]
+ self.programVersion = self.detectVersion(os.path.join(self.rootDir, 'commons.pri'))
+ self.detectMake()
+ self.binarySolver = tools.binary_pecoff.DeployToolsBinary()
+ self.binarySolver.readExcludeList(os.path.join(self.rootDir, 'ports/deploy/exclude.{}.{}.txt'.format(os.name, sys.platform)))
+ self.packageConfig = os.path.join(self.rootDir, 'ports/deploy/package_info.conf')
+ self.dependencies = []
+ self.installerConfig = os.path.join(self.installDir, 'installer/config')
+ self.installerPackages = os.path.join(self.installDir, 'installer/packages')
+ self.installerIconSize = 256
+ self.appIcon = os.path.join(self.rootDir,
+ 'StandAlone/share/themes/WebcamoidTheme/icons/hicolor/{1}x{1}/{0}.ico'.format(self.programName,
+ self.installerIconSize))
+ self.licenseFile = os.path.join(self.rootDir, 'COPYING')
+ self.installerRunProgram = '@TargetDir@/bin/' + self.programName + '.exe'
+ self.installerScript = os.path.join(self.rootDir, 'ports/deploy/installscript.windows.qs')
+ self.changeLog = os.path.join(self.rootDir, 'ChangeLog')
+ self.targetArch = '64bit' if 'x86_64' in self.qtInstallBins else '32bit'
+
+ @staticmethod
+ def removeUnneededFiles(path):
+ afiles = set()
+
+ for root, _, files in os.walk(path):
+ for f in files:
+ if f.endswith('.a') \
+ or f.endswith('.static.prl') \
+ or f.endswith('.pdb') \
+ or f.endswith('.lib'):
+ 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.installDir)
+
+ if self.targetArch == '32bit':
+ self.binarySolver.sysBinsPath = ['/usr/i686-w64-mingw32/bin']
+ else:
+ self.binarySolver.sysBinsPath = ['/usr/x86_64-w64-mingw32/bin']
+
+ self.binarySolver.detectStrip()
+
+ if self.qtIFWVersion == '' or int(self.qtIFWVersion.split('.')[0]) < 3:
+ appsDir = '@ApplicationsDir@'
+ else:
+ if self.targetArch == '32bit':
+ appsDir = '@ApplicationsDirX86@'
+ else:
+ appsDir = '@ApplicationsDirX64@'
+
+ self.installerTargetDir = appsDir + '/' + self.programName
+ arch = 'win32' if self.targetArch == '32bit' else 'win64'
+ self.outPackage = os.path.join(self.pkgsDir,
+ '{}-{}-{}.exe'.format(self.programName,
+ self.programVersion,
+ arch))
+
+ print('Copying Qml modules\n')
+ self.solvedepsQml()
+ print('\nCopying required plugins\n')
+ self.solvedepsPlugins()
+ print('\nRemoving Qt debug libraries')
+ self.removeDebugs()
+ print('Copying required libs\n')
+ self.solvedepsLibs()
+ print('\nWritting qt.conf file')
+ self.writeQtConf()
+ print('Stripping symbols')
+ self.binarySolver.stripSymbols(self.installDir)
+ print('Writting launcher file')
+ self.createLauncher()
+ print('Removing unnecessary files')
+ self.removeUnneededFiles(self.installDir)
+ print('\nWritting build system information\n')
+ self.writeBuildInfo()
+
+ def solvedepsLibs(self):
+ deps = set(self.binarySolver.scanDependencies(self.installDir))
+ extraDeps = ['libeay32.dll',
+ 'ssleay32.dll',
+ 'libEGL.dll',
+ 'libGLESv2.dll',
+ 'D3DCompiler_43.dll',
+ 'D3DCompiler_46.dll',
+ 'D3DCompiler_47.dll']
+
+ for dep in extraDeps:
+ path = self.whereBin(dep)
+
+ if path != '':
+ deps.add(path)
+
+ for depPath in self.binarySolver.allDependencies(path):
+ deps.add(depPath)
+
+ deps = sorted(deps)
+
+ for dep in deps:
+ depPath = os.path.join(self.binaryInstallDir, os.path.basename(dep))
+
+ if dep != depPath:
+ print(' {} -> {}'.format(dep, depPath))
+ self.copy(dep, depPath)
+ self.dependencies.append(dep)
+
+ def removeDebugs(self):
+ dbgFiles = set()
+
+ for root, _, files in os.walk(self.libQtInstallDir):
+ for f in files:
+ if f.endswith('.dll'):
+ fname, ext = os.path.splitext(f)
+ dbgFile = os.path.join(root, '{}d{}'.format(fname, ext))
+
+ if os.path.exists(dbgFile):
+ dbgFiles.add(dbgFile)
+
+ for f in dbgFiles:
+ os.remove(f)
+
+ 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, 'share')
+
+ try:
+ os.makedirs(self.pkgsDir)
+ except:
+ pass
+
+ depsInfoFile = os.path.join(shareDir, 'build-info.txt')
+
+ # 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 Wine version and emulated system info.
+
+ process = subprocess.Popen(['wine', '--version'], # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+ wineVersion = stdout.decode(sys.getdefaultencoding()).strip()
+
+ with open(depsInfoFile, 'a') as f:
+ print(' Wine Version: {}'.format(wineVersion))
+ f.write('Wine Version: {}\n'.format(wineVersion))
+
+ process = subprocess.Popen(['wine', 'cmd', '/c', 'ver'], # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+ fakeWindowsVersion = stdout.decode(sys.getdefaultencoding()).strip()
+
+ if len(fakeWindowsVersion) < 1:
+ fakeWindowsVersion = 'Unknown'
+
+ with open(depsInfoFile, 'a') as f:
+ print(' Windows Version: {}'.format(fakeWindowsVersion))
+ f.write('Windows Version: {}\n'.format(fakeWindowsVersion))
+ 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')
+
+ def createLauncher(self):
+ path = os.path.join(self.rootInstallDir, self.programName) + '.bat'
+ libDir = os.path.relpath(self.libInstallDir, self.rootInstallDir)
+
+ with open(path, 'w') as launcher:
+ launcher.write('@echo off\n')
+ launcher.write('\n')
+ launcher.write('rem Default values: desktop | angle | software\n')
+ launcher.write('rem set QT_OPENGL=angle\n')
+ launcher.write('\n')
+ launcher.write('rem Default values: d3d11 | d3d9 | warp\n')
+ launcher.write('rem set QT_ANGLE_PLATFORM=d3d11\n')
+ launcher.write('\n')
+ launcher.write('rem Default values: software | d3d12 | openvg\n')
+ launcher.write('rem set QT_QUICK_BACKEND=""\n')
+ launcher.write('\n')
+ launcher.write('start /b "" '
+ + '"%~dp0bin\\{}" '.format(self.programName)
+ + '-p "%~dp0{}\\avkys" '.format(libDir)
+ + '-c "%~dp0share\\config"\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 createPortable(self, mutex):
+ arch = 'win32' if self.targetArch == '32bit' else 'win64'
+ packagePath = \
+ os.path.join(self.pkgsDir,
+ '{}-portable-{}-{}.zip'.format(self.programName,
+ self.programVersion,
+ arch))
+
+ if not os.path.exists(self.pkgsDir):
+ os.makedirs(self.pkgsDir)
+
+ with zipfile.ZipFile(packagePath, 'w', zipfile.ZIP_DEFLATED, False) as zipFile:
+ for root, dirs, files in os.walk(self.rootInstallDir):
+ for f in dirs + files:
+ filePath = os.path.join(root, f)
+ dstPath = os.path.join(self.programName,
+ filePath.replace(self.rootInstallDir + os.sep, ''))
+ zipFile.write(filePath, dstPath)
+
+ mutex.acquire()
+ print('Created portable package:')
+ self.printPackageInfo(packagePath)
+ mutex.release()
+
+ def createAppInstaller(self, mutex):
+ packagePath = self.createInstaller()
+
+ if not packagePath:
+ return
+
+ mutex.acquire()
+ print('Created installable package:')
+ self.printPackageInfo(self.outPackage)
+ mutex.release()
+
+ def package(self):
+ mutex = threading.Lock()
+
+ threads = [threading.Thread(target=self.createPortable, args=(mutex,))]
+ packagingTools = ['zip']
+
+ if self.qtIFW != '':
+ threads.append(threading.Thread(target=self.createAppInstaller, args=(mutex,)))
+ packagingTools += ['Qt Installer Framework']
+
+ if len(packagingTools) > 0:
+ print('Detected packaging tools: {}\n'.format(', '.join(packagingTools)))
+
+ for thread in threads:
+ thread.start()
+
+ for thread in threads:
+ thread.join()
diff --git a/ports/deploy/deploy_windows.py b/ports/deploy/deploy_windows.py
new file mode 100644
index 0000000..0ec4842
--- /dev/null
+++ b/ports/deploy/deploy_windows.py
@@ -0,0 +1,393 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import math
+import os
+import platform
+import subprocess # nosec
+import sys
+import threading
+import zipfile
+
+import deploy_base
+import tools.binary_pecoff
+import tools.qt5
+
+
+class Deploy(deploy_base.DeployBase, tools.qt5.DeployToolsQt):
+ def __init__(self):
+ super().__init__()
+ self.installDir = os.path.join(self.buildDir, 'ports/deploy/temp_priv')
+ self.pkgsDir = os.path.join(self.buildDir, 'ports/deploy/packages_auto/windows')
+ self.detectQt(os.path.join(self.buildDir, 'StandAlone'))
+ self.rootInstallDir = os.path.join(self.installDir, 'usr')
+ self.binaryInstallDir = os.path.join(self.rootInstallDir, 'bin')
+ self.libInstallDir = self.qmakeQuery(var='QT_INSTALL_LIBS') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.libQtInstallDir = self.qmakeQuery(var='QT_INSTALL_ARCHDATA') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.qmlInstallDir = self.qmakeQuery(var='QT_INSTALL_QML') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.pluginsInstallDir = self.qmakeQuery(var='QT_INSTALL_PLUGINS') \
+ .replace(self.qmakeQuery(var='QT_INSTALL_PREFIX'),
+ self.rootInstallDir)
+ self.qtConf = os.path.join(self.binaryInstallDir, 'qt.conf')
+ self.qmlRootDirs = ['StandAlone/share/qml', 'libAvKys/Plugins']
+ self.mainBinary = os.path.join(self.binaryInstallDir, 'webcamoid.exe')
+ self.programName = os.path.splitext(os.path.basename(self.mainBinary))[0]
+ self.programVersion = self.detectVersion(os.path.join(self.rootDir, 'commons.pri'))
+ self.detectMake()
+ xspec = self.qmakeQuery(var='QMAKE_XSPEC')
+
+ if 'android' in xspec:
+ self.targetSystem = 'android'
+
+ self.binarySolver = tools.binary_pecoff.DeployToolsBinary()
+ self.binarySolver.readExcludeList(os.path.join(self.rootDir, 'ports/deploy/exclude.{}.{}.txt'.format(os.name, sys.platform)))
+ self.packageConfig = os.path.join(self.rootDir, 'ports/deploy/package_info.conf')
+ self.dependencies = []
+ self.installerConfig = os.path.join(self.installDir, 'installer/config')
+ self.installerPackages = os.path.join(self.installDir, 'installer/packages')
+ self.installerIconSize = 256
+ self.appIcon = os.path.join(self.rootDir,
+ 'StandAlone/share/themes/WebcamoidTheme/icons/hicolor/{1}x{1}/{0}.ico'.format(self.programName,
+ self.installerIconSize))
+ self.licenseFile = os.path.join(self.rootDir, 'COPYING')
+ self.installerRunProgram = '@TargetDir@/bin/' + self.programName + '.exe'
+ self.installerScript = os.path.join(self.rootDir, 'ports/deploy/installscript.windows.qs')
+ self.changeLog = os.path.join(self.rootDir, 'ChangeLog')
+
+ def prepare(self):
+ print('Executing make install')
+ self.makeInstall(self.buildDir)
+ self.detectTargetArch()
+
+ if self.qtIFWVersion == '' or int(self.qtIFWVersion.split('.')[0]) < 3:
+ appsDir = '@ApplicationsDir@'
+ else:
+ if self.targetArch == '32bit':
+ appsDir = '@ApplicationsDirX86@'
+ else:
+ appsDir = '@ApplicationsDirX64@'
+
+ self.installerTargetDir = appsDir + '/' + self.programName
+ arch = 'win32' if self.targetArch == '32bit' else 'win64'
+ self.outPackage = os.path.join(self.pkgsDir,
+ 'webcamoid-{}-{}.exe'.format(self.programVersion,
+ arch))
+
+ print('Copying Qml modules\n')
+ self.solvedepsQml()
+ print('\nCopying required plugins\n')
+ self.solvedepsPlugins()
+ print('\nRemoving Qt debug libraries')
+ self.removeDebugs()
+ print('Copying required libs\n')
+ self.solvedepsLibs()
+ print('\nWritting qt.conf file')
+ self.writeQtConf()
+ print('Stripping symbols')
+ self.binarySolver.stripSymbols(self.installDir)
+ print('Writting launcher file')
+ self.createLauncher()
+ print('Removing unnecessary files')
+ self.removeUnneededFiles(self.installDir)
+ print('\nWritting build system information\n')
+ self.writeBuildInfo()
+
+ def solvedepsLibs(self):
+ deps = set(self.binarySolver.scanDependencies(self.installDir))
+ extraDeps = ['libeay32.dll',
+ 'ssleay32.dll',
+ 'libEGL.dll',
+ 'libGLESv2.dll',
+ 'D3DCompiler_43.dll',
+ 'D3DCompiler_46.dll',
+ 'D3DCompiler_47.dll']
+
+ for dep in extraDeps:
+ path = self.whereBin(dep)
+
+ if path != '':
+ deps.add(path)
+
+ for depPath in self.binarySolver.allDependencies(path):
+ deps.add(depPath)
+
+ deps = sorted(deps)
+
+ for dep in deps:
+ dep = dep.replace('\\', '/')
+ depPath = os.path.join(self.binaryInstallDir, os.path.basename(dep))
+ depPath = depPath.replace('\\', '/')
+
+ if dep != depPath:
+ print(' {} -> {}'.format(dep, depPath))
+ self.copy(dep, depPath)
+ self.dependencies.append(dep)
+
+ def removeDebugs(self):
+ dbgFiles = set()
+
+ for root, _, files in os.walk(self.libQtInstallDir):
+ for f in files:
+ if f.endswith('.dll'):
+ fname, ext = os.path.splitext(f)
+ dbgFile = os.path.join(root, '{}d{}'.format(fname, ext))
+
+ if os.path.exists(dbgFile):
+ dbgFiles.add(dbgFile)
+
+ for f in dbgFiles:
+ os.remove(f)
+
+ def createLauncher(self):
+ path = os.path.join(self.rootInstallDir, self.programName) + '.bat'
+ libDir = os.path.relpath(self.libInstallDir, self.rootInstallDir)
+
+ with open(path, 'w') as launcher:
+ launcher.write('@echo off\n')
+ launcher.write('\n')
+ launcher.write('rem Default values: desktop | angle | software\n')
+ launcher.write('rem set QT_OPENGL=angle\n')
+ launcher.write('\n')
+ launcher.write('rem Default values: d3d11 | d3d9 | warp\n')
+ launcher.write('rem set QT_ANGLE_PLATFORM=d3d11\n')
+ launcher.write('\n')
+ launcher.write('rem Default values: software | d3d12 | openvg\n')
+ launcher.write('rem set QT_QUICK_BACKEND=""\n')
+ launcher.write('\n')
+ launcher.write('start /b "" '
+ + '"%~dp0bin\\{}" '.format(self.programName)
+ + '-p "%~dp0{}\\avkys" '.format(libDir)
+ + '-c "%~dp0share\\config"\n')
+
+ @staticmethod
+ def removeUnneededFiles(path):
+ afiles = set()
+
+ for root, _, files in os.walk(path):
+ for f in files:
+ if f.endswith('.a') \
+ or f.endswith('.static.prl') \
+ or f.endswith('.pdb') \
+ or f.endswith('.lib'):
+ afiles.add(os.path.join(root, f))
+
+ for afile in afiles:
+ os.remove(afile)
+
+ def searchPackageFor(self, path):
+ path = path.replace('C:/', '/c/')
+ os.environ['LC_ALL'] = 'C'
+ pacman = self.whereBin('pacman.exe')
+
+ if len(pacman) > 0:
+ process = subprocess.Popen([pacman, '-Qo', path], # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+
+ if process.returncode != 0:
+ prefix = '/c/msys32' if self.targetArch == '32bit' else '/c/msys64'
+ path = path[len(prefix):]
+ 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()])
+
+ 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():
+ try:
+ process = subprocess.Popen(['cmd', '/c', 'ver'], # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+ windowsVersion = stdout.decode(sys.getdefaultencoding()).strip()
+
+ return 'Windows Version: {}\n'.format(windowsVersion)
+ except:
+ pass
+
+ return ' '.join(platform.uname())
+
+ def writeBuildInfo(self):
+ shareDir = os.path.join(self.rootInstallDir, 'share')
+
+ try:
+ os.makedirs(shareDir)
+ except:
+ pass
+
+ depsInfoFile = os.path.join(shareDir, 'build-info.txt')
+
+ # 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 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 createPortable(self, mutex):
+ arch = 'win32' if self.targetArch == '32bit' else 'win64'
+ packagePath = \
+ os.path.join(self.pkgsDir,
+ '{}-portable-{}-{}.zip'.format(self.programName,
+ self.programVersion,
+ arch))
+
+ if not os.path.exists(self.pkgsDir):
+ os.makedirs(self.pkgsDir)
+
+ with zipfile.ZipFile(packagePath, 'w', zipfile.ZIP_DEFLATED, False) as zipFile:
+ for root, dirs, files in os.walk(self.rootInstallDir):
+ for f in dirs + files:
+ filePath = os.path.join(root, f)
+ dstPath = os.path.join(self.programName,
+ filePath.replace(self.rootInstallDir + os.sep, ''))
+ zipFile.write(filePath, dstPath)
+
+ mutex.acquire()
+ print('Created portable package:')
+ self.printPackageInfo(packagePath)
+ mutex.release()
+
+ def createAppInstaller(self, mutex):
+ packagePath = self.createInstaller()
+
+ if not packagePath:
+ return
+
+ mutex.acquire()
+ print('Created installable package:')
+ self.printPackageInfo(self.outPackage)
+ mutex.release()
+
+ def package(self):
+ mutex = threading.Lock()
+
+ threads = [threading.Thread(target=self.createPortable, args=(mutex,))]
+ packagingTools = ['zip']
+
+ if self.qtIFW != '':
+ threads.append(threading.Thread(target=self.createAppInstaller, args=(mutex,)))
+ packagingTools += ['Qt Installer Framework']
+
+ if len(packagingTools) > 0:
+ print('Detected packaging tools: {}\n'.format(', '.join(packagingTools)))
+
+ for thread in threads:
+ thread.start()
+
+ for thread in threads:
+ thread.join()
diff --git a/ports/deploy/exclude.nt.win32.txt b/ports/deploy/exclude.nt.win32.txt
new file mode 100644
index 0000000..490c564
--- /dev/null
+++ b/ports/deploy/exclude.nt.win32.txt
@@ -0,0 +1,2 @@
+C:/Windows/System32/.*
+C:/Program Files/.*
diff --git a/ports/deploy/exclude.posix.darwin.txt b/ports/deploy/exclude.posix.darwin.txt
new file mode 100644
index 0000000..f1cd9cb
--- /dev/null
+++ b/ports/deploy/exclude.posix.darwin.txt
@@ -0,0 +1,2 @@
+/usr/lib/.*
+/System/Library/Frameworks/.*
diff --git a/ports/deploy/exclude.posix.freebsd12.txt b/ports/deploy/exclude.posix.freebsd12.txt
new file mode 100644
index 0000000..13025e4
--- /dev/null
+++ b/ports/deploy/exclude.posix.freebsd12.txt
@@ -0,0 +1,110 @@
+# VDSO
+(.*/)*ld-linux.so.2
+(.*/)*ld-linux-x86-64.so.2
+
+# Glibc
+(.*/)*libc.so.[0-9]+
+(.*/)*libdl.so.[0-9]+
+(.*/)*libm.so.[0-9]+
+(.*/)*libmvec.so.[0-9]+
+(.*/)*libpthread.so.[0-9]+
+(.*/)*libresolv.so.[0-9]+
+(.*/)*librt.so.[0-9]+
+
+# GCC
+(.*/)*libgcc_s.so.[0-9]+
+(.*/)*libgomp.so.[0-9]+
+(.*/)*libstdc\+\+.so.[0-9]+
+
+# Core libraries
+(.*/)*libSM.so.6
+(.*/)*libblkid.so.1
+(.*/)*libcap.so.2
+(.*/)*libcom_err.so.2
+(.*/)*libcrypto.so.1.0.0
+(.*/)*libdb-5.3.so
+(.*/)*libdbus-1.so.3
+(.*/)*libexpat.so.1
+(.*/)*libfontconfig.so.1
+(.*/)*libfreetype.so.6
+(.*/)*libgcrypt.so.20
+(.*/)*libgmp.so.10
+(.*/)*libgpg-error.so.0
+(.*/)*libgssapi_krb5.so.2
+(.*/)*libharfbuzz.so.0
+(.*/)*libk5crypto.so.3
+(.*/)*libkeyutils.so.1
+(.*/)*libkrb5.so.3
+(.*/)*libkrb5support.so.0
+(.*/)*liblz4.so.1
+(.*/)*liblzma.so.5
+(.*/)*libmount.so.1
+(.*/)*libpcre.so.[0-9]+$
+(.*/)*libpcre16.so.0
+(.*/)*libssh2.so.1
+(.*/)*libssl.so.1.0.0
+(.*/)*libtasn1.so.6
+(.*/)*libusb-1.0.so.0
+(.*/)*libuuid.so.1
+(.*/)*libz.so.1
+
+# Glib2
+(.*/)*libgio-2.0.so.0
+(.*/)*libglib-2.0.so.0
+(.*/)*libgmodule-2.0.so.0
+(.*/)*libgobject-2.0.so.0
+
+# X11
+(.*/)*libX11-xcb.so.[0-9]+
+(.*/)*libX11.so.[0-9]+
+(.*/)*libXau.so.[0-9]+
+(.*/)*libXcursor.so.[0-9]+
+(.*/)*libXdamage.so.[0-9]+
+(.*/)*libXdmcp.so.[0-9]+
+(.*/)*libXext.so.[0-9]+
+(.*/)*libXfixes.so.[0-9]+
+(.*/)*libXi.so.[0-9]+
+(.*/)*libXinerama.so.[0-9]+
+(.*/)*libXrandr.so.[0-9]+
+(.*/)*libXrender.so.[0-9]+
+(.*/)*libXss.so.[0-9]+
+(.*/)*libXv.so.[0-9]+
+(.*/)*libXxf86vm.so.[0-9]+
+(.*/)*libglapi.so.[0-9]+
+(.*/)*libp11-kit.so.[0-9]+
+(.*/)*libxcb-dri2.so.[0-9]+
+(.*/)*libxcb-dri3.so.[0-9]+
+(.*/)*libxcb-glx.so.[0-9]+
+(.*/)*libxcb-icccm.so.[0-9]+
+(.*/)*libxcb-image.so.[0-9]+
+(.*/)*libxcb-keysyms.so.[0-9]+
+(.*/)*libxcb-present.so.[0-9]+
+(.*/)*libxcb-randr.so.[0-9]+
+(.*/)*libxcb-render-util.so.[0-9]+
+(.*/)*libxcb-render.so.[0-9]+
+(.*/)*libxcb-shape.so.[0-9]+
+(.*/)*libxcb-shm.so.[0-9]+
+(.*/)*libxcb-sync.so.[0-9]+
+(.*/)*libxcb-util.so.[0-9]+
+(.*/)*libxcb-xfixes.so.[0-9]+
+(.*/)*libxcb-xinerama.so.[0-9]+
+(.*/)*libxcb-xkb.so.[0-9]+
+(.*/)*libxcb.so.[0-9]+
+(.*/)*libxkbcommon-x11.so.[0-9]+
+(.*/)*libxkbcommon.so.[0-9]+
+(.*/)*libxshmfence.so.[0-9]+
+
+# OpenGL
+(.*/)*libdrm.so.2
+(.*/)*libgbm.so.1
+(.*/)*libEGL.so.1
+(.*/)*libGL.so.1
+(.*/)*libGLX.so.0
+(.*/)*libGLdispatch.so.0
+
+# Use system library instead
+(.*/)*libasound.so.2
+(.*/)*libpulse.so.0
+(.*/)*libpulse-simple.so.0
+(.*/)*libjack.so.0
+(.*/)*libv4l2.so.0
diff --git a/ports/deploy/exclude.posix.linux.txt b/ports/deploy/exclude.posix.linux.txt
new file mode 100644
index 0000000..13025e4
--- /dev/null
+++ b/ports/deploy/exclude.posix.linux.txt
@@ -0,0 +1,110 @@
+# VDSO
+(.*/)*ld-linux.so.2
+(.*/)*ld-linux-x86-64.so.2
+
+# Glibc
+(.*/)*libc.so.[0-9]+
+(.*/)*libdl.so.[0-9]+
+(.*/)*libm.so.[0-9]+
+(.*/)*libmvec.so.[0-9]+
+(.*/)*libpthread.so.[0-9]+
+(.*/)*libresolv.so.[0-9]+
+(.*/)*librt.so.[0-9]+
+
+# GCC
+(.*/)*libgcc_s.so.[0-9]+
+(.*/)*libgomp.so.[0-9]+
+(.*/)*libstdc\+\+.so.[0-9]+
+
+# Core libraries
+(.*/)*libSM.so.6
+(.*/)*libblkid.so.1
+(.*/)*libcap.so.2
+(.*/)*libcom_err.so.2
+(.*/)*libcrypto.so.1.0.0
+(.*/)*libdb-5.3.so
+(.*/)*libdbus-1.so.3
+(.*/)*libexpat.so.1
+(.*/)*libfontconfig.so.1
+(.*/)*libfreetype.so.6
+(.*/)*libgcrypt.so.20
+(.*/)*libgmp.so.10
+(.*/)*libgpg-error.so.0
+(.*/)*libgssapi_krb5.so.2
+(.*/)*libharfbuzz.so.0
+(.*/)*libk5crypto.so.3
+(.*/)*libkeyutils.so.1
+(.*/)*libkrb5.so.3
+(.*/)*libkrb5support.so.0
+(.*/)*liblz4.so.1
+(.*/)*liblzma.so.5
+(.*/)*libmount.so.1
+(.*/)*libpcre.so.[0-9]+$
+(.*/)*libpcre16.so.0
+(.*/)*libssh2.so.1
+(.*/)*libssl.so.1.0.0
+(.*/)*libtasn1.so.6
+(.*/)*libusb-1.0.so.0
+(.*/)*libuuid.so.1
+(.*/)*libz.so.1
+
+# Glib2
+(.*/)*libgio-2.0.so.0
+(.*/)*libglib-2.0.so.0
+(.*/)*libgmodule-2.0.so.0
+(.*/)*libgobject-2.0.so.0
+
+# X11
+(.*/)*libX11-xcb.so.[0-9]+
+(.*/)*libX11.so.[0-9]+
+(.*/)*libXau.so.[0-9]+
+(.*/)*libXcursor.so.[0-9]+
+(.*/)*libXdamage.so.[0-9]+
+(.*/)*libXdmcp.so.[0-9]+
+(.*/)*libXext.so.[0-9]+
+(.*/)*libXfixes.so.[0-9]+
+(.*/)*libXi.so.[0-9]+
+(.*/)*libXinerama.so.[0-9]+
+(.*/)*libXrandr.so.[0-9]+
+(.*/)*libXrender.so.[0-9]+
+(.*/)*libXss.so.[0-9]+
+(.*/)*libXv.so.[0-9]+
+(.*/)*libXxf86vm.so.[0-9]+
+(.*/)*libglapi.so.[0-9]+
+(.*/)*libp11-kit.so.[0-9]+
+(.*/)*libxcb-dri2.so.[0-9]+
+(.*/)*libxcb-dri3.so.[0-9]+
+(.*/)*libxcb-glx.so.[0-9]+
+(.*/)*libxcb-icccm.so.[0-9]+
+(.*/)*libxcb-image.so.[0-9]+
+(.*/)*libxcb-keysyms.so.[0-9]+
+(.*/)*libxcb-present.so.[0-9]+
+(.*/)*libxcb-randr.so.[0-9]+
+(.*/)*libxcb-render-util.so.[0-9]+
+(.*/)*libxcb-render.so.[0-9]+
+(.*/)*libxcb-shape.so.[0-9]+
+(.*/)*libxcb-shm.so.[0-9]+
+(.*/)*libxcb-sync.so.[0-9]+
+(.*/)*libxcb-util.so.[0-9]+
+(.*/)*libxcb-xfixes.so.[0-9]+
+(.*/)*libxcb-xinerama.so.[0-9]+
+(.*/)*libxcb-xkb.so.[0-9]+
+(.*/)*libxcb.so.[0-9]+
+(.*/)*libxkbcommon-x11.so.[0-9]+
+(.*/)*libxkbcommon.so.[0-9]+
+(.*/)*libxshmfence.so.[0-9]+
+
+# OpenGL
+(.*/)*libdrm.so.2
+(.*/)*libgbm.so.1
+(.*/)*libEGL.so.1
+(.*/)*libGL.so.1
+(.*/)*libGLX.so.0
+(.*/)*libGLdispatch.so.0
+
+# Use system library instead
+(.*/)*libasound.so.2
+(.*/)*libpulse.so.0
+(.*/)*libpulse-simple.so.0
+(.*/)*libjack.so.0
+(.*/)*libv4l2.so.0
diff --git a/ports/deploy/installscript.mac.qs b/ports/deploy/installscript.mac.qs
new file mode 100644
index 0000000..aaa90a2
--- /dev/null
+++ b/ports/deploy/installscript.mac.qs
@@ -0,0 +1,13 @@
+function Component()
+{
+}
+
+Component.prototype.beginInstallation = function()
+{
+ component.beginInstallation();
+}
+
+Component.prototype.createOperations = function()
+{
+ component.createOperations();
+}
diff --git a/ports/deploy/installscript.posix.qs b/ports/deploy/installscript.posix.qs
new file mode 100644
index 0000000..ccd4832
--- /dev/null
+++ b/ports/deploy/installscript.posix.qs
@@ -0,0 +1,53 @@
+function Component()
+{
+}
+
+Component.prototype.beginInstallation = function()
+{
+ component.beginInstallation();
+}
+
+Component.prototype.createOperations = function()
+{
+ component.createOperations();
+
+ component.addOperation("InstallIcons", "@TargetDir@/share/icons" );
+ component.addOperation("CreateDesktopEntry",
+ "Webcamoid.desktop",
+ "Name=Webcamoid\n"
+ + "GenericName=Webcam Capture Software\n"
+ + "GenericName[ca]=Programari de Captura de Càmera web\n"
+ + "GenericName[de]=Webcam-Capture-Software\n"
+ + "GenericName[el]=κάμερα συλλαμβάνει το λογισμικό\n"
+ + "GenericName[es]=Programa para Captura de la Webcam\n"
+ + "GenericName[fr]=Logiciel de Capture Webcam\n"
+ + "GenericName[gl]=Programa de Captura de Webcam\n"
+ + "GenericName[it]=Webcam Capture Software\n"
+ + "GenericName[ja]=ウェブカメラのキャプチャソフトウェア\n"
+ + "GenericName[ko]=웹캠 캡처 소프트웨어\n"
+ + "GenericName[pt]=Software de Captura de Webcam\n"
+ + "GenericName[ru]=Веб-камера захвата программного обеспечения\n"
+ + "GenericName[zh_CN]=摄像头捕捉软件\n"
+ + "GenericName[zh_TW]=攝像頭捕捉軟件\n"
+ + "Comment=Take photos and record videos with your webcam\n"
+ + "Comment[ca]=Fer fotos i gravar vídeos amb la seva webcam\n"
+ + "Comment[de]=Maak foto's en video's opnemen met uw webcam\n"
+ + "Comment[el]=Τραβήξτε φωτογραφίες και εγγραφή βίντεο με την κάμερα σας\n"
+ + "Comment[es]=Tome fotos y grabe videos con su camara web\n"
+ + "Comment[fr]=Prenez des photos et enregistrer des vidéos avec votre webcam\n"
+ + "Comment[gl]=Facer fotos e gravar vídeos coa súa cámara web\n"
+ + "Comment[it]=Scatta foto e registrare video con la tua webcam\n"
+ + "Comment[ja]=ウェブカメラで写真や記録ビデオを撮影\n"
+ + "Comment[ko]=웹캠으로 사진과 기록 비디오를 촬영\n"
+ + "Comment[pt]=Tirar fotos e gravar vídeos com sua webcam\n"
+ + "Comment[ru]=Возьмите фотографии и записывать видео с веб-камеры\n"
+ + "Comment[zh_CN]=拍摄照片和录制视频与您的摄像头\n"
+ + "Comment[zh_TW]=拍攝照片和錄製視頻與您的攝像頭\n"
+ + "Keywords=photo;video;webcam;\n"
+ + "Exec=" + installer.value("RunProgram") + "\n"
+ + "Icon=webcamoid\n"
+ + "Terminal=false\n"
+ + "Type=Application\n"
+ + "Categories=AudioVideo;Player;Qt;\n"
+ + "StartupNotify=true\n");
+}
diff --git a/ports/deploy/installscript.windows.qs b/ports/deploy/installscript.windows.qs
new file mode 100644
index 0000000..72ea769
--- /dev/null
+++ b/ports/deploy/installscript.windows.qs
@@ -0,0 +1,21 @@
+function Component()
+{
+}
+
+Component.prototype.beginInstallation = function()
+{
+ component.beginInstallation();
+}
+
+Component.prototype.createOperations = function()
+{
+ component.createOperations();
+
+ // Create shortcuts.
+ var installDir = ["@TargetDir@", "@StartMenuDir@", "@DesktopDir@"];
+
+ for (var dir in installDir)
+ component.addOperation("CreateShortcut",
+ "@TargetDir@/bin/webcamoid.exe",
+ installDir[dir] + "/webcamoid.lnk");
+}
diff --git a/ports/deploy/package_info.conf b/ports/deploy/package_info.conf
new file mode 100644
index 0000000..ce4a866
--- /dev/null
+++ b/ports/deploy/package_info.conf
@@ -0,0 +1,7 @@
+[Package]
+appName = Webcamoid
+description = Webcamoid, The ultimate webcam suite!
+url = https://webcamoid.github.io/
+titleColor = #3F1F7F
+runMessage = Launch Webcamoid now!
+licenseDescription = GNU General Public License v3.0
diff --git a/ports/deploy/tools/android.py b/ports/deploy/tools/android.py
new file mode 100644
index 0000000..f4c4eba
--- /dev/null
+++ b/ports/deploy/tools/android.py
@@ -0,0 +1,199 @@
+#!/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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import os
+import shutil
+import xml.etree.ElementTree as ET
+
+import tools.utils
+
+
+class AndroidTools(tools.utils.DeployToolsUtils):
+ def __init__(self):
+ super().__init__()
+ self.androidPlatform = ''
+ self.archMap = [('arm64-v8a' , 'aarch64', 'aarch64-linux-android'),
+ ('armeabi-v7a', 'arm' , 'arm-linux-androideabi'),
+ ('x86' , 'i686' , 'i686-linux-android' ),
+ ('x86_64' , 'x86_64' , 'x86_64-linux-android' )]
+ self.androidSDK = ''
+ self.androidNDK = ''
+ self.bundledInLib = []
+ self.qtLibs = []
+ self.localLibs = []
+
+ if 'ANDROID_HOME' in os.environ:
+ self.androidSDK = os.environ['ANDROID_HOME']
+
+ if 'ANDROID_NDK_ROOT' in os.environ:
+ self.androidNDK = os.environ['ANDROID_NDK_ROOT']
+ elif 'ANDROID_NDK' in os.environ:
+ self.androidNDK = os.environ['ANDROID_NDK']
+
+ def detectAndroidPlatform(self, path=''):
+ for makeFile in self.detectMakeFiles(path):
+ with open(makeFile) as f:
+ for line in f:
+ if line.startswith('DESTDIR') and '=' in line:
+ buildPath = os.path.join(path, line.split('=')[1].strip())
+ chunks = [part for part in buildPath.split(os.path.sep) if len(part) > 0]
+
+ if len(chunks) >= 2:
+ self.androidPlatform = chunks[len(chunks) - 2]
+
+ return
+
+ def detectLibPaths(self):
+ if len(self.androidNDK) < 1:
+ return []
+
+ for arch in self.archMap:
+ if self.targetArch == arch[0]:
+ return [os.path.join(self.androidNDK,
+ 'toolchains',
+ 'llvm',
+ 'prebuilt',
+ 'linux-x86_64',
+ 'sysroot',
+ 'usr',
+ 'lib',
+ arch[1] + '-linux-android')]
+
+ return []
+
+ def detectBinPaths(self):
+ if len(self.androidNDK) < 1:
+ return []
+
+ for arch in self.archMap:
+ if self.targetArch == arch[0]:
+ binPath = os.path.join(self.androidNDK,
+ 'toolchains',
+ 'llvm',
+ 'prebuilt',
+ 'linux-x86_64',
+ arch[2],
+ 'bin')
+
+ return [binPath]
+
+ return []
+
+ def fixQtLibs(self):
+ for root, dirs, files in os.walk(self.assetsIntallDir):
+ for f in files:
+ if f.endswith('.so'):
+ srcPath = os.path.join(root, f)
+ relPath = root.replace(self.assetsIntallDir, '')[1:]
+ prefix = 'lib' + relPath.replace(os.path.sep, '_') + '_'
+ lib = ''
+
+ if f.startswith(prefix):
+ lib = f
+ else:
+ lib = prefix + f
+
+ dstPath = os.path.join(self.libInstallDir, lib)
+ print(' {} -> {}'.format(srcPath, dstPath))
+ self.move(srcPath, dstPath)
+ self.bundledInLib += [(lib, os.path.join(relPath, f))]
+
+ def libBaseName(self, lib):
+ basename = os.path.basename(lib)
+
+ return basename[3: len(basename) - 3]
+
+ def fixLibsXml(self):
+ bundledInAssets = []
+ assetsDir = os.path.join(self.rootInstallDir, 'assets')
+
+ for root, dirs, files in os.walk(assetsDir):
+ for f in files:
+ srcPath = os.path.join(root.replace(assetsDir, '')[1:], f)
+ dstPath = os.path.sep.join(srcPath.split(os.path.sep)[1:])
+
+ if (len(dstPath) > 0):
+ bundledInAssets += [(srcPath, dstPath)]
+
+ libsXml = os.path.join(self.rootInstallDir, 'res', 'values', 'libs.xml')
+ libsXmlTemp = os.path.join(self.rootInstallDir, 'res', 'values', 'libsTemp.xml')
+
+ tree = ET.parse(libsXml)
+ root = tree.getroot()
+ oldFeatures = set()
+ oldPermissions = set()
+ resources = {}
+
+ for array in root:
+ if not array.attrib['name'] in resources:
+ resources[array.attrib['name']] = set()
+
+ for item in array:
+ if item.text:
+ lib = item.text.strip()
+
+ if len(lib) > 0:
+ lib = '- {}
'.format(lib)
+ resources[array.attrib['name']].add(lib)
+
+ qtLibs = set(['- {};{}
'.format(self.targetArch, self.libBaseName(lib)) for lib in self.qtLibs])
+
+ if 'qt_libs' in resources:
+ qtLibs -= resources['qt_libs']
+
+ qtLibs = '\n'.join(sorted(list(qtLibs)))
+ bundledInLib = set(['- {}:{}
'.format(lib[0], lib[1]) for lib in self.bundledInLib])
+
+ if 'bundled_in_lib' in resources:
+ bundledInLib -= resources['bundled_in_lib']
+
+ bundledInLib = '\n'.join(sorted(list(bundledInLib)))
+ bundledInAssets = set(['- {}:{}
'.format(lib[0], lib[1]) for lib in bundledInAssets])
+
+ if 'bundled_in_assets' in resources:
+ bundledInAssets -= resources['bundled_in_assets']
+
+ bundledInAssets = '\n'.join(sorted(list(bundledInAssets)))
+
+ localLibs = sorted(list(set(self.localLibs)))
+ localLibs = set(['- {};{}
'.format(self.targetArch, ':'.join(localLibs)) for lib in localLibs])
+
+ if 'load_local_libs' in resources:
+ localLibs -= resources['load_local_libs']
+
+ localLibs = '\n'.join(sorted(list(localLibs)))
+
+ replace = {'' : '',
+ '' : qtLibs,
+ '' : bundledInLib,
+ '': bundledInAssets,
+ '' : localLibs}
+
+ with open(libsXml) as inFile:
+ with open(libsXmlTemp, 'w') as outFile:
+ for line in inFile:
+ for key in replace:
+ line = line.replace(key, replace[key])
+
+ outFile.write(line)
+
+ os.remove(libsXml)
+ shutil.move(libsXmlTemp, libsXml)
diff --git a/ports/deploy/tools/binary.py b/ports/deploy/tools/binary.py
new file mode 100644
index 0000000..f228c3a
--- /dev/null
+++ b/ports/deploy/tools/binary.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import os
+import re
+import subprocess # nosec
+import threading
+import time
+
+import tools
+
+
+class DeployToolsBinary(tools.utils.DeployToolsUtils):
+ def __init__(self):
+ super().__init__()
+ self.detectStrip()
+ self.excludes = []
+
+ def isValid(self, path):
+ return False
+
+ def find(self, path):
+ binaries = []
+
+ for root, _, files in os.walk(path):
+ for f in files:
+ binaryPath = os.path.join(root, f)
+
+ if not os.path.islink(binaryPath) and self.isValid(binaryPath):
+ binaries.append(binaryPath)
+
+ return binaries
+
+ def dump(self, binary):
+ return {}
+
+ def dependencies(self, binary):
+ return []
+
+ def allDependencies(self, binary):
+ deps = self.dependencies(binary)
+ solved = set()
+
+ while len(deps) > 0:
+ dep = deps.pop()
+
+ for binDep in self.dependencies(dep):
+ if binDep != dep and not binDep in solved:
+ deps.append(binDep)
+
+ if self.system == 'mac':
+ i = dep.rfind('.framework/')
+
+ if i >= 0:
+ dep = dep[: i] + '.framework'
+
+ solved.add(dep)
+
+ return solved
+
+ def scanDependencies(self, path):
+ deps = set()
+
+ for binPath in self.find(path):
+ for dep in self.allDependencies(binPath):
+ deps.add(dep)
+
+ return sorted(deps)
+
+ def name(self, binary):
+ return ''
+
+ def detectStrip(self):
+ self.stripBin = self.whereBin('strip.exe' if self.system == 'windows' else 'strip')
+
+ def strip(self, binary):
+ if self.stripBin == '':
+ return
+
+ process = subprocess.Popen([self.stripBin, binary], # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ process.communicate()
+
+ def stripSymbols(self, path):
+ threads = []
+
+ for binary in self.find(path):
+ thread = threading.Thread(target=self.strip, args=(binary,))
+ threads.append(thread)
+
+ while threading.active_count() >= self.njobs:
+ time.sleep(0.25)
+
+ thread.start()
+
+ for thread in threads:
+ thread.join()
+
+ def readExcludeList(self, excludeList):
+ self.excludes = []
+
+ if os.path.exists(excludeList):
+ with open(excludeList) as f:
+ for line in f:
+ line = line.strip()
+
+ if len(line) > 0 and line[0] != '#':
+ i = line.find('#')
+
+ if i >= 0:
+ line = line[: i]
+
+ line = line.strip()
+
+ if len(line) > 0:
+ self.excludes.append(line)
+
+ def isExcluded(self, path):
+ for exclude in self.excludes:
+ if self.targetSystem == 'windows' or self.targetSystem == 'posix_windows':
+ path = path.lower().replace('\\', '/')
+ exclude = exclude.lower()
+
+ if re.fullmatch(exclude, path):
+ return True
+
+ return False
+
+ def resetFilePermissions(self, rootPath, binariesPath):
+ for root, dirs, files in os.walk(rootPath):
+ for d in dirs:
+ permissions = 0o755
+ path = os.path.join(root, d)
+
+ if self.system == 'mac':
+ os.chmod(path, permissions, follow_symlinks=False)
+ else:
+ os.chmod(path, permissions)
+
+ for f in files:
+ permissions = 0o644
+ path = os.path.join(root, f)
+
+ if root == binariesPath and self.isValid(path):
+ permissions = 0o744
+
+ if self.system == 'mac':
+ os.chmod(path, permissions, follow_symlinks=False)
+ else:
+ os.chmod(path, permissions)
diff --git a/ports/deploy/tools/binary_elf.py b/ports/deploy/tools/binary_elf.py
new file mode 100644
index 0000000..da119d8
--- /dev/null
+++ b/ports/deploy/tools/binary_elf.py
@@ -0,0 +1,345 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import fnmatch
+import os
+import re
+import struct
+import sys
+
+import tools.binary
+
+
+class DeployToolsBinary(tools.binary.DeployToolsBinary):
+ def __init__(self):
+ super().__init__()
+ self.ldLibraryPath = os.environ['LD_LIBRARY_PATH'].split(':') if 'LD_LIBRARY_PATH' in os.environ else []
+ self.libsSeachPaths = self.readLdconf() \
+ + ['/usr/lib',
+ '/usr/lib64',
+ '/lib',
+ '/lib64',
+ '/usr/local/lib',
+ '/usr/local/lib64']
+ self.emCodes = {3 : '386',
+ 40 : 'ARM',
+ 62 : 'X86_64',
+ 183: 'AARCH64'}
+
+ def readLdconf(self, ldconf='/etc/ld.so.conf'):
+ if not os.path.exists(ldconf):
+ return []
+
+ confDir = os.path.dirname(ldconf)
+ libpaths = []
+
+ with open(ldconf) as f:
+ for line in f:
+ i = line.find('#')
+
+ if i == 0:
+ continue
+
+ if i >= 0:
+ line = line[: i]
+
+ line = line.strip()
+
+ if len(line) < 1:
+ continue
+
+ if line.startswith('include'):
+ conf = line.split()[1]
+
+ if not conf.startswith('/'):
+ conf = os.path.join(confDir, conf)
+
+ dirname = os.path.dirname(conf)
+
+ if os.path.exists(dirname):
+ for f in os.listdir(dirname):
+ path = os.path.join(dirname, f)
+
+ if fnmatch.fnmatch(path, conf):
+ libpaths += self.readLdconf(path)
+ else:
+ libpaths.append(line)
+
+ return libpaths
+
+ def isValid(self, path):
+ with open(path, 'rb') as f:
+ return f.read(4) == b'\x7fELF'
+
+ @staticmethod
+ def readString(f):
+ s = b''
+
+ while True:
+ c = f.read(1)
+
+ if c == b'\x00':
+ break
+
+ s += c
+
+ return s
+
+ @staticmethod
+ def readNumber(f, arch):
+ if arch == '32bits':
+ return struct.unpack('I', f.read(4))[0]
+
+ return struct.unpack('Q', f.read(8))[0]
+
+ @staticmethod
+ def readDynamicEntry(f, arch):
+ if arch == '32bits':
+ return struct.unpack('iI', f.read(8))
+
+ return struct.unpack('qQ', f.read(16))
+
+ # https://refspecs.linuxfoundation.org/lsb.shtml (See Core, Generic)
+ # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
+ def dump(self, binary):
+ # ELF file magic
+ ELFMAGIC = b'\x7fELF'
+
+ # Sections
+ SHT_STRTAB = 0x3
+ SHT_DYNAMIC = 0x6
+
+ # Dynamic section entries
+ DT_NULL = 0
+ DT_NEEDED = 1
+ DT_RPATH = 15
+ DT_RUNPATH = 0x1d
+
+ with open(binary, 'rb') as f:
+ # Read magic signature.
+ magic = f.read(4)
+
+ if magic != ELFMAGIC:
+ return {}
+
+ # Read the data structure of the file.
+ eiClass = '32bits' if struct.unpack('B', f.read(1))[0] == 1 else '64bits'
+
+ # Read machine code.
+ f.seek(0x12, os.SEEK_SET)
+ machine = struct.unpack('H', f.read(2))[0]
+
+ # Get a pointer to the sections table.
+ sectionHeaderTable = 0
+
+ if eiClass == '32bits':
+ f.seek(0x20, os.SEEK_SET)
+ sectionHeaderTable = self.readNumber(f, eiClass)
+ f.seek(0x30, os.SEEK_SET)
+ else:
+ f.seek(0x28, os.SEEK_SET)
+ sectionHeaderTable = self.readNumber(f, eiClass)
+ f.seek(0x3c, os.SEEK_SET)
+
+ # Read the number of sections.
+ nSections = struct.unpack('H', f.read(2))[0]
+
+ # Read the index of the string table that stores sections names.
+ shstrtabIndex = struct.unpack('H', f.read(2))[0]
+
+ # Read sections.
+ f.seek(sectionHeaderTable, os.SEEK_SET)
+ neededPtr = []
+ rpathsPtr = []
+ runpathsPtr = []
+ strtabs = []
+ shstrtab = []
+
+ for section in range(nSections):
+ sectionStart = f.tell()
+
+ # Read the a pointer to the virtual address in the string table
+ # that contains the name of this section.
+ sectionName = struct.unpack('I', f.read(4))[0]
+
+ # Read the type of this section.
+ sectionType = struct.unpack('I', f.read(4))[0]
+
+ # Read the virtual address of this section.
+ f.seek(sectionStart + (0x0c if eiClass == '32bits' else 0x10), os.SEEK_SET)
+ shAddr = self.readNumber(f, eiClass)
+
+ # Read the offset in file to this section.
+ shOffset = self.readNumber(f, eiClass)
+ f.seek(shOffset, os.SEEK_SET)
+
+ if sectionType == SHT_DYNAMIC:
+ # Read dynamic sections.
+ while True:
+ # Read dynamic entries.
+ dTag, dVal = self.readDynamicEntry(f, eiClass)
+
+ if dTag == DT_NULL:
+ # End of dynamic sections.
+ break
+ elif dTag == DT_NEEDED:
+ # Dynamically imported libraries.
+ neededPtr.append(dVal)
+ elif dTag == DT_RPATH:
+ # RPATHs.
+ rpathsPtr.append(dVal)
+ elif dTag == DT_RUNPATH:
+ # RUNPATHs.
+ runpathsPtr.append(dVal)
+ elif sectionType == SHT_STRTAB:
+ # Read string tables.
+ if section == shstrtabIndex:
+ # We found the string table that stores sections names.
+ shstrtab = [shAddr, shOffset]
+ else:
+ # Save string tables for later usage.
+ strtabs += [[sectionName, shAddr, shOffset]]
+
+ # Move to next section.
+ f.seek(sectionStart + (0x28 if eiClass == '32bits' else 0x40), os.SEEK_SET)
+
+ # Libraries names and RUNPATHs are located in '.dynstr' table.
+ strtab = []
+
+ for tab in strtabs:
+ f.seek(tab[0] - shstrtab[0] + shstrtab[1], os.SEEK_SET)
+
+ if self.readString(f) == b'.dynstr':
+ strtab = tab
+
+ # Read dynamically imported libraries.
+ needed = set()
+
+ for lib in neededPtr:
+ f.seek(lib + strtab[2], os.SEEK_SET)
+ needed.add(self.readString(f).decode(sys.getdefaultencoding()))
+
+ # Read RPATHs
+ rpaths = set()
+
+ for path in rpathsPtr:
+ f.seek(path + strtab[2], os.SEEK_SET)
+ rpaths.add(self.readString(f).decode(sys.getdefaultencoding()))
+
+ # Read RUNPATHs
+ runpaths = set()
+
+ for path in runpathsPtr:
+ f.seek(path + strtab[2], os.SEEK_SET)
+ runpaths.add(self.readString(f).decode(sys.getdefaultencoding()))
+
+ return {'machine': machine,
+ 'imports': needed,
+ 'rpath': rpaths,
+ 'runpath': runpaths}
+
+ return {}
+
+ @staticmethod
+ def readRpaths(elfInfo, binDir):
+ rpaths = []
+ runpaths = []
+
+ # http://amir.rachum.com/blog/2016/09/17/shared-libraries/
+ for rpath in ['rpath', 'runpath']:
+ for path in elfInfo[rpath]:
+ if '$ORIGIN' in path:
+ path = path.replace('$ORIGIN', binDir)
+
+ if not path.startswith('/'):
+ path = os.path.join(binDir, path)
+
+ path = os.path.normpath(path)
+
+ if rpath == 'rpath':
+ rpaths.append(path)
+ else:
+ runpaths.append(path)
+
+ return rpaths, runpaths
+
+ def libPath(self, lib, machine, rpaths, runpaths):
+ # man ld.so
+ searchPaths = rpaths \
+ + self.ldLibraryPath \
+ + runpaths \
+ + self.libsSeachPaths
+
+ for libdir in searchPaths:
+ path = os.path.join(libdir, lib)
+
+ if os.path.exists(path):
+ depElfInfo = self.dump(path)
+
+ if depElfInfo:
+ if 'machine' in depElfInfo and (machine == 0 or depElfInfo['machine'] == machine):
+ return path
+ elif 'links' in depElfInfo and len(depElfInfo['links']) > 0:
+ return path
+
+ return ''
+
+ def dependencies(self, binary):
+ elfInfo = self.dump(binary)
+
+ if not elfInfo:
+ return []
+
+ rpaths, runpaths = self.readRpaths(elfInfo, os.path.dirname(binary))
+ libs = []
+ deps = []
+
+ if 'imports' in elfInfo:
+ deps = elfInfo['imports']
+ elif 'links' in elfInfo:
+ deps = elfInfo['links']
+
+ machine = 0
+
+ if 'machine' in elfInfo:
+ machine = elfInfo['machine']
+
+ for lib in deps:
+ libpath = self.libPath(lib, machine, rpaths, runpaths)
+
+ if len(libpath) > 0 and not self.isExcluded(libpath):
+ libs.append(libpath)
+
+ return libs
+
+ def name(self, binary):
+ dep = os.path.basename(binary)[3:]
+
+ return dep[: dep.find('.')]
+
+ def machineEMCode(self, binary):
+ info = self.dump(binary)
+
+ if 'machine' in info:
+ if info['machine'] in self.emCodes:
+ return self.emCodes[info['machine']]
+
+ return 'UNKNOWN'
diff --git a/ports/deploy/tools/binary_mach.py b/ports/deploy/tools/binary_mach.py
new file mode 100644
index 0000000..67c53a8
--- /dev/null
+++ b/ports/deploy/tools/binary_mach.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import os
+import struct
+import sys
+
+import tools.binary
+
+
+class DeployToolsBinary(tools.binary.DeployToolsBinary):
+ def __init__(self):
+ super().__init__()
+
+ # 32 bits magic number.
+ self.MH_MAGIC = 0xfeedface # Native endian
+ self.MH_CIGAM = 0xcefaedfe # Reverse endian
+
+ # 64 bits magic number.
+ self.MH_MAGIC_64 = 0xfeedfacf # Native endian
+ self.MH_CIGAM_64 = 0xcffaedfe # Reverse endian
+
+ def isValid(self, path):
+ try:
+ with open(path, 'rb') as f:
+ # Read magic number.
+ magic = struct.unpack('I', f.read(4))[0]
+
+ if magic == self.MH_MAGIC \
+ or magic == self.MH_CIGAM \
+ or magic == self.MH_MAGIC_64 \
+ or magic == self.MH_CIGAM_64:
+ return True
+ except:
+ pass
+
+ return False
+
+ # https://github.com/aidansteele/osx-abi-macho-file-format-reference
+ def dump(self, binary):
+ # Commands definitions
+ LC_REQ_DYLD = 0x80000000
+ LC_LOAD_DYLIB = 0xc
+ LC_RPATH = 0x1c | LC_REQ_DYLD
+ LC_ID_DYLIB = 0xd
+
+ dylibImports = []
+ rpaths = []
+ dylibId = ''
+
+ with open(binary, 'rb') as f:
+ # Read magic number.
+ magic = struct.unpack('I', f.read(4))[0]
+
+ if magic == self.MH_MAGIC or magic == self.MH_CIGAM:
+ is32bits = True
+ elif magic == self.MH_MAGIC_64 or magic == self.MH_CIGAM_64:
+ is32bits = False
+ else:
+ return {}
+
+ # Read number of commands.
+ f.seek(12, os.SEEK_CUR)
+ ncmds = struct.unpack('I', f.read(4))[0]
+
+ # Move to load commands
+ f.seek(8 if is32bits else 12, os.SEEK_CUR)
+
+ for _ in range(ncmds):
+ # Read a load command and store it's position in the file.
+ loadCommandStart = f.tell()
+ loadCommand = struct.unpack('II', f.read(8))
+
+ # If the command list a library
+ if loadCommand[0] in [LC_LOAD_DYLIB, LC_RPATH, LC_ID_DYLIB]:
+ # Save the position of the next command.
+ nextCommand = f.tell() + loadCommand[1] - 8
+
+ # Move to the string
+ f.seek(loadCommandStart + struct.unpack('I', f.read(4))[0], os.SEEK_SET)
+ dylib = b''
+
+ # Read string until null character.
+ while True:
+ c = f.read(1)
+
+ if c == b'\x00':
+ break
+
+ dylib += c
+ s = dylib.decode(sys.getdefaultencoding())
+
+ if loadCommand[0] == LC_LOAD_DYLIB:
+ dylibImports.append(s)
+ elif loadCommand[0] == LC_RPATH:
+ rpaths.append(s)
+ elif loadCommand[0] == LC_ID_DYLIB:
+ dylibId = s
+
+ f.seek(nextCommand, os.SEEK_SET)
+ else:
+ f.seek(loadCommand[1] - 8, os.SEEK_CUR)
+
+ return {'imports': dylibImports, 'rpaths': rpaths, 'id': dylibId}
+
+ @staticmethod
+ def solveRefpath(path):
+ if not path.startswith('@'):
+ return path
+
+ searchPaths = []
+
+ if 'DYLD_LIBRARY_PATH' in os.environ:
+ searchPaths += os.environ['DYLD_LIBRARY_PATH'].split(':')
+
+ if 'DYLD_FRAMEWORK_PATH' in os.environ:
+ searchPaths += os.environ['DYLD_FRAMEWORK_PATH'].split(':')
+
+ if path.endswith('.dylib'):
+ dep = os.path.basename(path)
+ else:
+ i = path.rfind(os.sep, 0, path.rfind('.framework'))
+ dep = path[i + 1:]
+
+ for fpath in searchPaths:
+ realPath = os.path.join(fpath, dep)
+
+ if os.path.exists(realPath):
+ return realPath
+
+ return ''
+
+ def dependencies(self, binary):
+ machInfo = self.dump(binary)
+
+ if not machInfo:
+ return []
+
+ libs = []
+
+ for mach in machInfo['imports']:
+ mach = self.solveRefpath(mach)
+
+ if mach == '' or self.isExcluded(mach) or not os.path.exists(mach):
+ continue
+
+ dirName = os.path.dirname(mach)
+ dirName = os.path.realpath(dirName)
+ baseName = os.path.basename(mach)
+ libs.append(os.path.join(dirName, baseName))
+
+ return libs
+
+ def name(self, binary):
+ dep = os.path.basename(binary)
+ i = dep.find('.')
+
+ if i >= 0:
+ dep = dep[: dep.find('.')]
+
+ if 'Qt' in dep and not 'Qt5' in dep:
+ dep = dep.replace('Qt', 'Qt5')
+
+ return dep
diff --git a/ports/deploy/tools/binary_pecoff.py b/ports/deploy/tools/binary_pecoff.py
new file mode 100644
index 0000000..15d1e15
--- /dev/null
+++ b/ports/deploy/tools/binary_pecoff.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import mimetypes
+import os
+import struct
+import sys
+
+import tools.binary
+
+
+class DeployToolsBinary(tools.binary.DeployToolsBinary):
+ def __init__(self):
+ super().__init__()
+
+ def isValid(self, path):
+ mimetype, _ = mimetypes.guess_type(path)
+
+ if mimetype == 'application/x-msdownload':
+ return True
+
+ if mimetype != 'application/octet-stream':
+ return False
+
+ with open(path, 'rb') as f:
+ if f.read(2) != b'MZ':
+ return []
+
+ f.seek(0x3c, os.SEEK_SET)
+ peHeaderOffset = struct.unpack('I', f.read(4))
+ f.seek(peHeaderOffset[0], os.SEEK_SET)
+ peSignatue = f.read(4)
+
+ if peSignatue != b'PE\x00\x00':
+ return False
+
+ return True
+
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680547(v=vs.85).aspx
+ # https://upload.wikimedia.org/wikipedia/commons/1/1b/Portable_Executable_32_bit_Structure_in_SVG_fixed.svg
+ def dump(self, binary):
+ dllImports = set()
+
+ if not os.path.exists(binary) or not os.path.isfile(binary):
+ return dllImports
+
+ with open(binary, 'rb') as f:
+ # Check DOS header signature.
+ if f.read(2) != b'MZ':
+ return []
+
+ # Move to COFF header.
+ f.seek(0x3c, os.SEEK_SET)
+ peHeaderOffset = struct.unpack('I', f.read(4))
+ f.seek(peHeaderOffset[0], os.SEEK_SET)
+ peSignatue = f.read(4)
+
+ # Check COFF header signature.
+ if peSignatue != b'PE\x00\x00':
+ return []
+
+ # Read COFF header.
+ coffHeader = struct.unpack('HHIIIHH', f.read(20))
+ nSections = coffHeader[1]
+ sectionTablePos = coffHeader[5] + f.tell()
+
+ # Read magic signature in standard COFF fields.
+ peType = 'PE32' if f.read(2) == b'\x0b\x01' else 'PE32+'
+
+ # Move to data directories.
+ f.seek(102 if peType == 'PE32' else 118, os.SEEK_CUR)
+
+ # Read the import table.
+ importTablePos, importTableSize = struct.unpack('II', f.read(8))
+
+ # Move to Sections table.
+ f.seek(sectionTablePos, os.SEEK_SET)
+ sections = []
+ idataTablePhysical = -1
+
+ # Search for 'idata' section.
+ for _ in range(nSections):
+ # Read section.
+ section = struct.unpack('8pIIIIIIHHI', f.read(40))
+ sectionName = section[0].replace(b'\x00', b'')
+
+ # Save a reference to the sections.
+ sections += [section]
+
+ if sectionName == b'idata' or sectionName == b'rdata':
+ idataTablePhysical = section[4]
+
+ # If import table was defined calculate it's position in
+ # the file in relation to the address given by 'idata'.
+ if importTableSize > 0:
+ idataTablePhysical += importTablePos - section[2]
+
+ if idataTablePhysical < 0:
+ return []
+
+ # Move to 'idata' section.
+ f.seek(idataTablePhysical, os.SEEK_SET)
+ dllList = set()
+
+ # Read 'idata' directory table.
+ while True:
+ # Read DLL entries.
+ try:
+ dllImport = struct.unpack('IIIII', f.read(20))
+ except:
+ break
+
+ # Null directory entry.
+ if dllImport[0] | dllImport[1] | dllImport[2] | dllImport[3] | dllImport[4] == 0:
+ break
+
+ # Locate where is located the DLL name in relation to the
+ # sections.
+ for section in sections:
+ if dllImport[3] >= section[2] \
+ and dllImport[3] < section[1] + section[2]:
+ dllList.add(dllImport[3] - section[2] + section[4])
+
+ break
+
+ for dll in dllList:
+ # Move to DLL name.
+ f.seek(dll, os.SEEK_SET)
+ dllName = b''
+
+ # Read string until null character.
+ while True:
+ c = f.read(1)
+
+ if c == b'\x00':
+ break
+
+ dllName += c
+
+ try:
+ dllImports.add(dllName.decode(sys.getdefaultencoding()))
+ except:
+ pass
+
+ return dllImports
+
+ def dependencies(self, binary):
+ deps = []
+
+ for dep in self.dump(binary):
+ depPath = self.whereBin(dep)
+
+ if len(depPath) > 0 and not self.isExcluded(depPath):
+ deps.append(depPath)
+
+ return deps
+
+ def name(self, binary):
+ dep = os.path.basename(binary)
+
+ return dep[: dep.find('.')]
diff --git a/ports/deploy/tools/qt5.py b/ports/deploy/tools/qt5.py
new file mode 100644
index 0000000..fcd1f6a
--- /dev/null
+++ b/ports/deploy/tools/qt5.py
@@ -0,0 +1,671 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import configparser
+import json
+import os
+import platform
+import re
+import shutil
+import subprocess # nosec
+import sys
+import time
+import xml.etree.ElementTree as ET
+
+import tools.utils
+
+
+class DeployToolsQt(tools.utils.DeployToolsUtils):
+ def __init__(self):
+ super().__init__()
+ self.qmake = ''
+ self.qtIFW = ''
+ self.qtIFWVersion = ''
+ self.qtInstallBins = ''
+ self.qtInstallQml = ''
+ self.qtInstallPlugins = ''
+ self.qmlRootDirs = []
+ self.qmlInstallDir = ''
+ self.dependencies = []
+ self.binarySolver = None
+ self.installerConfig = ''
+
+ def detectQt(self, path=''):
+ self.detectQmake(path)
+ self.qtInstallBins = self.qmakeQuery(var='QT_INSTALL_BINS')
+ self.qtInstallQml = self.qmakeQuery(var='QT_INSTALL_QML')
+ self.qtInstallPlugins = self.qmakeQuery(var='QT_INSTALL_PLUGINS')
+ self.detectQtIFW()
+ self.detectQtIFWVersion()
+
+ def detectQmake(self, path=''):
+ for makeFile in self.detectMakeFiles(path):
+ with open(makeFile) as f:
+ for line in f:
+ if line.startswith('QMAKE') and '=' in line:
+ self.qmake = line.split('=')[1].strip()
+
+ return
+
+ if 'QMAKE_PATH' in os.environ:
+ self.qmake = os.environ['QMAKE_PATH']
+
+ def detectTargetBinaryFromQt5Make(self, path=''):
+ for makeFile in self.detectMakeFiles(path):
+ with open(makeFile) as f:
+ for line in f:
+ if line.startswith('TARGET') and '=' in line:
+ return os.path.join(path, line.split('=')[1].strip())
+
+ return ''
+
+ def qmakeQuery(self, qmake='', var=''):
+ if qmake == '':
+ if 'QMAKE_PATH' in os.environ:
+ qmake = os.environ['QMAKE_PATH']
+ else:
+ qmake = self.qmake
+
+ try:
+ args = [qmake, '-query']
+
+ if var != '':
+ args += [var]
+
+ process = subprocess.Popen(args, # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+
+ return stdout.strip().decode(sys.getdefaultencoding())
+ except:
+ pass
+
+ return ''
+
+ @staticmethod
+ def detectVersion(proFile):
+ if 'DAILY_BUILD' in os.environ:
+ return 'daily'
+
+ verMaj = '0'
+ verMin = '0'
+ verPat = '0'
+
+ try:
+ with open(proFile) as f:
+ for line in f:
+ if line.startswith('VER_MAJ') and '=' in line:
+ verMaj = line.split('=')[1].strip()
+ elif line.startswith('VER_MIN') and '=' in line:
+ verMin = line.split('=')[1].strip()
+ elif line.startswith('VER_PAT') and '=' in line:
+ verPat = line.split('=')[1].strip()
+ except:
+ pass
+
+ return verMaj + '.' + verMin + '.' + verPat
+
+ def detectQtIFW(self):
+ if 'BINARYCREATOR' in os.environ:
+ self.qtIFW = os.environ['BINARYCREATOR']
+
+ return
+
+ # Try official Qt binarycreator first because it is statically linked.
+
+ if self.targetSystem == 'windows':
+ homeQt = 'C:\\Qt'
+ elif self.targetSystem == 'posix_windows':
+ if 'WINEPREFIX' in os.environ:
+ homeQt = os.path.expanduser(os.path.join(os.environ['WINEPREFIX'],
+ 'drive_c/Qt'))
+ else:
+ homeQt = os.path.expanduser('~/.wine/drive_c/Qt')
+ else:
+ homeQt = os.path.expanduser('~/Qt')
+
+ binCreator = 'binarycreator'
+
+ if self.targetSystem == 'windows' or self.targetSystem == 'posix_windows':
+ binCreator += '.exe'
+
+ for root, _, files in os.walk(homeQt):
+ for f in files:
+ if f == binCreator:
+ self.qtIFW = os.path.join(root, f)
+
+ return
+
+ # binarycreator offered by the system is most probably dynamically
+ # linked, so it's useful for test purposes only, but not recommended
+ # for distribution.
+ self.qtIFW = self.whereBin(binCreator)
+
+ def detectQtIFWVersion(self):
+ self.qtIFWVersion = ''
+
+ if self.qtIFW == '':
+ return
+
+ installerBase = os.path.join(os.path.dirname(self.qtIFW),
+ 'installerbase')
+
+ if self.targetSystem == 'windows' or self.targetSystem == 'posix_windows':
+ installerBase += '.exe'
+
+ self.qtIFWVersion = '2.0.0'
+
+ if not os.path.exists(installerBase):
+ return
+
+ if self.targetSystem == 'posix_windows':
+ installerBase = 'Z:' + installerBase.replace('/', '\\')
+ process = subprocess.Popen(['wine', # nosec
+ installerBase,
+ '--version'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate(input=b'\n')
+ else:
+ process = subprocess.Popen([installerBase, # nosec
+ '--version'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, _ = process.communicate()
+
+ for line in stdout.split(b'\n'):
+ if b'IFW Version:' in line:
+ self.qtIFWVersion = line.split(b' ')[2].replace(b'"', b'').replace(b',', b'').decode(sys.getdefaultencoding())
+
+ return
+
+ @staticmethod
+ def listQmlFiles(path):
+ qmlFiles = set()
+
+ if os.path.isfile(path):
+ baseName = os.path.basename(path)
+
+ if baseName == 'qmldir' or path.endswith('.qml'):
+ qmlFiles.add(path)
+ else:
+ for root, _, files in os.walk(path):
+ for f in files:
+ if f == 'qmldir' or f.endswith('.qml'):
+ qmlFiles.add(os.path.join(root, f))
+
+ return list(qmlFiles)
+
+ @staticmethod
+ def modulePath(importLine):
+ imp = importLine.strip().split()
+ path = imp[1].replace('.', '/')
+ majorVersion = imp[2].split('.')[0]
+
+ if int(majorVersion) > 1:
+ path += '.{}'.format(majorVersion)
+
+ return path
+
+ def scanImports(self, path):
+ if not os.path.isfile(path):
+ return []
+
+ fileName = os.path.basename(path)
+ imports = set()
+
+ if fileName.endswith('.qml'):
+ with open(path, 'rb') as f:
+ for line in f:
+ if re.match(b'^import \\w+' , line):
+ imports.add(self.modulePath(line.strip().decode(sys.getdefaultencoding())))
+ elif fileName == 'qmldir':
+ with open(path, 'rb') as f:
+ for line in f:
+ if re.match(b'^depends ' , line):
+ imports.add(self.modulePath(line.strip().decode(sys.getdefaultencoding())))
+
+ return list(imports)
+
+ def solvedepsQml(self):
+ qmlFiles = set()
+
+ for path in self.qmlRootDirs:
+ path = os.path.join(self.rootDir, path)
+
+ for f in self.listQmlFiles(path):
+ qmlFiles.add(f)
+
+ solved = set()
+ solvedImports = set()
+
+ while len(qmlFiles) > 0:
+ qmlFile = qmlFiles.pop()
+
+ for imp in self.scanImports(qmlFile):
+ if imp in solvedImports:
+ continue
+
+ sysModulePath = os.path.join(self.qtInstallQml, imp)
+ installModulePath = os.path.join(self.qmlInstallDir, imp)
+
+ if os.path.exists(sysModulePath):
+ print(' {} -> {}'.format(sysModulePath, installModulePath))
+ self.copy(sysModulePath, installModulePath)
+ solvedImports.add(imp)
+ self.dependencies.append(os.path.join(sysModulePath, 'qmldir'))
+
+ for f in self.listQmlFiles(sysModulePath):
+ if not f in solved:
+ qmlFiles.add(f)
+
+ solved.add(qmlFile)
+
+ def solvedepsPlugins(self):
+ pluginsMap = {
+ 'Qt53DRenderer': ['sceneparsers', 'geometryloaders'],
+ 'Qt53DQuickRenderer': ['renderplugins'],
+ 'Qt5Declarative': ['qml1tooling'],
+ 'Qt5EglFSDeviceIntegration': ['egldeviceintegrations'],
+ 'Qt5Gui': ['accessible',
+ 'generic',
+ 'iconengines',
+ 'imageformats',
+ 'platforms',
+ 'platforminputcontexts',
+ 'styles'],
+ 'Qt5Location': ['geoservices'],
+ 'Qt5Multimedia': ['audio', 'mediaservice', 'playlistformats'],
+ 'Qt5Network': ['bearer'],
+ 'Qt5Positioning': ['position'],
+ 'Qt5PrintSupport': ['printsupport'],
+ 'Qt5QmlTooling': ['qmltooling'],
+ 'Qt5Quick': ['scenegraph', 'qmltooling'],
+ 'Qt5Sensors': ['sensors', 'sensorgestures'],
+ 'Qt5SerialBus': ['canbus'],
+ 'Qt5Sql': ['sqldrivers'],
+ 'Qt5TextToSpeech': ['texttospeech'],
+ 'Qt5WebEngine': ['qtwebengine'],
+ 'Qt5WebEngineCore': ['qtwebengine'],
+ 'Qt5WebEngineWidgets': ['qtwebengine'],
+ 'Qt5WebView': ['webview'],
+ 'Qt5XcbQpa': ['xcbglintegrations']
+ }
+
+ pluginsMap.update({lib + 'd': pluginsMap[lib] for lib in pluginsMap})
+
+ if self.targetSystem == 'android':
+ pluginsMap.update({lib + '_' + self.targetArch: pluginsMap[lib] for lib in pluginsMap})
+
+ plugins = []
+
+ for dep in self.binarySolver.scanDependencies(self.installDir):
+ libName = self.binarySolver.name(dep)
+
+ if not libName in pluginsMap:
+ continue
+
+ for plugin in pluginsMap[libName]:
+ if not plugin in plugins:
+ sysPluginPath = os.path.join(self.qtInstallPlugins, plugin)
+ pluginPath = os.path.join(self.pluginsInstallDir, plugin)
+
+ if not os.path.exists(sysPluginPath):
+ continue
+
+ print(' {} -> {}'.format(sysPluginPath, pluginPath))
+ self.copy(sysPluginPath, pluginPath)
+ plugins.append(plugin)
+ self.dependencies.append(sysPluginPath)
+
+ def solvedepsAndroid(self):
+ installPrefix = self.qmakeQuery(var='QT_INSTALL_PREFIX')
+ qtLibsPath = self.qmakeQuery(var='QT_INSTALL_LIBS')
+ jars = []
+ permissions = set()
+ features = set()
+ initClasses = set()
+ libs = set()
+
+ for f in os.listdir(self.libInstallDir):
+ basename = os.path.basename(f)[3:]
+ basename = os.path.splitext(basename)[0]
+ depFile = os.path.join(qtLibsPath,
+ basename + '-android-dependencies.xml')
+
+ if os.path.exists(depFile):
+ tree = ET.parse(depFile)
+ root = tree.getroot()
+
+ for jar in root.iter('jar'):
+ jars.append(jar.attrib['file'])
+
+ if 'initClass' in jar.attrib:
+ initClasses.append(jar.attrib['initClass'])
+
+ for permission in root.iter('permission'):
+ permissions.add(permission.attrib['name'])
+
+ for feature in root.iter('feature'):
+ features.add(feature.attrib['name'])
+
+ for lib in root.iter('lib'):
+ if 'file' in lib.attrib:
+ libs.add(lib.attrib['file'])
+
+ self.localLibs = [os.path.basename(lib) for lib in libs]
+
+ print('Copying jar files\n')
+
+ for jar in sorted(jars):
+ srcPath = os.path.join(installPrefix, jar)
+ dstPath = os.path.join(self.rootInstallDir,
+ 'libs',
+ os.path.basename(jar))
+ print(' {} -> {}'.format(srcPath, dstPath))
+ self.copy(srcPath, dstPath)
+
+ manifest = os.path.join(self.rootInstallDir, 'AndroidManifest.xml')
+ manifestTemp = os.path.join(self.rootInstallDir, 'AndroidManifestTemp.xml')
+ tree = ET.parse(manifest)
+ root = tree.getroot()
+ oldFeatures = set()
+ oldPermissions = set()
+
+ for element in root:
+ if element.tag == 'uses-feature':
+ for key in element.attrib:
+ if key.endswith('name'):
+ oldFeatures.add(element.attrib[key])
+ elif element.tag == 'uses-permission':
+ for key in element.attrib:
+ if key.endswith('name'):
+ oldPermissions.add(element.attrib[key])
+
+ features -= oldFeatures
+ permissions -= oldPermissions
+ featuresWritten = len(features) < 1
+ permissionsWritten = len(permissions) < 1
+ replace = {'-- %%INSERT_INIT_CLASSES%% --' : ':'.join(sorted(initClasses)),
+ '-- %%BUNDLE_LOCAL_QT_LIBS%% --': '1',
+ '-- %%USE_LOCAL_QT_LIBS%% --' : '1',
+ '-- %%INSERT_LOCAL_LIBS%% --' : ':'.join(sorted(libs)),
+ '-- %%INSERT_LOCAL_JARS%% --' : ':'.join(sorted(jars))}
+
+ with open(manifest) as inFile:
+ with open(manifestTemp, 'w') as outFile:
+ for line in inFile:
+ for key in replace:
+ line = line.replace(key, replace[key])
+
+ outFile.write(line)
+ spaces = len(line)
+ line = line.lstrip()
+ spaces -= len(line)
+
+ if line.startswith('\n'.format(feature))
+
+ featuresWritten = True
+
+ if line.startswith('\n'.format(permission))
+
+ permissionsWritten = True
+
+ os.remove(manifest)
+ shutil.move(manifestTemp, manifest)
+
+ def writeQtConf(self):
+ paths = {'Plugins': os.path.relpath(self.pluginsInstallDir, self.binaryInstallDir),
+ 'Imports': os.path.relpath(self.qmlInstallDir, self.binaryInstallDir),
+ 'Qml2Imports': os.path.relpath(self.qmlInstallDir, self.binaryInstallDir)}
+ confPath = os.path.dirname(self.qtConf)
+
+ if not os.path.exists(confPath):
+ os.makedirs(confPath)
+
+ with open(self.qtConf, 'w') as qtconf:
+ qtconf.write('[Paths]\n')
+
+ for path in paths:
+ qtconf.write('{} = {}\n'.format(path, paths[path]))
+
+ @staticmethod
+ def readChangeLog(changeLog, appName, version):
+ if os.path.exists(changeLog):
+ with open(changeLog) as f:
+ for line in f:
+ if not line.startswith('{0} {1}:'.format(appName, version)):
+ continue
+
+ # Skip first line.
+ f.readline()
+ changeLogText = ''
+
+ for line_ in f:
+ if re.match('{} \d+\.\d+\.\d+:'.format(appName), line):
+ # Remove last line.
+ i = changeLogText.rfind('\n')
+
+ if i >= 0:
+ changeLogText = changeLogText[: i]
+
+ return changeLogText
+
+ changeLogText += line_
+
+ return ''
+
+ def createInstaller(self):
+ if not os.path.exists(self.qtIFW):
+ return False
+
+ # Read package config
+ packageConf = configparser.ConfigParser()
+ packageConf.optionxform=str
+ packageConf.read(self.packageConfig, 'utf-8')
+
+ # Create layout
+ componentName = 'com.{0}prj.{0}'.format(self.programName)
+ packageDir = os.path.join(self.installerPackages, componentName)
+
+ if not os.path.exists(self.installerConfig):
+ os.makedirs(self.installerConfig)
+
+ dataDir = os.path.join(packageDir, 'data')
+ metaDir = os.path.join(packageDir, 'meta')
+
+ if not os.path.exists(dataDir):
+ os.makedirs(dataDir)
+
+ if not os.path.exists(metaDir):
+ os.makedirs(metaDir)
+
+ self.copy(self.appIcon, self.installerConfig)
+ iconName = os.path.splitext(os.path.basename(self.appIcon))[0]
+ licenseOutFile = os.path.basename(self.licenseFile)
+
+ if not '.' in licenseOutFile and \
+ (self.targetSystem == 'windows' or \
+ self.targetSystem == 'posix_windows'):
+ licenseOutFile += '.txt'
+
+ self.copy(self.licenseFile, os.path.join(metaDir, licenseOutFile))
+ self.copy(self.rootInstallDir, dataDir)
+
+ configXml = os.path.join(self.installerConfig, 'config.xml')
+ appName = packageConf['Package']['appName'].strip()
+
+ with open(configXml, 'w') as config:
+ config.write('\n')
+ config.write('\n')
+ config.write(' {}\n'.format(appName))
+
+ if 'DAILY_BUILD' in os.environ:
+ config.write(' 0.0.0\n')
+ else:
+ config.write(' {}\n'.format(self.programVersion))
+
+ config.write(' {}\n'.format(packageConf['Package']['description'].strip()))
+ config.write(' {}\n'.format(appName))
+ config.write(' {}\n'.format(packageConf['Package']['url'].strip()))
+ config.write(' {}\n'.format(iconName))
+ config.write(' {}\n'.format(iconName))
+ config.write(' {}\n'.format(iconName))
+ config.write(' {}\n'.format(packageConf['Package']['titleColor'].strip()))
+ config.write(' {}\n'.format(self.installerRunProgram))
+ config.write(' {}\n'.format(packageConf['Package']['runMessage'].strip()))
+ config.write(' {}\n'.format(appName))
+ config.write(' {}MaintenanceTool\n'.format(appName))
+ config.write(' true\n')
+ config.write(' {}\n'.format(self.installerTargetDir))
+ config.write('\n')
+
+ self.copy(self.installerScript,
+ os.path.join(metaDir, 'installscript.qs'))
+
+ with open(os.path.join(metaDir, 'package.xml'), 'w') as f:
+ f.write('\n')
+ f.write('\n')
+ f.write(' {}\n'.format(appName))
+ f.write(' {}\n'.format(packageConf['Package']['description'].strip()))
+
+ if 'DAILY_BUILD' in os.environ:
+ f.write(' 0.0.0\n')
+ else:
+ f.write(' {}\n'.format(self.programVersion))
+
+ f.write(' {}\n'.format(time.strftime('%Y-%m-%d')))
+ f.write(' {}\n'.format(componentName))
+ f.write(' \n')
+ f.write(' \n'.format(packageConf['Package']['licenseDescription'].strip(),
+ licenseOutFile))
+ f.write(' \n')
+ f.write(' \n')
+ f.write(' \n')
+
+ if not 'DAILY_BUILD' in os.environ:
+ f.write(self.readChangeLog(self.changeLog,
+ appName,
+ self.programVersion))
+
+ f.write(' \n')
+ f.write(' true\n')
+ f.write(' true\n')
+ f.write(' false\n')
+ f.write('\n')
+
+ # Remove old file
+ if not os.path.exists(self.pkgsDir):
+ os.makedirs(self.pkgsDir)
+
+ if os.path.exists(self.outPackage):
+ os.remove(self.outPackage)
+
+ params = []
+
+ if self.targetSystem == 'posix_windows':
+ params = ['wine']
+
+ params += [self.qtIFW,
+ '-c', configXml,
+ '-p', self.installerPackages,
+ self.outPackage]
+ process = subprocess.Popen(params, # nosec
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ process.communicate()
+
+ return True
+
+ def copyAndroidTemplates(self):
+ installPrefix = self.qmakeQuery(var='QT_INSTALL_PREFIX')
+ sourcesPath = os.path.join(installPrefix, 'src')
+ templates = [os.path.join(sourcesPath, '3rdparty/gradle'),
+ os.path.join(sourcesPath, 'android/templates')]
+
+ for template in templates:
+ self.copy(template, self.rootInstallDir, overwrite=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)
+
+ properties = os.path.join(self.rootInstallDir, 'gradle.properties')
+ platform = self.androidPlatform.replace('android-', '')
+ javaDir = os.path.join(sourcesPath, 'android','java')
+
+ with open(properties, 'w') as f:
+ if 'sdkBuildToolsRevision' in deploymentSettings:
+ f.write('androidBuildToolsVersion={}\n'.format(deploymentSettings['sdkBuildToolsRevision']))
+
+ f.write('androidCompileSdkVersion={}\n'.format(platform))
+ f.write('buildDir=build\n')
+ f.write('qt5AndroidDir={}\n'.format(javaDir))
+
+ def createRccBundle(self):
+ rcc = os.path.join(os.path.dirname(self.qmake), 'rcc')
+ assetsDir = os.path.abspath(os.path.join(self.assetsIntallDir, '..'))
+ assetsFolder = os.path.relpath(self.assetsIntallDir, assetsDir)
+ qrcFile = os.path.join(self.assetsIntallDir, assetsFolder + '.qrc')
+
+ params = [rcc,
+ '--project',
+ '-o', qrcFile]
+ process = subprocess.Popen(params, # nosec
+ cwd=self.assetsIntallDir,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ process.communicate()
+
+ params = [rcc,
+ '--root=/{}'.format(assetsFolder),
+ '--binary',
+ '-o', self.assetsIntallDir + '.rcc',
+ qrcFile]
+ process = subprocess.Popen(params, # nosec
+ cwd=assetsDir,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ process.communicate()
+
+ shutil.rmtree(self.assetsIntallDir, True)
diff --git a/ports/deploy/tools/utils.py b/ports/deploy/tools/utils.py
new file mode 100644
index 0000000..ba14130
--- /dev/null
+++ b/ports/deploy/tools/utils.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Webcamoid, webcam capture application.
+# Copyright (C) 2017 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 .
+#
+# Web-Site: http://webcamoid.github.io/
+
+import fnmatch
+import hashlib
+import multiprocessing
+import os
+import platform
+import shutil
+import subprocess # nosec
+import sys
+
+class DeployToolsUtils:
+ def __init__(self):
+ self.make = ''
+
+ if os.name == 'posix' and sys.platform.startswith('darwin'):
+ self.system = 'mac'
+ elif os.name == 'nt' and sys.platform.startswith('win32'):
+ self.system = 'windows'
+ elif os.name == 'posix':
+ self.system = 'posix'
+ else:
+ self.system = ''
+
+ self.arch = platform.architecture()[0]
+ self.targetArch = self.arch
+ self.targetSystem = self.system
+ pathSep = ';' if self.system == 'windows' else ':'
+ self.sysBinsPath = []
+
+ if 'PATH' in os.environ:
+ self.sysBinsPath += \
+ [path.strip() for path in os.environ['PATH'].split(pathSep)]
+
+ self.njobs = multiprocessing.cpu_count()
+
+ if self.njobs < 4:
+ self.njobs = 4
+
+ def detectTargetArch(self, binary=''):
+ if binary == '':
+ binary = self.mainBinary
+
+ self.targetArch = platform.architecture(binary)[0]
+
+ def whereBin(self, binary):
+ for path in self.sysBinsPath:
+ path = os.path.join(path, binary)
+
+ if os.path.exists(path):
+ return path
+
+ return ''
+
+ def copy(self, src, dst='.', copyReals=False, overwrite=True):
+ if not os.path.exists(src):
+ return False
+
+ if os.path.isdir(src):
+ if os.path.isfile(dst):
+ return False
+
+ for root, dirs, files in os.walk(src):
+ for f in files:
+ fromF = os.path.join(root, f)
+ toF = os.path.relpath(fromF, src)
+ toF = os.path.join(dst, toF)
+ toF = os.path.normpath(toF)
+ self.copy(fromF, toF, copyReals, overwrite)
+
+ for d in dirs:
+ fromD = os.path.join(root, d)
+ toD = os.path.relpath(fromD, src)
+ toD = os.path.join(dst, toD)
+
+ try:
+ os.makedirs(os.path.normpath(toD))
+ except:
+ pass
+ elif os.path.isfile(src):
+ if os.path.isdir(dst):
+ dst = os.path.realpath(dst)
+ dst = os.path.join(dst, os.path.basename(src))
+
+ dirname = os.path.dirname(dst)
+
+ if not os.path.exists(dirname):
+ try:
+ os.makedirs(dirname)
+ except:
+ return False
+
+ if os.path.exists(dst):
+ if not overwrite:
+ return True
+
+ try:
+ os.remove(dst)
+ except:
+ return False
+
+ if copyReals and os.path.islink(src):
+ realpath = os.path.realpath(src)
+ basename = os.path.basename(realpath)
+ os.symlink(os.path.join('.', basename), dst)
+ self.copy(realpath,
+ os.path.join(dirname, basename),
+ copyReals,
+ overwrite)
+ else:
+ try:
+ if self.system == 'windows':
+ shutil.copy(src, dst)
+ else:
+ shutil.copy(src, dst, follow_symlinks=False)
+ except:
+ return False
+
+ return True
+
+
+ def move(self, src, dst='.', moveReals=False):
+ if not os.path.exists(src):
+ return False
+
+ if os.path.isdir(src):
+ if os.path.isfile(dst):
+ return False
+
+ for root, dirs, files in os.walk(src):
+ for f in files:
+ fromF = os.path.join(root, f)
+ toF = os.path.relpath(fromF, src)
+ toF = os.path.join(dst, toF)
+ toF = os.path.normpath(toF)
+ self.move(fromF, toF, moveReals)
+
+ for d in dirs:
+ fromD = os.path.join(root, d)
+ toD = os.path.relpath(fromD, src)
+ toD = os.path.join(dst, toD)
+
+ try:
+ os.makedirs(os.path.normpath(toD))
+ except:
+ pass
+ elif os.path.isfile(src):
+ if os.path.isdir(dst):
+ dst = os.path.realpath(dst)
+ dst = os.path.join(dst, os.path.basename(src))
+
+ dirname = os.path.dirname(dst)
+
+ if not os.path.exists(dirname):
+ try:
+ os.makedirs(dirname)
+ except:
+ return False
+
+ if os.path.exists(dst):
+ try:
+ os.remove(dst)
+ except:
+ return False
+
+ if moveReals and os.path.islink(src):
+ realpath = os.path.realpath(src)
+ basename = os.path.basename(realpath)
+ os.symlink(os.path.join('.', basename), dst)
+ self.move(realpath, os.path.join(dirname, basename), moveReals)
+ else:
+ try:
+ shutil.move(src, dst)
+ except:
+ return False
+
+ return True
+
+ def detectMake(self):
+ if 'MAKE_PATH' in os.environ:
+ self.make = os.environ['MAKE_PATH']
+
+ return
+
+ makes = ['mingw32-make', 'make'] if self.system == 'windows' else ['make']
+ ext = '.exe' if self.system == 'windows' else ''
+
+ for make in makes:
+ makePath = self.whereBin(make + ext)
+
+ if makePath != '':
+ self.make = makePath
+
+ break
+
+ def makeInstall(self, buildDir, installRoot=''):
+ previousDir = os.getcwd()
+ os.chdir(buildDir)
+
+ if installRoot == '':
+ process = subprocess.Popen([self.make, 'install'], # nosec
+ stdout=subprocess.PIPE)
+ else:
+ process = subprocess.Popen([self.make, 'INSTALL_ROOT=' + installRoot, 'install'], # nosec
+ stdout=subprocess.PIPE)
+
+ process.communicate()
+ os.chdir(previousDir)
+
+ return process.returncode
+
+ @staticmethod
+ def sha256sum(fileName):
+ sha = hashlib.sha256()
+
+ with open(fileName, 'rb') as f:
+ while True:
+ data = f.read(1024)
+
+ if not data:
+ break
+
+ sha.update(data)
+
+ return sha.hexdigest()
+
+ @staticmethod
+ def detectMakeFiles(makePath):
+ makeFiles = []
+
+ try:
+ for f in os.listdir(makePath):
+ path = os.path.join(makePath, f)
+
+ if os.path.isfile(path) and fnmatch.fnmatch(f.lower(), 'makefile*'):
+ makeFiles += [path]
+ except:
+ pass
+
+ return makeFiles