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