diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4a55408b112779261f67d6824e861a80d0978d68..bf1ae8878f8cf905afb4d332b4872f3983b385b5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -69,7 +69,7 @@ mac_x64:
     - cmake .. -DCMAKE_PREFIX_PATH="$PYPLAT;$OPTDIR;$QTDIR/lib/cmake" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DBA_APPLE_BUNDLE=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=11 -DCMAKE_INSTALL_PREFIX=/tmp/ba -DBA_PY_PACKAGE=ON
     - make -j$NPROC
     - ctest -j3 --output-on-failure
-    - zsh var/mk_pypack_macos.zsh
+    - python3 var/mac_py_package.py
     - cpack -B ./installer .
   artifacts: &mac_artifacts
     paths:
diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt
index 3dac630ba210a35ea2c0c52417898d47c74d0f45..4f90cd016f1cca6167b2f9ddfed554d80235732c 100644
--- a/App/CMakeLists.txt
+++ b/App/CMakeLists.txt
@@ -116,8 +116,12 @@ if(BA_APPLE_BUNDLE)
         RENAME qt.conf)
 
     # adjust MacOS bundle
-    set(MACPKG_ROOT_DIR "${CMAKE_INSTALL_PREFIX}/${destination_prefix}")
-    set(MACPKG_MAIN_EXE "${executable_name}")
+    get_property(exe_dir TARGET ${executable_name} PROPERTY RUNTIME_OUTPUT_DIRECTORY)
+    set(MACPKG_MAIN_EXE "${exe_dir}/${executable_name}.app/Contents/MacOS/${executable_name}")
+
+    # list of BornAgain core libraries
+    set(MACPKG_CORE_LIBS "$CACHE{BornAgain_LIBRARIES}")
+
     # list of required Qt plugins (paths relative to Qt root dir)
     set(MACPKG_QT_PLUGINS_RELDIR "${QT6_INSTALL_PLUGINS}")
     set(MACPKG_QT_PLUGINS
@@ -130,11 +134,11 @@ if(BA_APPLE_BUNDLE)
     # Qt plugins directory in within the MacOS bundle
     set(MACPKG_QT_PLUGINS_DIR PlugIns)  # conform to settings in `qt.conf`
     # convert CMake list to space-separated lists
-    string(REPLACE ";" " " MACPKG_EXTRA_LIBS "${BA_Dependencies}")
-    string(REPLACE ";" " " MACPKG_QT_PLUGINS "${MACPKG_QT_PLUGINS}")
+    set(MACPKG_EXTRA_LIBS "${BA_Dependencies}")
+
+    configure_file("${CMAKE_SOURCE_DIR}/devtools/deploy/mac/mac_package.py.in"
+        "${BUILD_VAR_DIR}/mac_package.py" @ONLY)
 
-    configure_file("${CMAKE_SOURCE_DIR}/devtools/deploy/mac/adjust_mac_bundle.zsh.in"
-        "${BUILD_VAR_DIR}/adjust_mac_bundle.zsh" @ONLY)
 endif()
 
 if(WIN32)
diff --git a/CHANGELOG b/CHANGELOG
index 58b32a06e4c151bc77c830daa71872240dbee24e..18dd23b4828a9b3824a6c8b9a4c90f6b769e29d7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,7 +3,7 @@ BornAgain-22.0, in preparation
     * Scan axes have bins of width 0
     * LambdaScan for time-of-flight offspec
     * Datafield::flat flattens out axis of size 1
-> API:
+   > API:
     * FlatDetector replaces RectangularDetector
     * SphericalDetector is obsolete. For 2D scattering, use FlatDetector as shown in examples
     * Offspec analyzer is set for detector, not scan(#651)
@@ -26,6 +26,25 @@ BornAgain-22.0, in preparation
     * In scattering simulation, restrict Re(n) to [0.9,1.1], to prevent unintended
       computation with erroneous material data (e.g. accidental omission of factor 1e-6) (#715)
 
+BornAgain-21.1, released 2023.10.19
+  > Bug fixes:
+    * Repair backward compatibility of GUI projects
+    * Apply q-space units in 1D data loader
+    * Fix simulation background (#323)
+    * Untangle polarizer and analyzer checkboxes (#739)
+    * Fix applying wavelength distribution to specular scan in GUI (#740)
+    * Fix depthprobe simulation with averaged particle layer (#753)
+    * Fix linking 1D data to instrument
+  > GUI improvements:
+    * Show SLD units in the material editor
+    * Change the default minimizer and the objective metric
+    * Add new options to 1D data loader:
+      * Sort by argument
+      * Discard negative argument
+      * Discard argument duplications
+    * Alpha and lambda distributions are disabled as unsupported in depth-probe and
+        off-specular simulations
+
 BornAgain-21.0, released 2023.08.16
   > New functionality:
     * Averaged magnetization profile can be obtained with 'magnetizationProfile()' (#77)
diff --git a/GUI/CMakeLists.txt b/GUI/CMakeLists.txt
index f19949702d2883e3f0c02a2fb8d6d5354970b0b3..8066cf044ef68530d4ae154f3091348a4f65ea2d 100644
--- a/GUI/CMakeLists.txt
+++ b/GUI/CMakeLists.txt
@@ -40,6 +40,10 @@ endif()
 add_library(${lib} SHARED ${source_files} ${RC_SRCS} ${include_files} ${form_files})
 #set_target_properties(${lib} PROPERTIES PREFIX ${libprefix} SUFFIX ${libsuffix})
 
+get_target_property(lib_dir ${lib} LIBRARY_OUTPUT_DIRECTORY)
+set(BornAgain_LIBRARIES "$CACHE{BornAgain_LIBRARIES};${lib_dir}/${libprefix}${lib}${libsuffix}"
+    CACHE INTERNAL "BornAgain libraries")
+
 # switch OFF Qt debug output in any configuration except DEBUG
 target_compile_definitions(${lib} PUBLIC $<$<NOT:$<CONFIG:Debug>>:QT_NO_DEBUG_OUTPUT>)
 
diff --git a/cmake/BornAgain/MakeLib.cmake b/cmake/BornAgain/MakeLib.cmake
index 0f9a0ef5b87cd18d09933931f6046773d748f830..5387f61af19d065216766a5648db3246ecf39b5d 100644
--- a/cmake/BornAgain/MakeLib.cmake
+++ b/cmake/BornAgain/MakeLib.cmake
@@ -35,6 +35,10 @@ function(MakeLib lib)
         # eg., libBornAgainBase.so
         _BASEFILENAME ${libprefix}${lib}${libsuffix})
 
+    get_target_property(lib_dir ${lib} LIBRARY_OUTPUT_DIRECTORY)
+    get_target_property(lib_name ${lib} _BASEFILENAME)
+    set(BornAgain_LIBRARIES "$CACHE{BornAgain_LIBRARIES};${lib_dir}/${lib_name}" CACHE INTERNAL "BornAgain libraries")
+
     # NOTE on run-time linking:
     # - Linux:
     # See <https://en.wikipedia.org/wiki/Rpath>
diff --git a/cmake/configurables/FixPack.cmake.in b/cmake/configurables/FixPack.cmake.in
index 6e19bbf0248f117f1848347f925a2fcb3e3f0d4f..cf376bf22d2fd62b41eba36f6966ac0051e8525a 100644
--- a/cmake/configurables/FixPack.cmake.in
+++ b/cmake/configurables/FixPack.cmake.in
@@ -33,7 +33,8 @@ if(LINUX)
         "@BUILD_VAR_DIR@/adjust_pkg_linux.sh" "${pkg_root}")
 elseif(APPLE)
     addPyWheel("${pkg_root}/@destination_pypackage@")
-    execute_process(COMMAND zsh "@BUILD_VAR_DIR@/adjust_mac_bundle.zsh"
+    message("FixPack: @Python3_EXECUTABLE@ '@BUILD_VAR_DIR@/mac_package.py' '${pkg_root}/@destination_root@'")
+    execute_process(COMMAND "@Python3_EXECUTABLE@" "@BUILD_VAR_DIR@/mac_package.py"
         "${pkg_root}/@destination_root@")
 elseif(WIN32)
     addPyWheel("${pkg_root}/@destination_pypackage@")
diff --git a/cmake/multipython/MakePythonWheel.cmake b/cmake/multipython/MakePythonWheel.cmake
index 5c3a399330e60bd02007c0157064e7aa42ceb62f..7806f152b8a01b03ca1be5aaa71be6d18154b04c 100644
--- a/cmake/multipython/MakePythonWheel.cmake
+++ b/cmake/multipython/MakePythonWheel.cmake
@@ -25,8 +25,12 @@ function(make_python_wheel)
         # On MacOS, building the Python packages needs further effort
         # which is performed in a dedicated shell script which
         # should be executed after build process is done (like CPack).
-        configure_file("${CMAKE_SOURCE_DIR}/devtools/deploy/mac/mk_pypack_macos.zsh.in"
-            ${BUILD_VAR_DIR}/mk_pypack_macos.zsh @ONLY)
+        set(BA_PY_MODE "ON")
+        set(MACPKG_EXTRA_LIBS "${_extra_libs}")
+
+        configure_file("${CMAKE_SOURCE_DIR}/devtools/deploy/mac/mac_package.py.in"
+            ${BUILD_VAR_DIR}/mac_py_package.py @ONLY)
+
         add_custom_target(BAPyWheel ALL
             COMMENT "${header} Script to build the Python wheel: "
             "'${${BUILD_VAR_DIR}/mk_pypack_macos.zsh}'"
diff --git a/devtools/deploy/mac/adjust_mac_bundle.zsh.in b/devtools/deploy/mac/adjust_mac_bundle.zsh.in
deleted file mode 100644
index c5ab991b1c9776353e031a4f5ae91e708f26225f..0000000000000000000000000000000000000000
--- a/devtools/deploy/mac/adjust_mac_bundle.zsh.in
+++ /dev/null
@@ -1,350 +0,0 @@
-#!/bin/zsh
-
-#  **************************************************************************  #
-#   BornAgain: simulate and fit reflection and scattering
-#
-#   @file      adjust_mac_bundle.zsh.in
-#   @brief     Adjusts the library references in the MacOS bundle to make
-#              them relocatable.
-#
-#   @homepage  http://apps.jcns.fz-juelich.de/BornAgain
-#   @license   GNU General Public License v3 or higher (see COPYING)
-#   @copyright Forschungszentrum Juelich GmbH 2016
-#   @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
-#  **************************************************************************  #
-
-# Adjust MacOS Bundle:
-# This script is called by CPack to adjust the bundle contents before making
-# the final DMG file.
-# See also:
-# - 'Qt for macOS - Deployment' <https://doc.qt.io/qt-5/macos-deployment.html>
-# - 'Using qt.conf' <https://doc.qt.io/qt-5/qt-conf.html>
-
-# Package structure:
-# <Package-root>
-#  |
-#  +--MacOS  {includes main executable: `bornagain`}
-#  |
-#  +--lib  {main libraries, like `_libBornAgainBase.so`}
-#  |
-#  +--bin  {not used}
-#  |
-#  +--Library  {extra libraries, like `libformfactor`}
-#  |
-#  +--Frameworks  {Qt framework}
-#  |
-#  +--PlugIns  {Qt plugins)
-#  |
-#  +--Resources  {icons and `qt.conf`}
-#  |
-#  +--share
-#     |
-#     +--BornAgain-<version>
-#        |
-#        +--Examples
-#        |
-#        +--Images
-
-# use extended glob (see <https://zsh.sourceforge.io/Doc/Release/Expansion.html>);
-# similar to `shopt -s extglob` in Bash.
-setopt KSH_GLOB
-# allow empty or failing globs
-setopt +o nomatch
-setopt nullglob
-
-# include shell helper functions
-# (expected to be in the same folder as the current script)
-source "@CMAKE_SOURCE_DIR@/devtools/deploy/mac/shutils.zsh"
-#========================================
-declare -r TITLE="* MacOS Package"
-
-#-- directories for the package binaries
-pkg_root="$1" # root dir, eg. '/tmp/bornagain.app/Contents'
-main_exe="$pkg_root/MacOS/@MACPKG_MAIN_EXE@"
-extra_libs="@MACPKG_EXTRA_LIBS@"
-# eg. input Qt dir = '/usr/local/opt/qt@5/lib/cmake/Qt'
-#   => '/usr/local/opt/qt@5'
-qtdir="@Qt_DIR@"
-# Qt plugins paths relative to Qt root dir
-qt_plugins_rel_dir="@MACPKG_QT_PLUGINS_RELDIR@"
-qt_framework_root="$qtdir"
-# list of required Qt plugins
-qt_plugins="@MACPKG_QT_PLUGINS@"
-declare -ar qt_plugins=( ${=qt_plugins} )
-
-if [[ -z "$pkg_root" ]]
-then
-    echo "$TITLE: Error: Provide the root directory of the package."
-    exit 1
-fi
-
-if [[ -z "$qt_framework_root" ]]
-then
-    echo "$TITLE: Error: Provide the root directory of the Qt framework."
-    exit 1
-fi
-
-if [[ ! -f "$main_exe" ]]
-then
-    echo "$TITLE: Error: Main executable does not exist ($main_exe)."
-    exit 1
-fi
-
-echo "$TITLE: package root = '$pkg_root'"
-echo "$TITLE: main executable = '$main_exe'"
-echo "$TITLE: Qt framework root = '$qt_framework_root'"
-
-#-- directories (relative to root) which define the package structure
-fwdir="Frameworks"
-declare -A pkgbindir=(
-    "lib"    "lib"  # main library dir
-    "exlib"  "Library"  # external libraries dir
-    "FW"     "$fwdir"   # frameworks dir
-    "FW_qt"  "$fwdir"  # Qt framework dir
-    "FW_qt_plug"  "@MACPKG_QT_PLUGINS_DIR@"  # Qt plugins dir
-)
-
-#-- copy extra libraries to the Framework-libraries dir;
-# the name of the libraries must be the same as the library ids.
-if [[ ! -z "$extra_libs" ]]; then
-    dst="$pkg_root/$pkgbindir[exlib]"
-    echo "$TITLE: Copy extra libraries to '$dst':"
-    mkdir -p "$dst"
-    for lib in ${=extra_libs}; do
-        # eg. 'usr/local/opt/libA.9.dylib' -> '<Package>/Framworks/lib/libA.9.dylib'
-        fnm=$(dylib_id "$lib")
-        cp -fL "$lib" "$dst/${fnm##*/}"
-    done
-    unset dst
-fi
-
-#-- collect a list of binaries which are already placed in the package
-declare -a libs_init=(
-    "$main_exe"
-    "$pkg_root/$pkgbindir[lib]"/*.(so|dylib)
-    "$pkg_root/$pkgbindir[exlib]"/*.(so|dylib)
-)
-
-# add the required Qt plugins
-echo "$TITLE: Copy required Qt plugins from '$qt_framework_root':"
-pkg_plugins_dir="$pkg_root/$pkgbindir[FW_qt_plug]"
-for plg in $qt_plugins; do
-    # full path of the plugin;
-    # eg. '/opt/qt@5/plugins/platforms/libqcocoa.dylib'
-    plgpth0="$qt_framework_root/$qt_plugins_rel_dir/$plg"
-    # copy the plugin to the same directory under the _package_ plugins dir;
-    # eg. '<Package>/PlugIns/platforms/libqcocoa.dylib'
-    pth="$pkg_plugins_dir/$plg"
-    mkdir -p "${pth%/*}"
-    cp -nL "$plgpth0" "$pth"
-    # add Qt plugin to the list of initial binaries
-    libs_init+=( "$pth" )
-done
-declare -r libs_init
-
-echo "$TITLE: Initially installed binaries under '$pkg_root':"
-for fnm in $libs_init; do
-    # eg., '+ lib/libA.dylib'
-    echo "  + '${fnm#$pkg_root/}'"
-done
-
-#-- find the dependencies of the binaries
-declare -ar refs_all=( $(find_dependencies $libs_init) )
-
-echo "$TITLE: All dependencies:"
-# a sorted list of dependencies
-for lib in ${(o)refs_all}; do
-    echo "  + '$lib'"
-done
-
-#-- distinguish absolute and relative references within dependencies
-declare -a abs_refs rel_refs py_refs fw_refs
-for ref in $refs_all; do
-    if [[ $ref = @* ]]; then
-        # relative reference; eg. '@rpath/foo.dylib'
-        rel_refs+=( "$ref" )
-    elif [[ $ref = *[Pp]ython* ]] && [[ ! $ref = *boost* ]]; then
-        # Python dependencies must be considered separately (exclude 'libboost_python')
-        # eg. '/opt/py@3.9/Frameworks/Python.framework/Versions/3.9/Python'
-        py_refs+=( "$ref" )
-    elif [[ $ref = *\.[Ff]ramework/* ]]; then
-        # frameworks must be considered separately
-        # eg. '/opt/qt@5/lib/QtGui.framework/Versions/5/QtGui'
-        fw_refs+=( "$ref" )
-    else
-        # absolute reference; eg. '/usr/opt/libA.so'
-        abs_refs+=( "$ref" )
-    fi
-done
-
-#-- copy all absolute dependencies to the package
-dst="$pkg_root/$pkgbindir[exlib]"
-mkdir -p "$dst"
-echo "$TITLE: Copy external libraries to '$dst':"
-# pkglib: map{ library reference => destination full-path }
-# e.g., /usr/local/opt/libA.dylib => /pkg-root/Library/libA.dylib
-declare -A pkglib
-for ref in $abs_refs; do
-    pth="$dst/${ref##*/}"  # destination full-path
-    pkglib[$ref]="$pth"
-    cp -nL "$ref" "$pth"
-done
-unset dst
-
-#-- copy all framework dependencies to the package
-# Qt framework
-qt_fwdir="$pkg_root/$pkgbindir[FW_qt]"
-echo "$TITLE: Copy Qt-framework libraries to '$qt_fwdir':"
-# extract framework path:
-# eg. '/usr/local/opt/qt@5/lib/QtWidgets.framework/Versions/5/QtWidgets (...)'
-#   => 'QtWidgets.framework/Versions/5/QtWidgets'
-framework_re='s;.+/([^/]+\.[Ff]ramework/[^[:blank:]]+).*;\1;'
-for ref in $fw_refs; do
-    # only Qt framework is considered
-    if [[ ${ref##*/} = Qt* ]]; then
-        # eg., copy '/opt/qt@5/lib/QtGui.framework/Versions/5/QtGui'
-        # to '<Frameworks>/Qt/QtGui.framework/Versions/5/QtGui'
-        qtfwdir0=$(echo $ref | sed -E $framework_re)
-        pth="$qt_fwdir/$qtfwdir0"
-        mkdir -p "${pth%/*}"
-        pkglib[$ref]="$pth"
-        cp -nL "$ref" "$pth"
-    else
-        echo "Framework '$ref' neglected." >&2
-    fi
-done
-
-# Add relatively-referenced Qt framework;
-# e.g., QtDBus framework referenced as `@rpath/QtDBus.framework/Versions/A/QtDBus`
-echo "$TITLE: Add relatively-referenced Qt framework libraries:"
-qt_exlib_dir="$pkg_root/$pkgbindir[exlib]"
-for ref in $rel_refs; do
-    # select only Qt relative references
-    if [[ ${ref##*/} = Qt* ]]; then
-       # '@rpath/QtDBus.framework/Versions/A/QtDBus' => 'QtDBus.framework/Versions/A/QtDBus'
-       ref_qt=${ref/@rpath\//}
-       qt_src="$qt_framework_root/lib/$ref_qt"
-       qt_dst="$qt_fwdir/$ref_qt"
-       qt_dst_dir="${qt_dst%/*}"
-       pkglib["REL-$qt_dst"]="$qt_dst"
-       mkdir -p "$qt_dst_dir"
-       cp -nL "$qt_src" "$qt_dst"
-
-       # add the *1st-order optional* dependencies of the Qt library (if any)
-       # to the same folder as that of the Qt library
-       deps1=$(dylib_deps "$qt_dst" | sed -nE "s;[[:blank:]]*(/.+/opt/.+)[[:blank:]]\(.+;\1;p")
-       has_deps=0
-       for libpth_ in ${=deps1}; do
-           has_deps=1
-           # '/usr/local/opt/libA.1.2.dylib' => 'libA.1.2.dylib'
-           libname_="${libpth_##*/}"
-           # cp the library to the external library dir of the package
-           libdst="$qt_exlib_dir/$libname_"
-           pkglib[$libpth_]="$libdst"
-           cp -nL "$libpth_" "$libdst"
-       done
-    fi
-done
-
-#-- collect all package binaries for later process
-declare -ar pkgbins=( $(rm_list_duplicates ${(v)pkglib} $libs_init "$main_exe") )
-
-echo "$TITLE: All binaries:"
-# a sorted list of binaries
-for lib in ${(o)pkgbins}; do
-    echo "  + '${lib#$pkg_root/}'"
-done
-
-#-- adjust references to libraries
-echo "$TITLE: Adjust references to libraries:"
-libdir="$pkg_root/$pkgbindir[lib]"
-declare -A rpaths bin_deps
-for bin in $pkgbins; do
-    declare -A rpaths_tmp=()
-    bin_deps[$bin]=$(dylib_deps "$bin")
-    bindir="${bin%/*}"  # abs. dir of target binary
-    # abspth0 = original abs. full-path of the library
-    # abspth_pkg = abs. full-path of the library in the package
-    for abspth0 abspth_pkg in ${(kv)pkglib}; do
-        # if the binary does not depend on the current library, do nothing
-        [[ "$bin_deps[$bin]" != *"$abspth0"* ]] && continue
-        # change the library reference in the binary
-        # eg. '/usr/local/opt/lib/libA.5.dylib' => '@rpath/libA.5.dylib'
-        libname="${abspth_pkg##*/}"  # library filename
-        libdir="${abspth_pkg%/*}" # abs. dir of the referenced library
-        librelpth=""  # rel. path of the library
-        # make a framework-relative path for the Qt framework libraries
-        # eg. '/opt/qt@5/lib/QtGui.framework/Versions/5/QtGui'
-        #  => 'QtGui.framework/Versions/5'
-        if [[ $libname == Qt* ]]; then
-            # rm framework root dir from the beginning;
-            # eg. '/opt/qt@5/lib/QtGui.framework/Versions/5/QtGui'
-            #  => 'QtGui.framework/Versions/5/QtGui'
-            librelpth="${abspth_pkg#$qt_fwdir/}"
-            # rm filename from the end
-            # eg. 'QtGui.framework/Versions/5/QtGui'
-            #  => 'QtGui.framework/Versions/5'
-            librelpth="${librelpth%/*}"
-        fi
-        ref_new="$libname"
-        # prepend with library rel. path, if any
-        [[ ! -z $librelpth ]] && ref_new="$librelpth/$ref_new"
-        install_name_tool "$bin" -change "$abspth0" "@rpath/$ref_new" 2> >(grep -v "invalidate the code signature")
-        # make a proper RPATH to refer to the library within the package
-        # eg. '@loader_path/../Frameworks/Qt/'
-        rpath="@loader_path/"$(find_rpath "$bindir" "$libdir" "$librelpth")
-        rpaths_tmp[$rpath]=1
-    done
-    # store a duplicateless list of rpaths needed for the binary,
-    # only if some rpaths are set.
-    # NOTE: libraries under the package lib dir. often need
-    #   an extra RPATHs '../Library'.
-    if [[ $bin == $libdir/* ]]; then
-        rpaths_tmp[@loader_path/../$pkgbindir[exlib]]=1
-    fi
-    rpath_set="${(k)rpaths_tmp}"
-    if [[ ! -z "$rpath_set" ]] && rpaths[$bin]="$rpath_set"
-done
-
-# find the Python dependence for the *main* libraries
-libs_main=( $pkg_root/$pkgbindir[lib]/*.(dylib|so) )
-declare -a libs_pydep  # list of Python-dependent libraries
-for lib in $libs_main; do
-    # get the first element the list Python dependences (all others must be the same)
-    _pydep=$(get_python_dependence "$bin_deps[$lib]")
-    # record the list of Python-dependent libraries
-    [[ ! -z ${_pydep//[[:blank:]]/} ]] && libs_pydep+=( "$lib" )
-done
-declare -r libs_pydep
-# NOTE: zsh array indexing starts at 1 (unless option KSH_ARRAYS is set)
-_pydep=( ${=_pydep} )
-pydepends_fullpath=$_pydep[1]
-pydepends_filename=$_pydep[2]
-unset _pydep
-declare -ar py_fmwk_rpaths=( $(get_python_framework_path "$pydepends_fullpath") )
-
-for lib in $libs_pydep; do
-    rpaths[$lib]+=" $py_fmwk_rpaths[@]"
-done
-
-echo "$TITLE: Python dependence for the main libraries in '$pkg_root/$pkgbindir[lib]':"
-echo " + path: '$pydepends_fullpath'"
-echo " + library: '$pydepends_filename'"
-echo " + framework paths:"
-for pth in $py_fmwk_rpaths; do
-    echo "   - '$pth'"
-done
-
-echo "$TITLE: Add proper RPATHs to the binaries:"
-for bin in $pkgbins; do
-    rpaths_bin="${=rpaths[$bin]}"
-    # trim extra blanks
-    rpaths_bin_tr=$(echo -e "${rpaths_bin}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
-    # eg. RPATHS for 'lib/libA.dylib': ../Library , ../Frameworks/Qt
-    if [[ ! -z $rpaths_bin_tr ]]; then
-        # eg. install_name_tool libA.so -add_rpath RPATH1 -add_rpath RPATH2 ...
-        eval install_name_tool $bin -add_rpath ${rpaths_bin_tr// / -add_rpath } 2> >(grep -v "invalidate the code signature")
-    fi
-done
-
-echo "$TITLE: Done."
diff --git a/devtools/deploy/mac/mac_package.py.in b/devtools/deploy/mac/mac_package.py.in
new file mode 100644
index 0000000000000000000000000000000000000000..2e7c55a587e908f5d3cb86887c8848662df73a78
--- /dev/null
+++ b/devtools/deploy/mac/mac_package.py.in
@@ -0,0 +1,903 @@
+#  **************************************************************************  #
+#   BornAgain: simulate and fit reflection and scattering
+#
+#   @file      mac_package.py.in
+#   @brief     Adjusts the library references in the MacOS bundle to make
+#              them relocatable.
+#
+#   @homepage  http://apps.jcns.fz-juelich.de/BornAgain
+#   @license   GNU General Public License v3 or higher (see COPYING)
+#   @copyright Forschungszentrum Juelich GmbH 2016
+#   @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
+#  **************************************************************************  #
+
+# Adjust MacOS Bundle:
+# This script is called by CPack to adjust the bundle contents before making
+# the final DMG file. The package root directory is provided as an argument when
+# CPack runs the script (see `cmake/configurables/FixPack.cmake.in`).
+# The script follows the rules explained in `man dyld (1)`.
+# Furthermore, it uses MacOS native tools `otool` and `install_name_tool` to adjust the binaries.
+# It finds the dependencies of the core libraries up to a maximum level.
+# The dependencies are then copied to the corresponding package directories,
+# and library references and RPATHs are modified accordingly.
+
+# Adjust Python wheel:
+# In the 'Python mode', the script produces a Python wheel
+# (see `cmake/multipython/MakePythonWheel.cmake`).
+
+# Requires MacOS-specific native tools `otool` and `install_name_tool`.
+# NOTE: Starting with macOS Catalina (macOS 10), Macs will use `zsh` as
+# the default login shell and interactive shell across the operating system.
+# All newly created user accounts in macOS Catalina will use zsh by default;
+# see <https://support.apple.com/en-us/HT208050>
+
+# See further:
+# - Runtime linking on Mac <https://matthew-brett.github.io/docosx/mac_runtime_link.html>
+# - Apple developer documentation: 'Run-Path Dependent Libraries' <https://apple.co/3HVbMWm>
+# - Loading Dynamic Libraries on Mac <http://clarkkromenaker.com/post/library-dynamic-loading-mac>
+# - <https://stackoverflow.com/q/66268814>
+# - dlopen manpage
+# - 'Qt for macOS - Deployment' <https://doc.qt.io/qt-6/macos-deployment.html>
+# - 'Using qt.conf' <https://doc.qt.io/qt-6/qt-conf.html>
+
+# Package structure:
+# <Package-root>
+#  |
+#  +--MacOS  {includes main executable: `bornagain`}
+#  |
+#  +--lib  {main libraries, like `_libBornAgainBase.so`}
+#  |
+#  +--bin  {not used}
+#  |
+#  +--Library  {extra libraries, like `libformfactor`}
+#  |
+#  +--Frameworks  {Qt framework}
+#  |
+#  +--PlugIns  {Qt plugins)
+#  |
+#  +--Resources  {icons and `qt.conf`}
+#  |
+#  +--share
+#     |
+#     +--BornAgain-<version>
+#        |
+#        +--Examples
+#        |
+#        +--Images
+#------------------------------------------------------------------------------80
+
+import sys, os, shutil, glob, platform, re, subprocess as subp
+from collections import namedtuple
+
+TITLE="* MacOS Package"
+PKGROOT = '<PKG-ROOT>'
+
+def mkdirs(path:str):
+    if not os.path.exists(path):
+        os.makedirs(path)
+
+
+def removeprefix(str_:str, pfx:str):
+    return str_.split(pfx, 1)[-1]
+
+
+def splat(str_list:str, separator = ' '):
+    return [x for x in str_list.split(separator) if x]
+
+
+def copyfile(src:str, dst:str, overwrite=False):
+    if not overwrite and os.path.exists(dst): return
+    shutil.copy(src, dst)
+
+
+def dylib_id(libpath:str):
+    """ Returns the base library id for the given library """
+
+    return subp.check_output(["otool", "-XD", libpath], encoding='utf-8').strip()
+
+
+def rm_list_duplicates(list0:list):
+    """ Removes duplicates from a given list """
+
+    return list(set(list0))
+
+
+def dylib_deps(libpath:str):
+    """ Uses 'otool' to get the 1st-order dependencies of a library (raw output).
+    Obtains the dependencies for a given library;
+    removes the name of the library itself from the references.
+    NOTE: Under MacOS, a Mach-O binary sometimes depends on itself.
+
+    * otool output example: '  /usr/local/opt/foo.dylib (compatibility ...)'
+    """
+
+    deps = subp.check_output(["otool", "-XL", libpath], encoding='utf-8').split('\n')
+    return [d.strip() for d in deps if d]
+
+
+def find_dylibs(abspath:str):
+    """ Returns the filenames corresponding to the pattern *.so or *.dylib """
+
+    return glob.glob(abspath + '/*.so') + glob.glob(abspath + '/*.dylib')
+
+
+def get_depends1(libpath:str) -> list:
+    """ Obtains 1st-order dependencies for a given file.
+    Obtains the 'non-system' dependencies for a given library;
+    eg. '  /usr/local/opt/foo.1.dylib (compatibility ...)' => '/usr/local/opt/foo.dylib'
+    * system dependencies pattern: /System/, /Library/, /usr/lib/, /usr/local/lib
+    [See man dyld(1)]
+    """
+
+    # discard system dependencies
+    deps_ = [d for d in dylib_deps(libpath)
+             if not re.match(r'/(usr/lib|usr/local/lib|System|Library)/', d.strip())]
+    deps = list()
+    for d in deps_:
+        # extract library path
+        m_ = re.search(r'\s*([\w@/.-]+)\s+.*', d)
+        if m_: deps.append(m_.group(1))
+
+    return deps
+
+
+def common_root(abspath1:str, abspath2:str):
+    """ Finds the common root of two given _absolute_ paths """
+
+    if abspath1 == abspath2:
+        # if paths are equal, the root is equal to either of them
+        return abspath1
+
+    # convert paths to arrays of directories:
+    # replace '/' with blank and make an array out of the result;
+    # eg. '/root/lib/opt' => [root, lib, opt]
+    dirs1 = [d for d in abspath1.split('/') if d]
+    idxMax1 = len(dirs1)
+    dirs2 = [d for d in abspath2.split('/') if d]
+    idxMax2 = len(dirs2)
+    common_root = ""
+    for idx in range(min(idxMax1, idxMax2)):
+        # extract the head (topmost) directory name from paths
+        # eg. 'root/lib/opt' => 'root'
+        head1 = dirs1[idx]
+        head2 = dirs2[idx]
+
+        # if both paths have the same head, then add head to the common root;
+        # otherwise, the common root is already obtained
+        if head1 != head2: break
+        # add the common head to the root
+        common_root += '/' + head1
+
+    return common_root
+
+
+def find_rpath(bin_abspath:str, lib_abspath:str, lib_relpath:str=""):
+    """ Finds the proper rpath for given binary pointing to a reference library
+    # usage: find_rpath(bin_abspath, lib_abspath, lib_relpath)
+    # example:
+    # bin_abspath='/root/usr/opt/bin'
+    # lib_abspath='/root/usr/Frameworks/Qux/lib'
+    # lib_relpath='Qux/lib'
+    # returns: `../../Frameworks`
+    """
+
+    # drop the final '/' chars from all paths
+    bin_abspath = os.path.dirname(bin_abspath)  # target binary for which a rpath is obtained
+    lib_abspath = os.path.dirname(lib_abspath)  # referenced library
+    lib_relpath = os.path.dirname(lib_relpath)  # relative path to the referenced library
+    # find the common root path
+    root_path = common_root(bin_abspath, lib_abspath)
+    if not root_path: return lib_abspath
+
+    # obtain the path from the binary to the root
+    # eg. '/root/local/opt' => 'local/opt' => '../..'
+    _binpth_to_root = re.sub(root_path + '(/|$)', '', bin_abspath)
+    binpth_to_root = re.sub('[^/]+', '..', _binpth_to_root)
+
+    # obtain the path from root to the referenced library;
+    # eg. '/root/local/opt' => 'local/opt'
+    # then, drop the relative path of the library from the end
+    _libpth_from_root = re.sub(root_path + '(/|$)', '', lib_abspath)
+    libpth_from_root = re.sub('(^|/)' + lib_relpath + '$', '', _libpth_from_root)
+
+    # return the proper relative RPATH to the referenced library
+    # eg. '../../Frameworks/Qt'
+    libpth_from_bin = libpth_from_root if not binpth_to_root \
+        else binpth_to_root + '/' + libpth_from_root
+    # return eg. '@loader_path/../../Frameworks'
+    return "@loader_path/" + libpth_from_bin
+
+
+class PythonFramework:
+    """ Python framework details """
+
+    # regexp to extract the Python dependence;
+    # eg., '/Frameworks/Python.framework/Versions/3.9/Python'
+    libdir_re = r'[\w./@.-]+'
+    pylibname_re = r'Python|libpython.+\.dylib'
+    py_fmwk_re = re.compile('(' + libdir_re + ')/(' + pylibname_re + ')')
+
+    # regexp to extract the Python framework path
+    # eg. '/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/Python'
+    #   => '/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/'
+    py_fmwk_path_re = r'(.*)/(Python|libpython).*'
+    # possible framework paths on MacOS
+    framework_paths = ("/usr/local/Library/Frameworks",
+                       "/Library/Frameworks", "/usr/local/Frameworks",
+                       "/opt/homebrew/Frameworks")
+
+    @staticmethod
+    def dependence(dependencies:list) -> (str, str):
+        """ Extracts the Python dependency from the given dependencies
+        # NOTE: Under OSX, Python's library name is either 'Python', or
+        # for Python3.9, 'libpython3.9.dylib'.
+        """
+
+        # regexp to correct the Python dependence; eg.:
+        # '/Users/usr1/.pyenv/versions/3.9.18/lib/libpython3.9.dylib' => 'libpython3.9.dylib'
+        # '/Frameworks/Python.framework/Versions/3.9/Python' => 'libpython3.9.dylib'
+        # '/Frameworks/Python.framework/Versions/3.9/libpython3.9.dylib' => 'libpython3.9.dylib'
+        # obtain the dependencies
+        pydepends_path_list = [d for d in dependencies if PythonFramework.py_fmwk_re.match(d)]
+        pydepends_path = pydepends_filename = None
+        if pydepends_path_list:
+            pydepends_filename = pydepends_path_list[0]
+            mtch = PythonFramework.py_fmwk_re.match(pydepends_filename)
+            pydepends_path = mtch[1]
+            pydepends_filename = mtch[2]
+        # return the Python dependence fullpath and filename
+        return pydepends_path, pydepends_filename
+
+    @staticmethod
+    def framework_path(pydepends_path:str) -> (str, str):
+        """ Produces proper Python framework paths for a given Python dependency """
+
+        # extract the Python framework path
+        py_fmwk_dir = re.match(PythonFramework.py_fmwk_path_re, pydepends_path, re.IGNORECASE)[1]
+
+        # when the library is like '.../Versions/3.9/Python', then
+        # add an extra '/lib' to the framework path;
+        # this is needed since it refers to the standard location of the
+        # Python shared library on OSX, '.../Versions/3.9/lib/libpython39.dylib'.
+        if pydepends_path.endswith('Python'):
+            py_fmwk_dir += '/lib'
+
+        # regexp to extract the Python version; eg. '3.9'
+        pyversion = None
+        pyversion_m = re.match(r'.+versions/([0-9.]+).*', pydepends_path, re.IGNORECASE)
+        if pyversion_m:
+            pyversion = pyversion_m[1]
+        else:
+            pyversion_m = re.match(r'.+/libpython(.+)\.dylib', pydepends_path)
+            if pyversion_m:
+                pyversion = pyversion_m[1]
+        # '3.9.18' => '3.9'
+        pyversion = re.match(r'(\d+.\d+).*', pyversion)[1]
+        if not pyversion:
+            raise ValueError("Cannot extract Python version from path '%s'"
+                             % pydepends_path)
+        # RPATHs corresponding to the common OSX framework paths
+        py_fmwk_basepath = "Python.framework/Versions/" + pyversion + "/lib"
+        # collect proper RPATHs for Python framework
+        py_fmwk_rpaths = [py_fmwk_dir]
+        for pth in PythonFramework.framework_paths:
+            py_fmwk_rpaths.append(pth + '/' + py_fmwk_basepath)
+        # return a list of possible framework paths
+        return py_fmwk_rpaths, pyversion
+
+    @staticmethod
+    def make_wheel(python_exe, py_output_dir, wheel_dir):
+        # make the Python wheel
+        subp.check_output([python_exe, '-m', 'pip', 'wheel', py_output_dir,
+                           '--no-deps', '--wheel', wheel_dir])
+
+class dylibRPATHs:
+    """ Obtains the RPATHs of a library """
+
+    # extract the LC_RPATH sections from the output of `otool -l` command
+    LC_RPATH_rx = re.compile(r'.+RPATH.*\n.+\n.+\n')
+    # extract 'rpath' from a LC_RPATH section
+    rpath_rx = re.compile(r'path\s+([\w@/.-]+)\s+.*')
+    # aux. dictionary to find the source file from a library reference
+    libId_src_tbl = dict()
+
+    @staticmethod
+    def find(libpath:str) -> tuple:
+        """ Returns the RPATHs for the given library """
+        otool_output = subp.check_output(["otool", "-l", libpath], encoding='utf-8')
+        # 'raw' rpaths which might include '@loader_path'
+        _rpaths = dylibRPATHs.rpath_rx.findall(
+            '\n'.join(dylibRPATHs.LC_RPATH_rx.findall(otool_output)))
+        return tuple(r.rstrip('/') for r in _rpaths)
+
+    @staticmethod
+    def resolve(rpathref:str, rpaths:list):
+        """ resolve a given rpath-reference to a list of possible paths
+        according to a given list of RPATHs """
+        return tuple(rpathref.replace('@rpath', p) for p in rpaths) if rpaths else tuple()
+
+    @staticmethod
+    def resolve_by_table(ref:str):
+        """ resolve a given rpath-reference to a table of reference => path """
+        return dylibRPATHs.libId_src_tbl.get(ref, '')
+
+
+class LibraryFile:
+    """ Library binary file """
+
+    def __init__(self, dtype:str='', src:str='', ref:str='', rpaths:list=None,
+                 dst:str='', ID:str='', dependencies:set=None, parent=None):
+        self.dtype = dtype  # dependency type
+        self.src = src   # path to the library file
+        self.ref = ref    # reference to the library
+        self.rpaths = rpaths if rpaths else set()  # rpaths of the library
+        self.dst = dst    # destination of the library in the package
+        self.ID = ID  # library ID
+        self.ID_new = '' # new library ID
+        # list of dependencies of the library
+        self.dependencies = dependencies if dependencies else set()
+        self.parent = parent
+
+    def follow(self):
+        """ Determines if the library dependencies should be followed """
+
+        # do not follow Python dependencies
+        ref_basename = os.path.basename(self.ref).lower()
+        if 'python' in ref_basename and 'boost' not in ref_basename:
+            return False
+        # follow only libraries for which there is an absolute source path
+        return self.src.startswith('/')
+
+    @staticmethod
+    def find_source(lib, parent_lib):
+        """ Finds the absolute path to the source, as far as possible """
+
+        if lib.ref.startswith('@rpath') and parent_lib:
+            if parent_lib.rpaths:
+                parent_lib_dir = os.path.dirname(parent_lib.src)
+                rpaths_fixed = tuple(os.path.realpath(r.replace('@loader_path', parent_lib_dir))
+                                     for r in parent_lib.rpaths)
+                # TODO: store fixed RPATHs
+                lib.src = next(
+                    filter(os.path.isfile, dylibRPATHs.resolve(lib.ref, rpaths_fixed)), '')
+        elif lib.ref.startswith('@loader_path'):
+            # relative reference; eg. '@rpath/foo.dylib'
+            lib.dtype = 'rel@loader_path'
+            lib.src = (os.path.dirname(parent_lib.src) + '/'
+                       + removeprefix(lib.ref, '@loader_path'))
+        elif lib.ref.startswith('/'):
+            # absolute path
+            lib.src = lib.ref
+
+        # last attempt
+        if not lib.src: lib.src = dylibRPATHs.resolve_by_table(lib.ref)
+        # find the realpath of the source
+        if lib.src: lib.src = os.path.realpath(lib.src)
+        return lib.src
+
+    @staticmethod
+    def dependency_type(lib):
+        """ Finds the dependency type """
+
+        if lib.dtype: return lib.dtype
+        dep = lib.ref
+        dep_basename_l = os.path.basename(dep).lower()
+        if 'python' in dep_basename_l and 'boost' not in dep_basename_l:
+            # Python dependencies must be considered separately (exclude 'libboost_python')
+            # eg. '/opt/py@3.9/Frameworks/Python.framework/Versions/3.9/Python'
+            lib.dtype = 'framework_py'
+        elif '.framework/' in dep.lower() and os.path.basename(dep).startswith('Qt'):
+            lib.dtype = 'framework_qt'
+        elif dep.startswith('@loader_path'):
+            # relative reference; eg. '@rpath/foo.dylib'
+            lib.dtype = 'rel@loader_path'
+        elif dep.startswith('@rpath'):
+            # relative reference; eg. '@rpath/foo.dylib'
+            lib.dtype = 'rel@rpath'
+        elif '.framework/' in dep.lower():
+            # Frameworks must be considered separately
+            # eg. '/opt/qt@6/lib/QtGui.framework/Versions/6/QtGui'
+            lib.dtype = 'framework'
+        elif dep.startswith('/'):
+            # absolute reference; eg. '/usr/opt/libA.so'
+            lib.dtype = 'abs'
+        else:
+            lib.dtype = 'unknown'
+
+        return lib.dtype
+
+    def __str__(self):
+        return ("LibraryFile: dtype=%s, src=%s, ref=%s,"
+                "rpaths=%s, dst=%s, ID=%s, dependencies=%s, parent-ref=%s"
+                % (self.dtype, self.src, self.ref, self.rpaths, self.dst, self.ID,
+                   self.dependencies, self.parent.ref if self.parent else None))
+
+    def __repr__(self):
+        return self.__str__()
+
+
+class Settings:
+    """ MacOS package configuration """
+
+    # TODO: make intermediate variables internal
+
+    # Python-related variables
+    PyInfo = namedtuple("PythonInfo", ["dir", "lib", "version", "rpaths"])
+
+    def __init__(self, pkg_root,
+                 main_exe_src, core_libs, extra_libs,
+                 python_mode, pkg_py_root_dir, pkg_py_lib_dir, pkg_py_extra_lib_dir,
+                 qt_framework_root, qt_framework_plugins_dir, qt_plugins_dir, qt_plugins):
+
+        # map {reference/abs. path => LibraryFile}
+        self.libs = dict()
+        # package-related directories
+        fwdir = "Frameworks"
+        self.dirs = {
+            #-- absolute paths
+            "root": pkg_root, # package root dir
+            "qt_root": qt_framework_root,
+            "qt_root_plugins_dir": qt_framework_plugins_dir,
+            #-- relative paths
+            "exe": "MacOS", # main executable dir
+            "corelib": "lib",  # core library dir
+            "exlib": "Library",  # external libraries dir
+            "FW": fwdir,  # frameworks dir
+            "FW_qt": fwdir + '/Qt',  # Qt framework dir
+            "FW_qt_plugin": qt_plugins_dir,  # Qt plugins dir
+        }
+
+        # Python-package mode
+        self.python_mode = bool(python_mode)
+        if self.python_mode:
+            self.dirs['root'] = pkg_py_root_dir
+            self.dirs['corelib'] = removeprefix(pkg_py_lib_dir, pkg_py_root_dir + '/')
+            core_libs.extend(find_dylibs(pkg_py_lib_dir))
+            self.dirs['exlib'] = removeprefix(pkg_py_extra_lib_dir, pkg_py_root_dir + '/')
+            extra_libs.extend(find_dylibs(pkg_py_extra_lib_dir))
+        else:
+            # main executable
+            _main_exe_dst = self.dirs['root'] + '/' + self.dirs['exe'] \
+                + '/' + os.path.basename(main_exe_src)
+            self.libs = {main_exe_src:
+                         LibraryFile(dtype='main_exe', src=main_exe_src, dst=_main_exe_dst)}
+            #-- Qt plugins
+            self.libs.update({self.dirs['qt_root_plugins_dir'] + '/' + l:
+                              LibraryFile(dtype='plugin_qt', src=l) for l in qt_plugins})
+
+        # core libraries
+        self.libs.update({l: LibraryFile(dtype='core_lib', src=l) for l in set(core_libs)})
+        # extra libraries
+        self.libs.update({l: LibraryFile(dtype='extra_lib', src=l) for l in set(extra_libs)})
+        # all initial libraries
+        self.libs_init = tuple(self.libs.keys())
+        # map {source-path => destination-path}
+        self.src_dst_tbl = dict()
+        # map {source-path => destination-path}
+        self.copy_tbl = dict()
+        # map {destination-path => LibraryFile}
+        self.dst_tbl = dict()
+        # map {reference => destination-path}
+        self.ref_dst_tbl = dict()
+        # map {reference_initial => reference_new}
+        self.ref_tbl = dict()
+        # Python-related info
+        self.py:Settings.PyInfo = None
+
+    @staticmethod
+    def _dependencies_at_level(libs_cur:dict, pkg_libs:dict) -> dict:
+        """ Obtains all dependencies at the current level """
+
+        deps_lv:dict[(str, LibraryFile)] = dict()  # map {ref => parent-LibraryFile}
+        for lib_ref, parent_lib in libs_cur.items():
+            # if not at the root level (i.e. library has parent) and
+            # library has been already discovered, then
+            # neglect the library to avoid infinite loops
+            if parent_lib and lib_ref in pkg_libs: continue
+
+            lib = pkg_libs.setdefault(lib_ref, LibraryFile())
+            lib.ref = lib_ref
+            lib.parent = parent_lib
+            # determine the type of the library and its source path
+            if not lib.src: LibraryFile.find_source(lib, parent_lib)
+            LibraryFile.dependency_type(lib)
+            # consider only absolute paths for the next level
+            if not lib.follow(): continue
+            # verify the existence of the file
+            if not os.path.isfile(lib.src):
+                raise FileNotFoundError("%s: dependency does not exist: '%s'"
+                                        % (TITLE, lib.src))
+            # find the 1st-level dependencies of the library
+            lib.dependencies = get_depends1(lib.src)
+            # find RPATH references of the library
+            lib.rpaths = set(dylibRPATHs.find(lib.src))
+            # find library ID; eg., '@rpath/local/libA.dylib'
+            lib.ID = dylib_id(lib.src)
+            # add the library ID to the ID-table
+            dylibRPATHs.libId_src_tbl[lib.ID] = lib.src
+            # add the library and its parent for the next iteration
+            deps_lv.update({d: lib for d in lib.dependencies})
+
+        return deps_lv
+
+    @staticmethod
+    def dependencies(pkg_libs:dict, pkg_dirs:dict, libs_init:list, level_max:int):
+        """ Gathers all dependencies for the given initial libraries
+        up to a maximum (but arbitrary) level.
+        """
+
+        print("%s: Find dependencies (up to level %s)..." % (TITLE, level_max))
+        # root level libraries/binaries
+        for ref, pkglib in pkg_libs.items():
+            pkglib.parent = None
+            pkglib.src = ref
+
+        # map {library-id => library source path}
+        dylibRPATHs.libId_src_tbl = {dylib_id(l): l for l in libs_init}
+        # find dependencies of libraries recursively,
+        # beginning from the initial libraries
+        level = 0 # current level nr.
+        libs_lv = {l: None for l in pkg_libs.keys()}  # map {library ref => parent}
+        while libs_lv:
+            level += 1
+            # avoid going infinitely deep (eg. due to some mistake)
+            if level > level_max:
+                print("%s: Dependency level %i exceeds the maximum allowed depth (%i)."
+                      % (TITLE, level, level_max))
+                break
+            # obtain all dependencies at the current level
+            print("==>[L%i]" % level)
+            for ref, parent_lib in libs_lv.items():
+                print("[L%i] %s <= %s"
+                      % (level, ref.replace(pkg_dirs['root'], PKGROOT),
+                         parent_lib.src.replace(pkg_dirs['root'], PKGROOT)
+                         if parent_lib and parent_lib.src else 'None'))
+            # parents for the next iteration
+            libs_lv = Settings._dependencies_at_level(libs_lv, pkg_libs)
+
+            # TODO:
+            # if no source file is found for a ref., make another attempt
+            # to find the source via the reference table
+            # for lib in (l for l in libs_lv.values() if l and not l.src):
+            #     lib.src = dylibRPATHs.resolve_by_table(lib.ref)
+        print("==>END.")
+        return pkg_libs
+
+    @staticmethod
+    def destinations(pkg_libs, pkg_dirs, libs_init) -> (dict, dict):
+        """ Determines destinations for the binaries in the package """
+
+        # regex to obtain Qt framework subdirectory
+        # eg. "/usr/local/Cellar/qt/6.5.1_3/lib/QtWidgets.framework/Versions/A/QtWidgets"
+        #     => ['QtWidgets.framework', '/Versions/A/QtWidgets']
+        qt_fw_rpath_rx = re.compile(r'.+/(Qt\w+.framework)/(.+)', re.IGNORECASE)
+        # Qt framework destination
+        pkg_qt_framework_dir = pkg_dirs['root'] + '/' + pkg_dirs['FW_qt']
+        # Qt plugins destination
+        pkg_qt_plugins_dir = os.path.realpath(pkg_dirs['root'] + '/' + pkg_dirs['FW_qt_plugin'])
+
+        # extra libraries destination
+        # eg. 'usr/local/opt/libA.9.dylib' => '<Package>/Library/libA.9.dylib'
+        # the name of the libraries must be the same as the library ids
+        pkg_exlib_dir = os.path.realpath(pkg_dirs['root'] + '/' + pkg_dirs['exlib'])
+
+        # core libraries destination
+        # eg. 'buildir/lib/CoreLib.so' => '<Package>/lib/CoreLib.so'
+        # the name of the libraries must be the same as the library ids
+        pkg_corelib_dir = os.path.realpath(pkg_dirs['root'] + '/' + pkg_dirs['corelib'])
+        # map {source-path => destination-path}
+        pkg_copy_tbl = {p.src: p.dst for p in pkg_libs.values() if p.dst}
+        # map {destination-path => LibraryFile}
+        pkg_dst_tbl = dict()
+
+        for ref, lib in pkg_libs.items():
+            # neglect libraries which have no source path or
+            # those which have already a destination
+            if not lib.src: continue
+            if lib.dst:
+                pkg_dst_tbl[lib.dst] = lib
+                continue
+
+            basename_new = os.path.basename(lib.ID)
+
+            # make new library ids:
+            # eg. '/usr/opt/lib/libtiff.6.dylib' => '@rpath/libtiff.6.dylib'
+            # '@loader_path/../lib/libicudata.73.dylib' => '@rpath/libicudata.73.dylib'
+            if basename_new: lib.ID_new = "@rpath/" + basename_new
+            # Qt framework libraries
+            if lib.dtype == 'framework_qt':
+                _mg = qt_fw_rpath_rx.match(lib.src).groups()
+                # eg. destination = <PKG-ROOT>/Frameworks/qt/QtWidgets.framework/Versions/A/QtWidgets"
+                _libID = _mg[0] + '/' + basename_new
+
+                # eg. '/opt/qt/QtCore.framework/Versions/A/QtCore' =>
+                #     '@rpath/QtCore.framework/Versions/A/QtCore'
+                lib.ID_new = "@rpath/" + _libID
+                lib.dst = pkg_qt_framework_dir + '/' + _libID
+            elif lib.dtype == 'plugin_qt':
+                # full source path of the plugin; eg. '/opt/qt@5/plugins/platforms/libqcocoa.dylib'.
+                # full destination path of the plugin (same directory under the _package_ plugins dir);
+                # eg. '<Package>/PlugIns/platforms/libqcocoa.dylib'
+                lib.dst = pkg_qt_plugins_dir + '/' \
+                    + removeprefix(lib.src, pkg_dirs['qt_root_plugins_dir'] + '/')
+            elif lib.dtype == 'core_lib':
+                lib.dst = pkg_corelib_dir + '/' + basename_new
+            elif lib.dtype == 'extra_lib':
+                lib.dst = pkg_exlib_dir + '/' + basename_new
+            # dependencies with absolute path
+            elif lib.dtype == 'abs':
+                lib.dst = pkg_exlib_dir + '/' + basename_new
+            # dependencies with relative '@loader_path' reference for which a source path is found
+            elif lib.dtype == 'rel@loader_path':
+                if lib.src in pkg_copy_tbl:
+                    lib.dst = pkg_copy_tbl[lib.src]
+                else:
+                    lib.dst = pkg_exlib_dir + '/' + basename_new
+            # dependencies with '@rpath' reference for which a source path is found
+            elif lib.dtype == 'rel@rpath':
+                if lib.src in pkg_copy_tbl:
+                    lib.dst = pkg_copy_tbl[lib.src]
+                else:
+                    lib.dst = pkg_exlib_dir + '/' + basename_new
+
+            if not lib.dst: continue
+            # verify that when the source is the same, the destinations is the same, regardless of the ref.
+            if lib.src in pkg_copy_tbl and lib.dst != pkg_copy_tbl[lib.src]:
+                raise AssertionError("Multiple destinations found for a single source file"
+                    "'%s': %s != %s" % (lib.src, lib.dst, pkg_copy_tbl[lib.src]))
+
+            pkg_copy_tbl[lib.src] = lib.dst
+            pkg_dst_tbl[lib.dst] = lib
+
+        return pkg_copy_tbl, pkg_dst_tbl
+
+    @staticmethod
+    def reference_destinations(pkg_libs:dict) -> dict:
+        """ Determines destinations for the references """
+
+        # map {basename => destination-path}
+        pkg_basename_dst_tbl = {os.path.basename(p.dst): p.dst
+                                for p in pkg_libs.values() if p.dst}
+        # map {reference => destination-path}
+        pkg_ref_dst_tbl = dict()
+        for lib in pkg_libs.values():
+            for dep in lib.dependencies:
+                # attempt to obtain the destination from package library
+                dep_lib = pkg_libs.get(dep)
+                # ignore Python framework files
+                if dep_lib and dep_lib.dtype == "framework_py": continue
+                _dst = dep_lib.dst if dep_lib else None
+                # otherwise, attempt to find the destination upon the basename
+                pkg_ref_dst_tbl[dep] = _dst \
+                    or pkg_basename_dst_tbl.get(os.path.basename(dep)) or ''
+
+        return pkg_ref_dst_tbl
+
+    @staticmethod
+    def new_references_rpaths(pkg_libs:dict, pkg_dst_tbl:dict, pkg_ref_dst_tbl:dict) -> (dict, tuple):
+        """ Determines proper RPATHs for each library """
+
+        pkg_ref_tbl = dict() # map {reference_initial => reference_new}
+        pkg_dst_lib = {l.dst: l for l in pkg_dst_tbl.values()} # map {destination path => LibraryFile}
+
+        #-- determine proper Python RPATHs
+        # find all Python framework binaries
+        py_dir = py_lib = py_version = py_rpaths = None
+        py_framework_files = {l.ref: l for l in pkg_libs.values()
+                              if l.dtype == "framework_py"}
+
+        py_dir, py_lib = PythonFramework.dependence(py_framework_files.keys())
+        py_rpaths, py_version = PythonFramework.framework_path(py_dir + '/' + py_lib)
+        for ref in py_framework_files.keys():
+            pkg_ref_tbl[ref] = "@rpath/" + py_lib
+
+        #-- determine all proper RPATHs for package binaries
+        for ref, lib in ((r, l) for r, l in pkg_libs.items() if l.dst):
+            rpaths_ini = set(lib.rpaths)  # initial RPATHs
+            rpaths_new = set()  # new RPATHs
+            # record references conversions
+            if lib.ID_new: pkg_ref_tbl[ref] = lib.ID_new
+            for dep in lib.dependencies:
+                # add Python-related RPATHs
+                if re.match(PythonFramework.py_fmwk_path_re, dep):
+                    rpaths_new.update(py_rpaths)
+                # add RPATHs related to other libraries within the package
+                _dep_dst = pkg_ref_dst_tbl.get(dep)
+                if not _dep_dst: continue
+                # find the proper RPATH
+                _rel_path = pkg_dst_lib[_dep_dst].ID_new.replace("@rpath/", "")
+                _rpth = find_rpath(lib.dst, _dep_dst, _rel_path)
+                rpaths_new.add(_rpth.rstrip('/'))
+
+            # new RPATHs should be different from the initial ones
+            lib.rpaths = rpaths_new.difference(rpaths_ini)
+
+        return pkg_ref_tbl, (py_dir, py_lib, py_version, py_rpaths)
+
+#------------------------------------------------------------------------------80
+
+_core_libs = "@MACPKG_CORE_LIBS@"  # core libraries and executables
+core_libs = splat(_core_libs, ';')
+_extra_libs = "@MACPKG_EXTRA_LIBS@"
+extra_libs = splat(_extra_libs, ';')
+
+main_exe_src = "@MACPKG_MAIN_EXE@"
+pkg_root = ""
+if len(sys.argv) > 1:
+    # root dir, eg. '/tmp/bornagain.app/Contents'
+    pkg_root = os.path.dirname(sys.argv[1] + '/')
+
+# Python-related variables
+python_mode = bool("@BA_PY_MODE@")
+python_exe = "@__Py3_EXECUTABLE@"
+pkg_py_output_dir = os.path.dirname("@BA_PY_OUTPUT_DIR@/")
+pkg_py_root_dir = os.path.dirname("@BA_PY_INIT_OUTPUT_DIR@/")
+pkg_py_lib_dir = os.path.dirname("@BA_PY_LIBRARY_OUTPUT_DIR@/")
+pkg_py_extra_lib_dir = os.path.dirname("@BA_PY_EXTRA_LIBRARY_OUTPUT_DIR@/")
+pkg_py_wheel_dir = os.path.dirname("@BA_PY_PACKAGE_WHEEL_DIR@/")
+
+# Qt-related variables
+# eg. input Qt dir = '/usr/local/opt/qt@5/lib/cmake/Qt'
+#   => '/usr/local/opt/qt@5'
+qtdir = "@Qt_DIR@/"
+# Qt plugins paths relative to Qt root dir
+qt_plugins_rel_dir = "@MACPKG_QT_PLUGINS_RELDIR@/"
+qt_framework_root = os.path.dirname(qtdir)
+qt_framework_plugins_dir = os.path.dirname(qt_framework_root + '/' + qt_plugins_rel_dir)
+
+# list of required Qt plugins
+_qt_plugins = "@MACPKG_QT_PLUGINS@"
+qt_plugins = splat(_qt_plugins, ';')
+qt_plugins_dir = "@MACPKG_QT_PLUGINS_DIR@/"
+
+if python_mode:
+    if not python_exe:
+        raise ValueError("%s: Error: Provide the Python executable." % TITLE)
+
+    if not pkg_py_root_dir:
+        raise ValueError("%s: Error: Provide the root directory of the Python package." % TITLE)
+
+    if not extra_libs:
+        core_libs = find_dylibs(pkg_py_lib_dir)
+
+    pkg_root = pkg_py_root_dir
+else:
+    if not pkg_root:
+        raise ValueError("%s: Error: Provide the root directory of the package." % TITLE)
+
+    if not main_exe_src:
+        raise ValueError("%s: Error: Main executable does not exist." % TITLE)
+
+#------------------------------------------------------------------------------80
+
+print("-- Package variables --")
+print("%s: package root = '%s'" % (TITLE, pkg_root))
+print("%s: main executable = '%s'" % (TITLE, main_exe_src))
+print("%s: core libraries = '%s'" % (TITLE, core_libs))
+print("%s: extra libraries = '%s'" % (TITLE, extra_libs))
+print("-- Python")
+print("%s: Python mode = %s" % (TITLE, python_mode))
+print("%s: Python executable = '%s'" % (TITLE, python_exe))
+print("%s: Python package root dir = '%s'" % (TITLE, pkg_py_root_dir))
+print("%s: Python package libraries dir = '%s'" % (TITLE, pkg_py_lib_dir))
+print("%s: Python package extra libraries dir = '%s'" % (TITLE, pkg_py_extra_lib_dir))
+print("%s: Python wheel dir = '%s'" % (TITLE, pkg_py_wheel_dir))
+print("-- Qt")
+print("%s: Qt framework root = '%s'" % (TITLE, qt_framework_root))
+print("%s: Qt plugings dir = '%s'" % (TITLE, qt_plugins_dir))
+print("%s: Qt plugings = '%s'" % (TITLE, qt_plugins))
+print("-----------------------")
+
+#-- package definition
+pkg = Settings(pkg_root,
+               main_exe_src, core_libs, extra_libs,
+               python_mode, pkg_py_root_dir, pkg_py_lib_dir, pkg_py_extra_lib_dir,
+               qt_framework_root, qt_framework_plugins_dir, qt_plugins_dir, qt_plugins)
+
+#-- print package information
+print("\n%s: package directories:" % TITLE)
+for lbl, val in pkg.dirs.items():
+    print("  %s: %s" % (lbl, val))
+
+print("\n%s: Initial binaries:" % TITLE)
+for fnm in pkg.libs_init:
+    # eg., '+ lib/libA.dylib'
+    print("  + '%s'" % removeprefix(fnm, pkg.dirs['root'] + '/'))
+
+print()
+
+#-- find the dependencies of the binaries
+pkg.libs = Settings.dependencies(pkg.libs, pkg.dirs, pkg.libs_init, level_max=10)
+
+#-- determine the destination of libraries in the package
+# map {source-path => destination-path}
+pkg.src_dst_tbl, pkg.dst_tbl = Settings.destinations(pkg.libs, pkg.dirs, pkg.libs_init)
+
+#-- print all discovered dependencies
+print("\n%s: All dependencies (type, source, reference):" % TITLE)
+for lib in sorted(pkg.libs.values(), key=lambda l: (l.dtype, l.src)):
+    print("  + [%s]: %s {%s}" %
+          (lib.dtype,
+           lib.src.replace(pkg_root, PKGROOT),
+           lib.ref.replace(pkg_root, PKGROOT)))
+
+print("\n%s: Installation table (source, destination):" % TITLE)
+for src in sorted(pkg.src_dst_tbl.keys()):
+    dst = pkg.src_dst_tbl.get(src)
+    print("  + %s => %s" %
+          (src, dst.replace(pkg_root, PKGROOT) if dst else 'None'))
+
+#-- determine proper RPATHs for each library
+print("\n%s: Building reference-destination table..." % TITLE)
+pkg.ref_dst_tbl = Settings.reference_destinations(pkg.libs)
+for ref, dst in pkg.ref_dst_tbl.items():
+    if not dst:
+        raise AssertionError("%s: reference '%s' is not resolved" % (TITLE, ref))
+    print("  + {%s} => %s" % (ref, dst.replace(pkg_root, PKGROOT)))
+
+pkg.ref_tbl, py_info = Settings.new_references_rpaths(pkg.libs, pkg.dst_tbl, pkg.ref_dst_tbl)
+pkg.py = Settings.PyInfo(*py_info)
+
+print("\n%s: Python dependence of package libraries:" % TITLE)
+if pkg.py.lib:
+    print("  + path: '%s'" % pkg.py.dir)
+    print("  + library: '%s'" % pkg.py.lib)
+    print("  + version: '%s'" % pkg.py.version)
+    print("  + framework paths:")
+    for pth in pkg.py.rpaths:
+        print("   - %s" % pth)
+else:
+    print("  No Python dependence found.")
+
+print("\n%s: Reference conversion table (ref_old, ref_new):" % TITLE)
+for ref_old, ref_new in pkg.ref_tbl.items():
+    print("  + %s => %s" % (ref_old, ref_new))
+
+print("\n%s: Library RPATHs (source, RPATHs):" % TITLE)
+for lib in (l for l in pkg.dst_tbl.values() if l.rpaths):
+    print("  + %s: %s" % (lib.src.replace(pkg_root, PKGROOT),
+                        lib.rpaths))
+
+#-- copy binaries to their package destinations
+print("\n%s: Install package files..." % TITLE)
+for dst, lib in pkg.dst_tbl.items():
+    mkdirs(os.path.dirname(dst))
+    copyfile(lib.src, dst)
+
+#-- apply RPATHs
+print("\n%s: Add proper RPATHs to the binaries..." % TITLE)
+for lib in pkg.dst_tbl.values():
+    # eg. RPATHS for 'lib/libA.dylib': ../Library , ../Frameworks/Qt
+    # eg. install_name_tool libA.so -add_rpath RPATH1 -add_rpath RPATH2 ...
+    _cmd_list = []
+    for cmd in (["-add_rpath", r] for r in lib.rpaths):
+        _cmd_list.extend(cmd)
+    # execute the command
+    if not _cmd_list: continue
+    _prc = subp.run(["install_name_tool", lib.dst, *_cmd_list])
+    if _prc.returncode != 0:
+        print("%s: Command failed: %s" % (TITLE, _prc.args))
+
+# modify the library references;
+# eg. '/usr/local/opt/foo.dylib' => '@rpath/foo.dylib'
+print("\n%s: Change library references in binaries..." % TITLE)
+for lib in pkg.dst_tbl.values():
+    # eg. install_name_tool libA.so -change REF1_old REF1_new -change REF2_old REF2_new ...
+    _cmd_list = []
+    for ref_old in lib.dependencies:
+        ref_new = pkg.ref_tbl[ref_old]
+        if ref_old != ref_new: _cmd_list.extend(["-change", ref_old,ref_new])
+    # execute the command
+    if not _cmd_list: continue
+    _prc = subp.run(["install_name_tool", lib.dst, *_cmd_list])
+    if _prc.returncode != 0:
+        print("%s: Command failed: %s" % (TITLE, _prc.args))
+
+#-- make Python wheel (when in Python mode)
+if pkg.python_mode:
+    print("%s: creating Python wheel in '%s'..." % (TITLE, pkg_py_wheel_dir))
+    print("  Python executable = '%s'" % python_exe)
+    PythonFramework.make_wheel(python_exe, pkg_py_output_dir, pkg_py_wheel_dir)
+
+#-- END
+print("\n%s: Done.\n" % TITLE)
diff --git a/devtools/deploy/mac/mk_pypack_macos.zsh.in b/devtools/deploy/mac/mk_pypack_macos.zsh.in
index 7eb01138503bbb1e29fd230b53d83de3f227fc8f..990aa24efd421220e8a5a075e8fa9e1aa7cf4273 100644
--- a/devtools/deploy/mac/mk_pypack_macos.zsh.in
+++ b/devtools/deploy/mac/mk_pypack_macos.zsh.in
@@ -56,6 +56,11 @@
 # To obtain the value of the config variable use:
 # % brew config | sed -nE 's;.*HOMEBREW_PREFIX:[[:blank:]]*(.+)[[:blank:]]*;\1;p'
 
+# include shell helper functions
+# (expected to be in the same folder as the current script)
+source "@CMAKE_SOURCE_DIR@/devtools/deploy/mac/shutils.zsh"
+#========================================
+
 # externally given variables
 pyoutdir="@BA_PY_OUTPUT_DIR@"
 libdir="@BA_PY_LIBRARY_OUTPUT_DIR@"
@@ -122,24 +127,7 @@ lib_id_re='s;.+name[[:blank:]]+.+/('$libname_re')[[:blank:]]+\(.+;\1;p'
 function dylib_base_id
 {
     # return the base library id for the given library ($1)
-    otool -l "$1" | grep -A2 -F LC_ID_DYLIB | sed -nE $lib_id_re
-}
-
-# use otool to get the 1st-order dependencies of a library (raw output)
-function dylib_deps
-{
-    # obtain the dependencies for a given library ($1);
-    # remove the name of the library itself from the references.
-    # Under MacOS, a Mach-O binary sometimes depends on itself.
-    # otool output example: '  /usr/local/opt/foo.dylib (compatibility ...)'
-    local basename="${1##*/}"
-    otool -XL "$1" | grep -vF "$basename"
-}
-
-function rm_list_duplicates
-{
-    # remove duplicates from a given list, $@; (separator is a space char)
-    echo $@ | tr -s ' ' '\n' | sort -u | tr '\n' ' '
+    otool -XD "$1" | sed -nE $lib_id_re
 }
 
 # get dependencies of a given library on BornAgain libraries
@@ -176,7 +164,7 @@ function get_python_dependence
     # regexp to correct the Python dependence; eg.:
     # '/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/Python' => 'libpython3.9.dylib'
     # '/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/libpython3.9.dylib' => 'libpython3.9.dylib'
-    pylib_re='s;.*[pP]ython.+[Vv]ersions/([0-9.]+).+(Python|libpython).*;libpython\1.dylib;'
+    pylib_re='s;.+/(Python|libpython)([0-9.]+)\.dylib;libpython\2.dylib;'
     # obtain the dependencies
     pydeps0=$(dylib_deps "$1")
     pydepends_fullpath=$(echo "$pydeps0" | sed -nE $py_fmwk_re)
@@ -186,43 +174,10 @@ function get_python_dependence
 }
 
 
-# get Python dependence for a given file
-function get_python_framework_path
-{
-    local pydepends_fullpath=$1
-    # regexp to extract the Python framework path
-    # eg. '/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/Python'
-    #   => '/usr/local/opt/python@3.9/Frameworks/Python.framework/Versions/3.9/'
-    py_fmwk_path_re='s;(.*)/(Python|libpython).*;\1;'
-    # regexp to extract the Python version; eg. '3.9'
-    pyversion_re='s;.*[pP]ython.+[Vv]ersions/([0-9.]+).*;\1;'
-    # obtain the dependencies; remove the first line which is
-    # the name of the file itself
-    pyversion=$(echo "$pydepends_fullpath" | sed -E $pyversion_re)
-    py_fmwk_dir=$(echo "$pydepends_fullpath" | sed -E $py_fmwk_path_re)
-    # collect RPATHs corresponding to the common framework paths
-    framework_paths="/usr/local/Library/Frameworks  /Library/Frameworks  /usr/local/Frameworks"
-    py_fmwk_path="Python.framework/Versions/$pyversion/lib"
-    # when the library is like '.../Versions/3.9/Python', then
-    # add an extra '/lib' to the framework path.
-    # This is needed since refer always to the Python shared library
-    # '.../Versions/3.9/lib/libpython39.dylib'.
-    if [[ $pydepends_fullpath = */Python ]]; then
-        py_fmwk_dir="$py_fmwk_dir/lib"
-    fi
-    py_fmwk_rpaths="$py_fmwk_dir"
-    for pth in `echo ${=framework_paths}`; do
-        py_fmwk_rpaths="$py_fmwk_rpaths  $pth/$py_fmwk_path"
-    done
-    # return a list of possible framework paths
-    echo $py_fmwk_rpaths
-}
-
-
 # rename the external libraries to their library ids
 for lib in $xlibdir/*.dylib*; do
     lib_id=$(dylib_base_id $lib)
-    mv -fn "$lib" "$xlibdir/$lib_id"
+    mv -f "$lib" "$xlibdir/$lib_id"
 done
 
 # gather all dependencies
diff --git a/devtools/deploy/mac/shutils.zsh b/devtools/deploy/mac/shutils.zsh
index c255009a2fc47762778c25f752b5d527c2694c57..fcb78ac868d173051c3d68a24441b7e8d260a98d 100644
--- a/devtools/deploy/mac/shutils.zsh
+++ b/devtools/deploy/mac/shutils.zsh
@@ -317,6 +317,26 @@ function get_python_framework_path
     print $py_fmwk_rpaths
 }
 
+function get_python_framework_path_rel
+# produce proper Python framework paths for a given _relative_ Python dependency ($1)
+{
+    pydepends_relpath=$1
+    # regexp to extract the Python framework path
+    # eg. '@rpath/libpython39.dylib' => 'libpython39.dylib'
+    pyversion_re='s;.*/libpython([0-9].[0-9]+).*;\1;'
+    declare -r pyversion=$(echo "$1" | sed -E $pyversion_re)
+    # RPATHs corresponding to the common OSX framework paths
+    declare -r py_fmwk_basepath="Python.framework/Versions/$pyversion/lib"
+    declare -ar framework_paths=( /usr/local/Library/Frameworks
+      /Library/Frameworks  /usr/local/Frameworks )
+    # collect proper RPATHs for Python framework
+    py_fmwk_rpaths=""
+    for pth in $framework_paths; do
+        py_fmwk_rpaths+=" $pth/$py_fmwk_basepath"
+    done
+    # return a list of possible framework paths
+    print $py_fmwk_rpaths
+}
 #========================================
 
 #-- perform tests --