Skip to content

Instantly share code, notes, and snippets.

@R0GERIUS
Last active June 10, 2022 12:11
Show Gist options
  • Save R0GERIUS/11810f0ccbbf0ef702b7d056dba02095 to your computer and use it in GitHub Desktop.
Save R0GERIUS/11810f0ccbbf0ef702b7d056dba02095 to your computer and use it in GitHub Desktop.
script to build qBittorrent with Qt5 on macOS, no Homebrew required!
#!/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