-
-
Save Kolcha/3ccd533123b773ba110b8fd778b1c2bf to your computer and use it in GitHub Desktop.
#!/bin/zsh | |
# standalone script to build qBittorent for macOS (including Apple Silicon based Macs) | |
# | |
# 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=3.0.11 # OpenSSL https://www.openssl.org/source/ | |
boost_ver=1.83.0 # Boost https://www.boost.org/ | |
libtorrent_ver=2.0.9 # libtorrent https://github.com/arvidn/libtorrent/releases | |
qt_ver=6.5.3 # Qt https://code.qt.io/cgit/qt/qt5.git/refs/tags | |
cmake_ver=3.27.6 # CMake https://cmake.org/download/ | |
# build environment variables | |
target_arch=$(uname -m) # target architecture, host by default | |
min_macos_ver=11.0 # 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 | |
universal_arch_tag="universal" # value used as arch to build a universal binary | |
# --------------------------------------------------------------------------------------------------------------------- | |
# 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" ;; | |
qt ) _needs_arg; qt_ver="$OPTARG" ;; | |
cmake ) _needs_arg; cmake_ver="$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}" | |
# get rid of symlinks in paths, Qt6 build system fails if they are faced... requires zsh! | |
work_dir=${work_dir:A} | |
prod_dir=${prod_dir:A} | |
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 | |
_qbt_dmg_path="${prod_dir}/qBittorrent-${qbittorrent_ver}-macOS-${target_arch}.dmg" | |
[[ "${target_arch}" == "${universal_arch_tag}" ]] && target_arch="x86_64;arm64" | |
# --------------------------------------------------------------------------------------------------------------------- | |
# 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 | |
_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 | |
[[ -d ${_lt_src_dir} ]] || curl -L https://github.com/arvidn/libtorrent/releases/download/v${libtorrent_ver}/libtorrent-rasterbar-${libtorrent_ver}.tar.gz | tar xz | |
# Qt6 requires ninja build system. even this is not strict requirement, but why not to satisfy it? | |
# download ninja sources and later build them for required architecture, use master branch | |
_ninja_ver="master" # ninja https://github.com/ninja-build/ninja/releases | |
_ninja_src_dir_name="ninja-${_ninja_ver}" | |
_ninja_src_dir="${_src_dir}/${_ninja_src_dir_name}" | |
_ninja_tmp_dir="${work_dir}/build-$(uname -m)/${_ninja_src_dir_name}" | |
# anything known to git (i.e. branch names or tags) can be used as 'version' | |
[[ -d ${_ninja_src_dir} ]] || curl -L https://github.com/ninja-build/ninja/archive/{$_ninja_ver}.tar.gz | tar xz | |
# 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 | |
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,qtsvg,qttools,qttranslations | |
cd .. | |
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" | |
# Qt6 uses CMake as build system, but also provides convenient configure script | |
# this script relies on cmake executable in PATH | |
export PATH="$(dirname ${cmake})":$PATH | |
# Qt6 requires ninja build system. even this is not strict requirement, but why not to satisfy it? | |
# even more, ninja can be used to build almost any cmake-based project | |
# ninja is a build tool, so it must be compiled only for build host architecture, so that's why CMAKE_OSX_ARCHITECTURES is omitted | |
if ! [[ -d ${_ninja_tmp_dir} ]] | |
then | |
${cmake} -S ${_ninja_src_dir} -B ${_ninja_tmp_dir} -D CMAKE_VERBOSE_MAKEFILE=ON -D CMAKE_OSX_DEPLOYMENT_TARGET=${min_macos_ver} -D CMAKE_BUILD_TYPE=Release | |
${cmake} --build ${_ninja_tmp_dir} -j$(sysctl -n hw.ncpu) | |
fi | |
# to be able to use ninja, it must be available in PATH | |
export PATH=${_ninja_tmp_dir}:$PATH | |
# --------------------------------------------------------------------------------------------------------------------- | |
# everything is prepared now, time to start the build | |
# | |
# all dependencies are built as static libraries, the main reason for that was a possibility to use LTO | |
# | |
# all options used at configuration step are set only based on only my opinion or preference, there are no strict | |
# reasons for most of options in most cases | |
# OpenSSL doesn't provide the way to build an universal binary in one build step, so the only one way to get it - | |
# is build for each architecture separately and then merge architecture-specific binaries into one universal binary | |
function build_openssl_arch() | |
{ | |
local arch=$1 | |
local ssl_lib_dir="${work_dir}/lib-${arch}/${_ssl_src_dir_name}" | |
local ssl_tmp_dir="${work_dir}/build-${arch}/${_ssl_src_dir_name}" | |
if ! [[ -d ${ssl_lib_dir} ]] | |
then | |
rm -rf ${ssl_tmp_dir} && mkdir -p $_ && cd $_ | |
"${_ssl_src_dir}/Configure" no-comp no-dynamic-engine no-tests no-shared no-zlib --openssldir=/etc/ssl --prefix=${ssl_lib_dir} -mmacosx-version-min=${min_macos_ver} darwin64-${arch}-cc | |
make -j$(sysctl -n hw.ncpu) | |
make install_sw | |
cd - | |
fi | |
} | |
# OpenSSL is used by Qt, which is compiled as universal binaries in case of cross-compilation | |
# so OpenSSL is also must be universal binary when it is used for cross-compilation | |
[[ "${target_arch}" != "$(uname -m)" ]] && _ssl_lib_dir="${work_dir}/lib-${universal_arch_tag}/${_ssl_src_dir_name}" | |
if ! [[ -d ${_ssl_lib_dir} ]] | |
then | |
if [[ "${target_arch}" == "$(uname -m)" ]] | |
then | |
build_openssl_arch ${target_arch} | |
else | |
build_openssl_arch x86_64 | |
build_openssl_arch arm64 | |
mkdir -p "${_ssl_lib_dir}/lib" | |
# copy include directory (includes are the same for each arch) | |
cp -r "${work_dir}/lib-x86_64/${_ssl_src_dir_name}/include" "${_ssl_lib_dir}/" | |
# create universal binaries using lipo | |
lipo -create "${work_dir}/lib-x86_64/${_ssl_src_dir_name}/lib/libcrypto.a" "${work_dir}/lib-arm64/${_ssl_src_dir_name}/lib/libcrypto.a" -output "${_ssl_lib_dir}/lib/libcrypto.a" | |
lipo -create "${work_dir}/lib-x86_64/${_ssl_src_dir_name}/lib/libssl.a" "${work_dir}/lib-arm64/${_ssl_src_dir_name}/lib/libssl.a" -output "${_ssl_lib_dir}/lib/libssl.a" | |
fi | |
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=14 -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 | |
_qt_arch=${target_arch} | |
# Qt6 supports universal binaries out of the box, and in case of cross-compilation | |
# it is easier to build universal binaries rather than only for target architecture | |
if [[ "${target_arch}" != "$(uname -m)" ]] | |
then | |
_qt_arch="x86_64;arm64" | |
_qt_tmp_dir="${work_dir}/build-${universal_arch_tag}/${_qt_src_dir_name}" | |
_qt_lib_dir="${work_dir}/lib-${universal_arch_tag}/${_qt_src_dir_name}" | |
fi | |
if ! [[ -d ${_qt_lib_dir} ]] | |
then | |
rm -rf ${_qt_tmp_dir} && mkdir -p $_ && cd $_ | |
"${_qt_src_dir}/configure" -prefix ${_qt_lib_dir} -release -static -appstore-compliant -no-pch -no-dbus -no-icu -qt-pcre -system-zlib -ssl -openssl-linked -no-securetransport -no-cups -qt-libpng -qt-libjpeg -no-feature-testlib -no-feature-concurrent -no-feature-androiddeployqt -no-feature-assistant -no-feature-designer -no-feature-pixeltool -- -D CMAKE_VERBOSE_MAKEFILE=ON -D CMAKE_PREFIX_PATH="${_ssl_lib_dir}" -D CMAKE_OSX_ARCHITECTURES=${_qt_arch} | |
${cmake} --build . --parallel | |
${cmake} --install . | |
cd - | |
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_OSX_DEPLOYMENT_TARGET=${min_macos_ver} -D CMAKE_OSX_ARCHITECTURES=${target_arch} -D CMAKE_BUILD_TYPE=Release -D QT6=ON | |
${cmake} --build ${_qbt_tmp_dir} -j$(sysctl -n hw.ncpu) | |
# build result .dmg image containing qBittorrent | |
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} |
Does someone have a link to the latest dmg for arm64?
On the official website all download links are 'universal' builds nowadays, which includes both x86_64 and arm64 in one app.
Works beautifully on Apple M2, MacOS Sonoma 14.5. Installed Xcode 15.4 and set following to compile a bleeding edge version of qBittorrent:
qbittorrent_ver=master
openssl_ver=3.3.1
boost_ver=1.85.0
libtorrent_ver=2.0.10
qt_ver=6.7.2
cmake_ver=3.29.6
Thank you so much @Kolcha
Working on Apple Intel, Sonoma 14.5, Xcode 15.4
qbittorrent_ver=release-4.6.5
openssl_ver=3.3.0
boost_ver=1.85.0
libtorrent_ver=2.0.10
qt_ver=6.7.2
cmake_ver=3.29.2
🥹 I have been waiting for the autoscroll feature to be fixed for two years.... Qt version 6.7.2 fixes it and your script allowed me to build it. Thanks @Kolcha
Does someone have a link to the latest dmg for arm64?