Last active
June 10, 2022 12:11
-
-
Save R0GERIUS/11810f0ccbbf0ef702b7d056dba02095 to your computer and use it in GitHub Desktop.
script to build qBittorrent with Qt5 on macOS, no Homebrew required!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# standalone script to build qBittorent for macOS (including Apple Silicon based Macs) by Kolcha | |
# | |
# only Xcode must be installed (Xcode 12 is required to produce arm64 binaries) | |
# all required dependencies and tools are automatically downloaded and used only from script's working directory | |
# (can be specified), nothing is installed into the system | |
# working directory is removed on completion if it was not specified | |
# | |
# by default script produces binaries for the architecture it was launched on, but cross-compilation is also supported | |
# in both directions, i.e. x86_64 -> arm64 and arm64 -> x86_64 | |
# | |
# following conventions are used in the script: | |
# - variables names are in snake-case (special variables are an exception) | |
# - variables starting with '_' (underscore) should be considered "internal" | |
# - variables without '_' prefix can be considered "options" or "default values" | |
# | |
# script accepts few arguments which can customize its behavior the same way as editing "option" variables, | |
# see 'command line arguments parsing' section for details and possible options | |
# | |
# script is not interactive and doesn't ask anything, it just automates build routine | |
# it can be launched multiple times with the same or different set of arguments, this may be useful for development | |
# environment setup for example (just pass some working directory, and it will contain everything required for | |
# qBittorrent development) | |
# moreover, passing the same working directory with other arguments allows you to get environment with few different | |
# Qt and/or libtorrent versions (or libraries for different architectures) that you can switch easily | |
# | |
# script is "smart enough" to download and build only required parts (which it considers "missing", but not due to | |
# dependency change) in case when the same working directory is specified multiple times | |
# qBittorrent is compiled in any case, result .dmg file is overridden if required | |
# ===================================================================================================================== | |
# software versions to use | |
qbittorrent_ver=master # qBittorrent https://github.com/qbittorrent/qBittorrent/releases | |
openssl_ver=1.1.1o # OpenSSL https://www.openssl.org/source/ | |
boost_ver=1.79.0 # Boost https://www.boost.org/ | |
libtorrent_ver=2.0.6 # libtorrent https://github.com/arvidn/libtorrent/releases | |
libtorrent_commit="" # libtorrent patch commit | |
qt_ver=5.15.4-lts-lgpl # Qt https://code.qt.io/cgit/qt/qt5.git/refs/tags | |
cmake_ver=3.23.2 # CMake https://cmake.org/download/ | |
# build environment variables | |
target_arch=$(uname -m) # target architecture, host by default | |
# qBittorrent requires C++17 features available only since macOS 10.14 | |
cxxstd=17 # C++ standard | |
min_macos_ver=10.15 # minimum version of the target platform | |
# ===================================================================================================================== | |
work_dir="" # working directory, all files will be placed here | |
prod_dir="${HOME}/Downloads" # output directory for result qBittorrent .dmg image | |
# --------------------------------------------------------------------------------------------------------------------- | |
# command line arguments parsing | |
# https://stackoverflow.com/questions/402377/using-getopts-to-process-long-and-short-command-line-options | |
_die() { echo "$*" >&2; exit 2; } | |
_needs_arg() { if [ -z "$OPTARG" ]; then _die "No arg for --$OPT option"; fi; } | |
_no_arg() { if [ -n "$OPTARG" ]; then _die "No arg allowed for --$OPT option"; fi; } | |
while getopts ha:w:o:-: OPT; do | |
if [ "$OPT" = "-" ]; then | |
OPT="${OPTARG%%=*}" | |
OPTARG="${OPTARG#$OPT}" | |
OPTARG="${OPTARG#=}" | |
fi | |
case "$OPT" in | |
h | help ) | |
echo "no help there! but script accepts few command line agruments, just open it to find them :)" | |
exit 0 | |
;; | |
a | target-arch ) _needs_arg; target_arch="$OPTARG" ;; | |
w | workdir ) _needs_arg; work_dir="${OPTARG%/}" ;; | |
o | outdir ) _needs_arg; prod_dir="${OPTARG%/}" ;; | |
qbittorrent ) _needs_arg; qbittorrent_ver="$OPTARG" ;; | |
openssl ) _needs_arg; openssl_ver="$OPTARG" ;; | |
boost ) _needs_arg; boost_ver="$OPTARG" ;; | |
libtorrent ) _needs_arg; libtorrent_ver="$OPTARG" ;; | |
libtorrent-commit ) _needs_arg; libtorrent_commit="$OPTARG" ;; | |
qt ) _needs_arg; qt_ver="$OPTARG" ;; | |
cmake ) _needs_arg; cmake_ver="$OPTARG" ;; | |
cxx ) _needs_arg; cxxstd="$OPTARG" ;; | |
macos ) _needs_arg; min_macos_ver="$OPTARG" ;; | |
??* ) _die "Illegal option --$OPT" ;; # bad long option | |
? ) exit 2 ;; # bad short option (error reported via getopts) | |
esac | |
done | |
shift $((OPTIND-1)) | |
# --------------------------------------------------------------------------------------------------------------------- | |
set -o errexit # exit immediately if a command exits with a non-zero status | |
set -o nounset # treat unset variables as an error when substituting | |
set -o xtrace # print commands and their arguments as they are executed | |
set -o pipefail # the return value of a pipeline is the status of the last command to exit with a non-zero status | |
# --------------------------------------------------------------------------------------------------------------------- | |
# working directory setup | |
if [[ -z ${work_dir} ]] | |
then | |
work_dir=$(mktemp -d) | |
_remove_work_dir=1 | |
else | |
mkdir -p "${work_dir}" | |
_remove_work_dir=0 | |
fi | |
# output directory setup | |
[[ -d "${prod_dir}" ]] || mkdir -p "${prod_dir}" | |
_abs_path() { _old_pwd=$PWD; cd "$1"; echo $PWD; cd "$_old_pwd"; } | |
work_dir=$(_abs_path "${work_dir}") | |
prod_dir=$(_abs_path "${prod_dir}") | |
cd "${work_dir}" | |
_src_dir="${work_dir}/src" # sources will be downloaded here | |
_tmp_dir="${work_dir}/build-${target_arch}" # build intermediates will placed here | |
_lib_dir="${work_dir}/lib-${target_arch}" # compiled libraries and headers go here | |
# --------------------------------------------------------------------------------------------------------------------- | |
# download everything required (only missing parts will be downloaded) | |
mkdir -p ${_src_dir} | |
pushd "${_src_dir}" > /dev/null | |
_qbt_src_dir_name="qBittorrent-${qbittorrent_ver}" | |
_qbt_src_dir="${_src_dir}/${_qbt_src_dir_name}" | |
_qbt_tmp_dir="${_tmp_dir}/${_qbt_src_dir_name}" | |
# anything known to git (i.e. branch names or tags) can be used as 'version' | |
[[ -d ${_qbt_src_dir} ]] || curl -L https://github.com/qbittorrent/qBittorrent/archive/{$qbittorrent_ver}.tar.gz | tar xz | |
_ssl_src_dir_name="openssl-${openssl_ver}" | |
_ssl_src_dir="${_src_dir}/${_ssl_src_dir_name}" | |
_ssl_tmp_dir="${_tmp_dir}/${_ssl_src_dir_name}" | |
_ssl_lib_dir="${_lib_dir}/${_ssl_src_dir_name}" | |
[[ -d ${_ssl_src_dir} ]] || curl -L https://www.openssl.org/source/openssl-${openssl_ver}.tar.gz | tar xz | |
_boost_ver_u=${boost_ver//./_} | |
_boost_src_dir_name="boost_${_boost_ver_u}" | |
_boost_src_dir="${_src_dir}/${_boost_src_dir_name}" | |
# boost will NOT be compiled, only headers are enough, since 1.69.0 Boost.System is header-only | |
[[ -d ${_boost_src_dir} ]] || curl -L https://boostorg.jfrog.io/artifactory/main/release/${boost_ver}/source/boost_${_boost_ver_u}.tar.bz2 | tar xj | |
[[ ${libtorrent_commit} ]] && _lt_src_dir_name="libtorrent" || _lt_src_dir_name="libtorrent-rasterbar-${libtorrent_ver}" | |
_lt_src_dir="${_src_dir}/${_lt_src_dir_name}" | |
_lt_tmp_dir="${_tmp_dir}/${_lt_src_dir_name}" | |
_lt_lib_dir="${_lib_dir}/${_lt_src_dir_name}" | |
# use libtorrent release archives, because GitHub doesn't include submodules into generated archives, | |
# but since 2.0 libtorrent has few, and they are required for compilation | |
if ! [[ -d ${_lt_src_dir} ]] | |
then | |
if ! [[ ${libtorrent_commit} ]] | |
then | |
curl -L https://github.com/arvidn/libtorrent/releases/download/v${libtorrent_ver}/libtorrent-rasterbar-${libtorrent_ver}.tar.gz | tar xz | |
else | |
git clone --recurse-submodules https://github.com/arvidn/libtorrent.git | |
cd ${_lt_src_dir} | |
git checkout --recurse-submodules ${libtorrent_commit} | |
cd .. | |
fi | |
fi | |
# unfortunately, Qt repository must be cloned... it is much easier rather to deal with release archive | |
_qt_src_dir_name="qt-${qt_ver}" | |
_qt_src_dir="${_src_dir}/${_qt_src_dir_name}" | |
_qt_tmp_dir="${_tmp_dir}/${_qt_src_dir_name}" | |
_qt_lib_dir="${_lib_dir}/${_qt_src_dir_name}" | |
if ! [[ -d ${_qt_src_dir} ]] | |
then | |
curl -L https://gist.githubusercontent.com/R0GERIUS/a2d62e21995e732b9fe510efce71965d/raw/26e9f02b88d6eb41b3a6b636248d6290094ecdc9/qt5_xcode13.patch -O | |
git clone https://code.qt.io/qt/qt5.git ${_qt_src_dir_name} | |
cd ${_qt_src_dir_name} | |
git checkout "v${qt_ver}" # use only tags, not branches | |
perl init-repository --module-subset=qtbase,qtmacextras,qtsvg,qttools,qttranslations | |
git apply --whitespace=nowarn ${_src_dir}/qt5_xcode13.patch | |
cd .. | |
fi | |
# Qt is not ready for Apple Silicon out of the box, but it is easy to get support of it just using few tricks | |
_host_arch=$(uname -m) | |
_qt_cross_compile_args="" | |
# first trick is required when compiling on Apple Silicon based Mac, | |
# without it Qt build system produces x86_64 binaries instead of arm64 | |
_host_conf_file="${_qt_src_dir}/qtbase/mkspecs/common/macx.conf" | |
# in any case (regardless of architecture) replace QMAKE_APPLE_DEVICE_ARCHS in macx.conf | |
perl -pi -e "s/QMAKE_APPLE_DEVICE_ARCHS = \w+/QMAKE_APPLE_DEVICE_ARCHS = ${_host_arch}/g" "${_host_conf_file}" | |
# qBittorrent requires C++17 features available only since macOS 10.14, | |
# we are building Qt library only for qBittorrent, so let Qt also benefit from new standard | |
# even Qt 5.15.x still has QMAKE_MACOSX_DEPLOYMENT_TARGET lower than 10.14, so just replace it | |
perl -pi -e "s/QMAKE_MACOSX_DEPLOYMENT_TARGET = \d+\.\d+/QMAKE_MACOSX_DEPLOYMENT_TARGET = ${min_macos_ver}/g" "${_host_conf_file}" | |
# next trick is required for cross-compilation | |
# to get arm64 binaries using only x86_64 host, some Qt' mkspec is required to describe desired target | |
# Qt doesn't have such spec for arm64 macOS binaries, so just create new one based on 'macx-clang' used for macOS builds | |
# note: this trick works in both directions, i.e. it is possible to get x86_64 binaries on Apple Silicon Mac | |
if [[ "${target_arch}" != "${_host_arch}" ]] | |
then | |
_target_mkspec_name="macx-clang-${target_arch}" | |
if ! [[ -d "${_qt_src_dir}/qtbase/mkspecs/${_target_mkspec_name}" ]] | |
then | |
cp -r "${_qt_src_dir}/qtbase/mkspecs/macx-clang" "${_qt_src_dir}/qtbase/mkspecs/${_target_mkspec_name}" | |
target_macx_conf_file="${_qt_src_dir}/qtbase/mkspecs/${_target_mkspec_name}/qmake.conf" | |
# delete last line in file 'load(qt_config)', all variables should be set before it | |
sed -i '' -e '$ d' "${target_macx_conf_file}" | |
# next line is crucial, it overrides QMAKE_APPLE_DEVICE_ARCHS inherited from macx.conf | |
# and thereby makes cross-compilation possible | |
echo "QMAKE_APPLE_DEVICE_ARCHS = ${target_arch}" >> "${target_macx_conf_file}" | |
# re-add previously deleted line | |
echo "" >> "${target_macx_conf_file}" | |
echo "load(qt_config)" >> "${target_macx_conf_file}" | |
fi | |
_qt_cross_compile_args="-xplatform ${_target_mkspec_name}" | |
fi | |
popd > /dev/null # back to working directory | |
# download CMake, 3.19.2 and above is required for Apple Silicon support | |
_cmake_dir_name="cmake-${cmake_ver}-macos-universal" | |
[[ -d ${_cmake_dir_name} ]] || curl -L https://github.com/Kitware/CMake/releases/download/v${cmake_ver}/cmake-${cmake_ver}-macos-universal.tar.gz | tar xz | |
cmake="${work_dir}/${_cmake_dir_name}/CMake.app/Contents/bin/cmake" | |
# --------------------------------------------------------------------------------------------------------------------- | |
# everything is prepared now, time to start the build | |
# | |
# all dependencies are built as static libraries, the main reason for that were some Qt-specific issues, | |
# see comments below for details | |
# | |
# all options used at configuration stage are set only based on only my opinion or preference, there are no strict | |
# reasons for most of options in most cases | |
if ! [[ -d ${_ssl_lib_dir} ]] | |
then | |
rm -rf ${_ssl_tmp_dir} && mkdir -p ${_ssl_tmp_dir} | |
pushd ${_ssl_tmp_dir} > /dev/null | |
"${_ssl_src_dir}/Configure" no-comp no-deprecated no-dynamic-engine no-tests no-shared no-zlib --openssldir=/etc/ssl --prefix=${_ssl_lib_dir} -mmacosx-version-min=${min_macos_ver} darwin64-${target_arch}-cc | |
make -j$(sysctl -n hw.ncpu) | |
make install_sw | |
popd > /dev/null | |
fi | |
if ! [[ -d ${_lt_lib_dir} ]] | |
then | |
rm -rf ${_lt_tmp_dir} | |
${cmake} -S ${_lt_src_dir} -B ${_lt_tmp_dir} -D CMAKE_VERBOSE_MAKEFILE=ON -D CMAKE_PREFIX_PATH="${_boost_src_dir};${_ssl_lib_dir}" -D CMAKE_CXX_STANDARD=${cxxstd} -D CMAKE_CXX_EXTENSIONS=OFF -D CMAKE_OSX_DEPLOYMENT_TARGET=${min_macos_ver} -D CMAKE_OSX_ARCHITECTURES=${target_arch} -D CMAKE_BUILD_TYPE=Release -D BUILD_SHARED_LIBS=OFF -D deprecated-functions=OFF -D CMAKE_INSTALL_PREFIX=${_lt_lib_dir} | |
${cmake} --build ${_lt_tmp_dir} -j$(sysctl -n hw.ncpu) | |
${cmake} --install ${_lt_tmp_dir} | |
fi | |
# due to next reasons Qt is compiled as static libraries (even I don't prefer that...): | |
# - useful tool called 'macdeployqt' is compiled only for target architecture, so it can't be used when cross-compiling | |
# - mentioned 'macdeployqt' in 5.15.2 doesn't do its tasks as it should do, and some manual adjustments are required | |
# - without 'macdeployqt' deploying Qt binaries into application bundle is complex and error-prone task | |
if ! [[ -d ${_qt_lib_dir} ]] | |
then | |
rm -rf ${_qt_tmp_dir} && mkdir -p ${_qt_tmp_dir} | |
pushd ${_qt_tmp_dir} > /dev/null | |
"${_qt_src_dir}/configure" -prefix ${_qt_lib_dir} -opensource -confirm-license -release -static -appstore-compliant -c++std c++${cxxstd} -no-pch -I "${_ssl_lib_dir}/include" -L "${_ssl_lib_dir}/lib" -make libs -no-compile-examples -no-dbus -no-icu -qt-pcre -system-zlib -ssl -openssl-linked -no-cups -qt-libpng -qt-libjpeg -no-feature-testlib -no-feature-concurrent -platform macx-clang ${_qt_cross_compile_args} | |
make -j$(sysctl -n hw.ncpu) | |
make install | |
popd > /dev/null | |
fi | |
# build qBittorrent each time script launched | |
rm -rf ${_qbt_tmp_dir} | |
${cmake} -S ${_qbt_src_dir} -B ${_qbt_tmp_dir} -D CMAKE_VERBOSE_MAKEFILE=ON -D CMAKE_PREFIX_PATH="${_boost_src_dir};${_ssl_lib_dir};${_lt_lib_dir};${_qt_lib_dir}" -D CMAKE_CXX_STANDARD=${cxxstd} -D CMAKE_CXX_EXTENSIONS=OFF -D CMAKE_CXX_VISIBILITY_PRESET=hidden -D CMAKE_VISIBILITY_INLINES_HIDDEN=ON -D CMAKE_OSX_DEPLOYMENT_TARGET=${min_macos_ver} -D CMAKE_OSX_ARCHITECTURES=${target_arch} -D CMAKE_BUILD_TYPE=Release | |
${cmake} --build ${_qbt_tmp_dir} -j$(sysctl -n hw.ncpu) | |
# build result .dmg image containing qBittorrent | |
mkdir -p ${prod_dir} | |
_qbt_dmg_path="${prod_dir}/${_qbt_src_dir_name}-macOS-${target_arch}.dmg" | |
pushd ${_qbt_tmp_dir} > /dev/null | |
mv qbittorrent.app qBittorrent.app | |
codesign --deep --force --verify --verbose --sign "-" qBittorrent.app | |
hdiutil create -srcfolder qBittorrent.app -nospotlight -layout NONE -fs HFS+ -format ULFO -ov ${_qbt_dmg_path} | |
popd > /dev/null | |
# only automatically created directory will be removed | |
[[ $_remove_work_dir -eq 0 ]] || rm -rf ${work_dir} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment