diff --git a/.gitlab-ci.astron.yml b/.gitlab-ci.astron.yml
index 310737ce040b8141e41be3d78f17db68aaa94cb7..439879802e1ce70777178e2f6f0a6bde3fe8ce18 100644
--- a/.gitlab-ci.astron.yml
+++ b/.gitlab-ci.astron.yml
@@ -40,10 +40,13 @@ cibuild-python-wheels:
     - wget -qO - https://get.docker.com/ | sh
     - pip install cibuildwheel
     - cibuildwheel --output-dir wheelhouse
-  # rules:
-  #   # Only run cibuildwheel when building the default branch or a tag.
-  #   - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-  #   - if: '$CI_COMMIT_TAG'
+  rules:
+    # Run cibuildwheel for merge requests, or when building a tag or the default branch
+    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
+    - if: $CI_COMMIT_TAG
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+  tags:
+    - das6
   artifacts:
     paths:
       - wheelhouse/*.whl
diff --git a/.gitlab-ci.common.yml b/.gitlab-ci.common.yml
index 79a8b15d6496c6512cb341f27591c81052be46aa..f2a5de0d8ed05d49a82a7620d4dcb38445e3405e 100644
--- a/.gitlab-ci.common.yml
+++ b/.gitlab-ci.common.yml
@@ -292,16 +292,6 @@ deploy-image-2204:
     # For testing this job on a branch, set the DEPLOY_IMAGE variable to true.
     - if: '$DEPLOY_IMAGE'
 
-python-wheel-2204:
-  extends: .needs-base-2204
-  stage: deploy
-  image: $BASE_IMAGE_2204
-  variables:
-    GIT_SUBMODULE_STRATEGY: normal
-  script:
-    - python3 setup.py bdist_wheel
-    - pip install --user dist/*.whl
-    - python3 -c "import everybeam"
   artifacts:
     paths:
-      - dist/*.whl
+      - dist/everybeam*.whl
diff --git a/CMake/PythonInstall.cmake b/CMake/PythonInstall.cmake
deleted file mode 100644
index bdd6c6c3ca7d471ce0e38c9c4d4e49f96ed24fb6..0000000000000000000000000000000000000000
--- a/CMake/PythonInstall.cmake
+++ /dev/null
@@ -1,131 +0,0 @@
-# - Install Python source files.
-#  python_install(source1..sourceN DESTINATION install_dir)
-# Install Python source files and byte-compile them in the directory
-# ${PYTHON_INSTALL_DIR}/${install_dir}.
-
-# Copyright (C) 2020 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-# Search for the Python interpreter.
-find_package(PythonInterp)
-
-# Derive the Python site-packages installation directory and build directory.
-if(PYTHON_EXECUTABLE)
-  set(_cmd
-    "from distutils.sysconfig import get_python_lib"
-    "from os.path import join"
-    "print(join(
-       get_python_lib(plat_specific=True, standard_lib=True, prefix=''),
-       'site-packages'))"
-  )
-  execute_process(
-    COMMAND "${PYTHON_EXECUTABLE}" "-c" "${_cmd}"
-    OUTPUT_VARIABLE _pydir
-    ERROR_VARIABLE _pyerr
-    OUTPUT_STRIP_TRAILING_WHITESPACE)
-  if(_pyerr)
-    message(FATAL_ERROR "Python command failed:\n${_pyerr}")
-  endif(_pyerr)
-
-  if(NOT DEFINED PYTHON_BUILD_DIR)
-    set(_PRINT_PYTHON_DIRS TRUE)
-  endif()
-
-  set(PYTHON_BUILD_DIR "${CMAKE_BINARY_DIR}/${_pydir}" CACHE PATH
-    "Build directory for Python extensions" FORCE)
-  set(PYTHON_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${_pydir}" CACHE PATH
-    "Installation directory for Python extensions" FORCE)
-
-  if(_PRINT_PYTHON_DIRS)
-    message(STATUS "Build directory for Python extensions:        ${PYTHON_BUILD_DIR}")
-    message(STATUS "Installation directory for Python extensions: ${PYTHON_INSTALL_DIR}")
-  endif()
-endif(PYTHON_EXECUTABLE)
-
-
-#
-# macro python_install
-#
-macro(python_install)
-
-  # Precondition check.
-  if(NOT PYTHON_EXECUTABLE)
-    message(FATAL_ERROR "python_install: Python interpreter not available")
-  endif(NOT PYTHON_EXECUTABLE)
-
-  # Parse arguments.
-  # apart from the python files list, there are two additional arguments
-  # DESTINATION (required), where to put the py files (relative to python lib dir)
-  # EXECUTABLE (optional), makes the py files executable
-  string(REGEX REPLACE ";?DESTINATION.*" "" _py_files "${ARGN}")
-  string(REGEX REPLACE ";?EXECUTABLE.*" "" _py_files "${_py_files}")
-  string(REGEX MATCH "DESTINATION;.*" _dest_dir "${ARGN}")
-  string(REGEX REPLACE "^DESTINATION;" "" _dest_dir "${_dest_dir}")
-  string(REGEX REPLACE ";?EXECUTABLE.*" "" _dest_dir "${_dest_dir}")
-  string(REGEX MATCH "EXECUTABLE;" _executable "${ARGN}")
-
-  #check if optional argument EXECUTABLE is set
-  #if so, then install the _py_files as EXECUTABLE type (executable)
-  #else as normal files (not executable)
-  if("${_executable}" STRGREATER "")
-    set(INSTALL_TYPE PROGRAMS)
-  else()
-    set(INSTALL_TYPE FILES)
-  endif("${_executable}" STRGREATER "")
-
-  if(_py_files MATCHES "^$")
-    message(FATAL_ERROR "python_install: no sources files specified")
-  endif(_py_files MATCHES "^$")
-  if(_dest_dir MATCHES "^$" OR _dest_dir MATCHES ";")
-    message(FATAL_ERROR "python_install: destination directory invalid")
-  endif(_dest_dir MATCHES "^$" OR _dest_dir MATCHES ";")
-
-  # Set python package build/install directory.
-  set(_inst_dir "${PYTHON_INSTALL_DIR}/${_dest_dir}")
-  set(_build_dir "${PYTHON_BUILD_DIR}/${_dest_dir}")
-
-  # Install and byte-compile each Python file.
-  foreach(_py ${_py_files})
-    get_filename_component(_py_path ${_py} PATH)
-    get_filename_component(_py_abs ${_py} ABSOLUTE)
-
-    # check if _py is a path in CMAKE_BINARY_DIR. If so, then it is most likely a configured_file.
-    # then strip the CMAKE_CURRENT_BINARY_DIR prefix.
-    if(${_py} MATCHES "^(${CMAKE_CURRENT_BINARY_DIR})")
-      string(REGEX REPLACE "^(${CMAKE_CURRENT_BINARY_DIR}/)" "" _py "${_py}")
-      get_filename_component(_py_path ${_py} PATH)
-    endif()
-
-    # Create a symlink to each Python file; needed to mimic install tree.
-    file(MAKE_DIRECTORY ${_build_dir}/${_py_path})
-    execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
-      ${_py_abs} ${_build_dir}/${_py})
-    install(${INSTALL_TYPE} ${_py_abs} DESTINATION ${_inst_dir}/${_py_path})
-    if(USE_PYTHON_COMPILATION)
-      set(_py_code
-        "import py_compile, os"
-        "destdir = os.environ.get('DESTDIR','')"
-        "print('-- Byte-compiling: %s${_inst_dir}/${_py}' % destdir)"
-        "py_compile.compile('%s${DESTDIR}${_inst_dir}/${_py}' % destdir, doraise=True)")
-      install(CODE
-        "execute_process(COMMAND ${PYTHON_EXECUTABLE} -c \"${_py_code}\"
-                       RESULT_VARIABLE _result)
-       if(NOT _result EQUAL 0)
-         message(FATAL_ERROR \"Byte-compilation FAILED: \$ENV{DESTDIR}${_inst_dir}/${_py}\")
-       endif(NOT _result EQUAL 0)")
-    endif(USE_PYTHON_COMPILATION)
-  endforeach(_py ${_py_files})
-
-  # Make sure that there's a __init__.py file in each build/install directory.
-  string(REGEX REPLACE "/" ";" _dir_list ${_dest_dir})
-  set(_init_dir)
-  foreach(_dir ${_dir_list})
-    set(_init_dir "${_init_dir}/${_dir}")
-    execute_process(COMMAND ${CMAKE_COMMAND} -E touch
-      "${PYTHON_BUILD_DIR}${_init_dir}/__init__.py")
-    install(CODE
-      "execute_process(COMMAND ${CMAKE_COMMAND} -E touch
-        \"\$ENV{DESTDIR}${PYTHON_INSTALL_DIR}${_init_dir}/__init__.py\")")
-  endforeach(_dir ${_dir_list})
-
-endmacro(python_install)
diff --git a/CMake/config.h.in b/CMake/config.h.in
index c128798ca6728c828bc5b1313593abaee0b8da3b..e2270e4ff5aabf245fde9cdd2c413e182ed4bcc8 100644
--- a/CMake/config.h.in
+++ b/CMake/config.h.in
@@ -5,7 +5,7 @@
 #define EVERYBEAM_CONFIG_H_
 
 #define EVERYBEAM_DATADIR "@EVERYBEAM_DATADIR@"
-#define EVERYBEAM_FULL_DATADIR "@EVERYBEAM_FULL_DATADIR@"
+#define EVERYBEAM_ABSOLUTE_DATADIR "@EVERYBEAM_ABSOLUTE_DATADIR@"
 #define TEST_MEASUREMENTSET "@TEST_MEASUREMENTSET@"
 #define LOFAR_HBA_MOCK_MS "@LOFAR_HBA_MOCK_MS@"
 #define LOFAR_LBA_MOCK_MS "@LOFAR_LBA_MOCK_MS@"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8e030aa5ff139cae88011425ebf9f72d358929b6..70d3dada4687d37765595a8b3c8ba0fe560302ed 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,7 +7,7 @@ cmake_minimum_required(VERSION 3.15)
 
 #------------------------------------------------------------------------------
 # Set version name and project number
-set(EVERYBEAM_VERSION 0.5.3)
+set(EVERYBEAM_VERSION 0.5.3) # Keep in sync with `pyproject.toml` file
 if(EVERYBEAM_VERSION MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)")
   set(EVERYBEAM_VERSION_MAJOR "${CMAKE_MATCH_1}")
   set(EVERYBEAM_VERSION_MINOR "${CMAKE_MATCH_2}")
@@ -53,6 +53,16 @@ option(DOWNLOAD_LWA "Download and install OVRO-LWA coefficient file" OFF)
 
 string(TOLOWER ${CMAKE_PROJECT_NAME} projectname)
 
+# When the software is built with, e.g., `pip` or `build`, `scikit-build-core`
+# controls the build, and software will be installed in `site-packages`. In
+# this case we do not want to install libraries in `lib`, but instead follow
+# the convention of installing libraries in a directory `<package>.libs`.
+if(SKBUILD)
+  set(INSTALL_LIBDIR everybeam.libs)
+else()
+  set(INSTALL_LIBDIR lib)
+endif()
+
 # Set the path to CMake modules
 set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/CMake)
 
@@ -85,6 +95,9 @@ else()
   set(FIND_CUDA
       OFF
       CACHE INTERNAL "")
+  # Ensure that the ska-sdp-func library is installed in the same directory as
+  # the other everybeam libraries.
+  set(SDP_FUNC_LIB_INSTALL_DIR "${INSTALL_LIBDIR}")
   FetchContent_Declare(
     ska-sdp-func
     GIT_REPOSITORY ${ska-sdp-func_GIT_REPOSITORY}
@@ -106,11 +119,23 @@ set(EVERYBEAM_DATADIR
     "share/${projectname}"
     CACHE STRING "EveryBeam data directory")
 if(IS_ABSOLUTE ${EVERYBEAM_DATADIR})
-  set(EVERYBEAM_FULL_DATADIR "${EVERYBEAM_DATADIR}")
+  set(EVERYBEAM_ABSOLUTE_DATADIR "${EVERYBEAM_DATADIR}")
 else()
-  set(EVERYBEAM_FULL_DATADIR "${CMAKE_INSTALL_PREFIX}/${EVERYBEAM_DATADIR}")
+  set(EVERYBEAM_ABSOLUTE_DATADIR "${CMAKE_INSTALL_PREFIX}/${EVERYBEAM_DATADIR}")
 endif()
-message("Storing data in: " ${EVERYBEAM_FULL_DATADIR})
+
+# When the software is built with, e.g., `pip` or `build`, `scikit-build-core`
+# controls the build. In this case we need to follow the naming convention in
+# PEP-491 for the data directory, otherwise the data files will not be
+# installed in the right directory when the python wheel is unpacked.
+if(SKBUILD)
+  # Following naming convention of data directory in PEP 491
+  set(EVERYBEAM_INSTALL_DATADIR
+      "${projectname}-${EVERYBEAM_VERSION}.data/data/share/${projectname}")
+else()
+  set(EVERYBEAM_INSTALL_DATADIR ${EVERYBEAM_DATADIR})
+endif()
+message("Installing data files in: ${EVERYBEAM_INSTALL_DATADIR}")
 
 # Find and include git submodules
 find_package(Git QUIET)
@@ -234,16 +259,15 @@ set(CMAKE_SKIP_BUILD_RPATH FALSE)
 # when building, don't use the install RPATH already
 # (but later on when installing)
 set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
-set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
 # add the automatically determined parts of the RPATH
 # which point to directories outside the build tree to the install RPATH
 set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
 # the RPATH to be used when installing, but only if it's not a system directory
 list(FIND CMAKE_PLATFORM_IMPLICIT_LINK_DIRECTORIES
-     "${CMAKE_INSTALL_PREFIX}/lib" isSystemDir)
+     "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBDIR}" isSystemDir)
 if("${isSystemDir}" STREQUAL "-1")
-  set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
-endif("${isSystemDir}" STREQUAL "-1")
+  set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${INSTALL_LIBDIR}")
+endif()
 
 #------------------------------------------------------------------------------
 # Set up a test_data directory in the build directory.
diff --git a/cibuildwheel/before_all.sh b/cibuildwheel/before_all.sh
index 1372d9697c82621bb47aa58a7c68a7e06649680a..9d85f5e4e4f457f78d423699e7e0848cc6fa6204 100755
--- a/cibuildwheel/before_all.sh
+++ b/cibuildwheel/before_all.sh
@@ -26,11 +26,12 @@ function download_and_build_fftw
   /bin/echo -e "\n==> Building and installing FFTW ${FFTW_VERSION} ...\n"
   cd "${WORKDIR}/fftw-${FFTW_VERSION}"
   ./configure \
+    --quiet \
     --prefix /usr/local \
     --enable-threads \
     --enable-shared \
     --enable-float
-  make -j"${nproc}" install
+  make --jobs="${nproc}" --quiet install
 }
 
 
@@ -46,11 +47,12 @@ function download_and_build_hdf5
   /bin/echo -e "\n==> Building and installing HDF5 ${HDF5_VERSION} ...\n"
   cd "${WORKDIR}/hdf5-${HDF5_VERSION}"
   ./configure \
+    --quiet \
     --prefix /usr/local \
     --enable-build-mode=production \
     --with-szlib \
     --enable-cxx
-  make -j"${nproc}" install
+  make --jobs="${nproc}" --quiet install
 }
 
 
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index d7278c30e18f754bfe3d8284b9515a5be8235d14..66574d6e1699fc9556b01dc59f9489b09eb9a7b0 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -103,10 +103,15 @@ target_link_libraries(
 target_link_libraries(everybeam PRIVATE schaapcommon Threads::Threads xtensor
                                         ${BLAS_LIBRARIES} ${LAPACK_LIBRARIES})
 
+if(SKBUILD)
+  set_target_properties(everybeam PROPERTIES INSTALL_RPATH "$ORIGIN")
+endif()
+
 install(
   TARGETS everybeam everybeam-core
+  COMPONENT libraries
   EXPORT EveryBeamTargets
-  DESTINATION lib)
+  DESTINATION ${INSTALL_LIBDIR})
 
 install(
   FILES antenna.h
diff --git a/cpp/hamaker/CMakeLists.txt b/cpp/hamaker/CMakeLists.txt
index fe51c4b3840ef26b74f60c328c84c7bf08d5a041..0b1b919577771740b60e908b714682da4217869b 100644
--- a/cpp/hamaker/CMakeLists.txt
+++ b/cpp/hamaker/CMakeLists.txt
@@ -10,16 +10,21 @@ target_link_libraries(hamaker PUBLIC ${HDF5_CXX_LIBRARIES} everybeam-core)
 string(TOLOWER ${CMAKE_PROJECT_NAME} projectname)
 set_target_properties(hamaker PROPERTIES LIBRARY_OUTPUT_NAME
                                          "${projectname}-hamaker")
+if(SKBUILD)
+  set_target_properties(hamaker PROPERTIES INSTALL_RPATH "$ORIGIN")
+endif()
 
 # install libhamaker.so
 install(
   TARGETS hamaker
+  COMPONENT libraries
   EXPORT EveryBeamTargets
-  DESTINATION lib)
+  DESTINATION ${INSTALL_LIBDIR})
 
 # install coefficients
-message("install hamaker in: " ${EVERYBEAM_FULL_DATADIR})
-install(FILES "${CMAKE_SOURCE_DIR}/coeffs/HamakerHBACoeff.h5"
-        DESTINATION ${EVERYBEAM_DATADIR})
-install(FILES "${CMAKE_SOURCE_DIR}/coeffs/HamakerLBACoeff.h5"
-        DESTINATION ${EVERYBEAM_DATADIR})
+message("Install Hamaker coefficients in: ${EVERYBEAM_INSTALL_DATADIR}")
+install(
+  FILES "${CMAKE_SOURCE_DIR}/coeffs/HamakerHBACoeff.h5"
+        "${CMAKE_SOURCE_DIR}/coeffs/HamakerLBACoeff.h5"
+  COMPONENT data-files
+  DESTINATION ${EVERYBEAM_INSTALL_DATADIR})
diff --git a/cpp/lobes/CMakeLists.txt b/cpp/lobes/CMakeLists.txt
index cb0339c80318947ebfb81268e4471ffa0e75521f..68d5665b5c8bcec154717e6c118458ae546bbb10 100644
--- a/cpp/lobes/CMakeLists.txt
+++ b/cpp/lobes/CMakeLists.txt
@@ -17,10 +17,11 @@ if(DOWNLOAD_LOBES OR BUILD_TESTING)
 endif()
 
 # install coefficients
-message("install lobes coefficients in: " ${EVERYBEAM_FULL_DATADIR}/lobes)
+message("Install LOBES coefficients in: ${EVERYBEAM_INSTALL_DATADIR}/lobes")
 install(
   DIRECTORY "${CMAKE_BINARY_DIR}/coeffs/lobes"
-  DESTINATION ${EVERYBEAM_DATADIR}
+  COMPONENT data-files
+  DESTINATION ${EVERYBEAM_INSTALL_DATADIR}
   FILES_MATCHING
   PATTERN "LOBES_*")
 
diff --git a/cpp/lwa/CMakeLists.txt b/cpp/lwa/CMakeLists.txt
index 9ccf7f3587cf68bf3a718a20c6b64bd39149fcf1..c700974706d7dca3d68dbdda512add489c595be5 100644
--- a/cpp/lwa/CMakeLists.txt
+++ b/cpp/lwa/CMakeLists.txt
@@ -16,8 +16,8 @@ if(DOWNLOAD_LWA OR BUILD_TESTING)
   add_dependencies(everybeam download_lwa_coefficients)
 
   # install coefficients
-  message("install LWA coefficients in: " ${EVERYBEAM_FULL_DATADIR}/lwa)
+  message("Install LWA coefficients in: ${EVERYBEAM_INSTALL_DATADIR}/lwa")
   install(FILES "${CMAKE_BINARY_DIR}/coeffs/lwa/LWA_OVRO.h5"
-          DESTINATION "${EVERYBEAM_DATADIR}/lwa")
+          DESTINATION "${EVERYBEAM_INSTALL_DATADIR}/lwa")
 
 endif()
diff --git a/cpp/options.cc b/cpp/options.cc
index 6d7add45e8bcf2af65834af7430750f1fa2158dd..4dc2081bef114360a09ef32aabd6fb8dad2db77a 100644
--- a/cpp/options.cc
+++ b/cpp/options.cc
@@ -13,7 +13,7 @@ namespace everybeam {
 // TODO(RAP-260) Improve this path lookup.
 std::filesystem::path GetDataDirectory() {
   const char* envvar;
-  if (std::strcmp(EVERYBEAM_DATADIR, EVERYBEAM_FULL_DATADIR) == 0)
+  if (std::strcmp(EVERYBEAM_DATADIR, EVERYBEAM_ABSOLUTE_DATADIR) == 0)
     return std::filesystem::path(EVERYBEAM_DATADIR);
   if ((envvar = std::getenv("EVERYBEAM_DATADIR")))
     return std::filesystem::path(envvar);
@@ -21,6 +21,6 @@ std::filesystem::path GetDataDirectory() {
     return std::filesystem::path(envvar) / EVERYBEAM_DATADIR;
   if ((envvar = std::getenv("VIRTUAL_ENV")))
     return std::filesystem::path(envvar) / EVERYBEAM_DATADIR;
-  return std::filesystem::path(EVERYBEAM_FULL_DATADIR);
+  return std::filesystem::path(EVERYBEAM_ABSOLUTE_DATADIR);
 }
 }  // namespace everybeam
diff --git a/cpp/oskar/CMakeLists.txt b/cpp/oskar/CMakeLists.txt
index 47e0cd0de035ff4932e906dbe991a5f42820413f..af0499e8741c944f78d03b6eacf85e7c7aa747c9 100644
--- a/cpp/oskar/CMakeLists.txt
+++ b/cpp/oskar/CMakeLists.txt
@@ -13,7 +13,9 @@ add_library(oskar SHARED oskarelementresponse.cc oskardatafile.cc
 string(TOLOWER ${CMAKE_PROJECT_NAME} projectname)
 set_target_properties(oskar PROPERTIES LIBRARY_OUTPUT_NAME
                                        "${projectname}-oskar")
-
+if(SKBUILD)
+  set_target_properties(oskar PROPERTIES INSTALL_RPATH "$ORIGIN")
+endif()
 # Make sure that when other targets within this project link against the oskar target,
 # they can find the include files.
 target_include_directories(
@@ -29,9 +31,12 @@ target_link_libraries(oskar PRIVATE ska-sdp-func::ska-sdp-func)
 # install libeverybeam-oskar.so
 install(
   TARGETS oskar
+  COMPONENT libraries
   EXPORT EveryBeamTargets
-  DESTINATION lib)
+  DESTINATION ${INSTALL_LIBDIR})
 
 #install oskar coefficients
-install(FILES "${CMAKE_SOURCE_DIR}/coeffs/oskar.h5"
-        DESTINATION ${EVERYBEAM_DATADIR})
+install(
+  FILES "${CMAKE_SOURCE_DIR}/coeffs/oskar.h5"
+  COMPONENT data-files
+  DESTINATION ${EVERYBEAM_INSTALL_DATADIR})
diff --git a/cpp/skamidbeam/CMakeLists.txt b/cpp/skamidbeam/CMakeLists.txt
index 217505fb1436ac2198aa90747d57d8a20d76276f..042b38979d214c83e181c55f1e39f22c0f999d7c 100644
--- a/cpp/skamidbeam/CMakeLists.txt
+++ b/cpp/skamidbeam/CMakeLists.txt
@@ -18,5 +18,6 @@ target_include_directories(
 # install libeverybeam-skamidbeam.so
 install(
   TARGETS skamidbeam
+  COMPONENT libraries
   EXPORT EveryBeamTargets
-  DESTINATION lib)
+  DESTINATION ${INSTALL_LIBDIR})
diff --git a/pyproject.toml b/pyproject.toml
index 7bc4b61e99a3aacf0f215e49ade992f19ba43d49..577aaaa672309a79c721f7f21c32fd92def7c028 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,23 +1,76 @@
+#########################
+# build-system settings #
+#########################
+
 [build-system]
 requires = [
-    "build",
-    "cmake>=3.18",
-    "oldest-supported-numpy",
-    "setuptools",
-    "wheel",
+    "scikit-build-core",
+]
+build-backend = "scikit_build_core.build"
+
+
+####################
+# project settings #
+####################
+
+[project]
+name = "everybeam"
+version = "0.5.3"  # Keep in sync with top-level `CMakeLists.txt` file
+description = "EveryBeam"
+readme = {file = "README.md", content-type = "text/markdown"}
+requires-python = ">=3.7"
+license = {text = "GPLv3+"}
+authors = [
+    {name = "Bram Veenboer", email =  "veenboer@astron.nl"},
+    {name = "André Offringa", email = "offringa@astron.nl"},
+    {name = "Sebastiaan van der Tol", email = "tol@astron.nl"},
+    {name = "Tammo Jan Dijkema", email = "dijkema@astron.nl"},
+    {name = "Jakob Maljaars", email = "jakob.maljaars@stcorp.nl"},
+    {name = "Maik Nijhuis", email = "maik.nijhuis@triopsys.nl"},
+]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Science/Research",
+    "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
+    "Programming Language :: C++",
+    "Programming Language :: Python :: 3",
+    "Topic :: Scientific/Engineering :: Astronomy",
 ]
+dependencies = [
+    "numpy",
+]
+
+[project.urls]
+Homepage = "https://git.astron.nl/RD/EveryBeam/"
+Documentation = "https://everybeam.readthedocs.io/"
+Repository = "https://git.astron.nl/RD/EveryBeam.git"
+
+
+#########################
+# cibuildwheel settings #
+#########################
 
 [tool.cibuildwheel]
+## Build
 before-all = "cibuildwheel/before_all.sh"
 before-build = "cibuildwheel/before_build.sh"
 build = "cp3{7,8,9,10,11}-*_x86_64"
-build-verbosity = 1
 environment = """ \
     WORKDIR="/tmp" \
     FFTW_VERSION="3.3.8" \
     HDF5_VERSION="1.12.2" \
 """
-# test-command = "cd {package}/python/test && pytest"
+## Test
+before-test = [
+    "scripts/download_ms.sh lba.MS.tar.bz2 LOFAR_LBA_MOCK.ms",
+    "scripts/download_ms.sh L258627-one-timestep.tar.bz2 LOFAR_HBA_MOCK.ms",
+]
+test-command = [
+    "export DATA_DIR={project}",
+    "export EVERYBEAM_DATADIR=$(dirname $(which python))/../share/everybeam",
+    "pytest -v {package}/python/test/test_pybindings.py"
+]
+test-requires = "pytest-lazy-fixture"
 
 [tool.cibuildwheel.macos]
 repair-wheel-command = """\
@@ -47,3 +100,17 @@ manylinux-x86_64-image = "quay.io/casacore/casacore:master_wheel310"
 [[tool.cibuildwheel.overrides]]
 select="cp311-*"
 manylinux-x86_64-image = "quay.io/casacore/casacore:master_wheel311"
+
+
+#########################
+# scikit-build settings #
+#########################
+
+[tool.scikit-build]
+cmake.minimum-version = "3.15"
+ninja.minimum-version = "1.5"
+install.components = ["data-files", "libraries"]
+logging.level = "INFO"
+
+[tool.scikit-build.cmake.define]
+BUILD_WITH_PYTHON = "ON"
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index bd98c17084918c17ed3555720aba85e07961bef7..7da84972ca8f7f96edf05a6f861e511a377f85de 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -36,27 +36,37 @@ target_include_directories(pyeverybeam
                            PUBLIC "$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/cpp>")
 target_link_libraries(pyeverybeam PUBLIC everybeam)
 set_target_properties(pyeverybeam PROPERTIES OUTPUT_NAME everybeam)
+if(SKBUILD)
+  set_target_properties(pyeverybeam PROPERTIES INSTALL_RPATH
+                                               "$ORIGIN/${INSTALL_LIBDIR}")
+endif()
 
-# If the PYTHON_LIBRARY_DIR is not specified, install in lib dir
-if(NOT DEFINED ${PYTHON_LIBRARY_DIR})
-  execute_process(
-    COMMAND ${PYTHON_EXECUTABLE} -c
-            "import site; print(site.getsitepackages()[0])"
-    OUTPUT_VARIABLE PYTHON_DIST_PATH
-    OUTPUT_STRIP_TRAILING_WHITESPACE)
-
-  if(PYTHON_DIST_PATH MATCHES
-     "\\/(lib.*\\/python${PYTHON_VERSION_MAJOR}\\.${PYTHON_VERSION_MINOR}\\/.*)"
-  )
-    set(PYTHON_LIBRARY_DIR ${CMAKE_MATCH_1})
+# Define where the python module will be installed. When using scikit-build,
+# it must be the current directory; otherwise, use site-packages directory.
+if(NOT DEFINED PYTHON_LIBRARY_DIR)
+  if(DEFINED SKBUILD)
+    set(PYTHON_LIBRARY_DIR .)
   else()
-    message(
-      FATAL_ERROR "Failed to parse PYTHON_DIST_PATH='${PYTHON_DIST_PATH}'")
+    execute_process(
+      COMMAND ${PYTHON_EXECUTABLE} -c
+              "import site; print(site.getsitepackages()[0])"
+      OUTPUT_VARIABLE PYTHON_DIST_PATH
+      OUTPUT_STRIP_TRAILING_WHITESPACE)
+
+    if(PYTHON_DIST_PATH
+       MATCHES
+       "\\/(lib.*\\/python${PYTHON_VERSION_MAJOR}\\.${PYTHON_VERSION_MINOR}\\/.*)"
+    )
+      set(PYTHON_LIBRARY_DIR ${CMAKE_MATCH_1})
+    else()
+      message(
+        FATAL_ERROR "Failed to parse PYTHON_DIST_PATH='${PYTHON_DIST_PATH}'")
+    endif()
   endif()
 endif()
 
 # Install pyeverybeam in site-packages directory
 install(
   TARGETS pyeverybeam
-  COMPONENT python
+  COMPONENT libraries
   LIBRARY DESTINATION ${PYTHON_LIBRARY_DIR})
diff --git a/setup.py b/setup.py
deleted file mode 100644
index dcbd48e4520dd715ffb11ae29db2e329ac060038..0000000000000000000000000000000000000000
--- a/setup.py
+++ /dev/null
@@ -1,155 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-# Copyright (c) 2016 The Pybind Development Team, All rights reserved.
-
-# Setup.py that calls CMake. Largely taken from https://github.com/pybind/cmake_example/
-
-import os
-import re
-import subprocess
-import sys
-
-from setuptools import Extension, setup
-from setuptools.command.build_ext import build_ext
-
-
-# Set the location where data files are to be installed
-EVERYBEAM_DATADIR = os.path.join("share", "everybeam")
-
-
-# A CMakeExtension needs a sourcedir instead of a file list.
-# The name must be the _single_ output extension from the CMake build.
-# If you need multiple extensions, see scikit-build.
-class CMakeExtension(Extension):
-    def __init__(self, name, sourcedir=""):
-        Extension.__init__(self, name, sources=[])
-        self.sourcedir = os.path.abspath(sourcedir)
-
-
-class CMakeBuild(build_ext):
-    def build_extension(self, ext):
-        extdir = os.path.abspath(
-            os.path.dirname(self.get_ext_fullpath(ext.name))
-        )
-
-        # required for auto-detection & inclusion of auxiliary "native" libs
-        if not extdir.endswith(os.path.sep):
-            extdir += os.path.sep
-
-        debug = (
-            int(os.environ.get("DEBUG", 0))
-            if self.debug is None
-            else self.debug
-        )
-        cfg = "Debug" if debug else "Release"
-
-        # CMake lets you override the generator - we need to check this.
-        # Can be set with Conda-Build, for example.
-        cmake_generator = os.environ.get("CMAKE_GENERATOR", "")
-
-        # Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON
-        cmake_args = [
-            f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}",
-            f"-DPYTHON_EXECUTABLE={sys.executable}",
-            f"-DCMAKE_BUILD_TYPE={cfg}",  # not used on MSVC, but no harm
-        ]
-        build_args = []
-        # Adding CMake arguments set as environment variable
-        # (needed e.g. to build for ARM OSx on conda-forge)
-        if "CMAKE_ARGS" in os.environ:
-            cmake_args += [
-                item for item in os.environ["CMAKE_ARGS"].split(" ") if item
-            ]
-
-        # Adding CMake arguments specific to building EveryBeam
-        cmake_args += [
-            "-DBUILD_WITH_PYTHON=On",
-            f"-DEVERYBEAM_DATADIR={EVERYBEAM_DATADIR}",
-        ]
-
-        # Using Ninja-build since it a) is available as a wheel and b)
-        # multithreads automatically. MSVC would require all variables be
-        # exported for Ninja to pick it up, which is a little tricky to do.
-        # Users can override the generator with CMAKE_GENERATOR in CMake
-        # 3.15+.
-        if not cmake_generator or cmake_generator == "Ninja":
-            try:
-                import ninja  # noqa: F401
-
-                ninja_executable_path = os.path.join(ninja.BIN_DIR, "ninja")
-                cmake_args += [
-                    "-GNinja",
-                    f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}",
-                ]
-            except ImportError:
-                pass
-
-        if sys.platform.startswith("darwin"):
-            # Cross-compile support for macOS - respect ARCHFLAGS if set
-            archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", ""))
-            if archs:
-                cmake_args += [
-                    "-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))
-                ]
-
-        # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
-        # across all generators.
-        if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
-            # self.parallel is a Python 3 only way to set parallel jobs by hand
-            # using -j in the build_ext call, not supported by pip or PyPA-build.
-            if hasattr(self, "parallel") and self.parallel:
-                # CMake 3.12+ only.
-                build_args += [f"-j{self.parallel}"]
-
-        build_temp = os.path.join(self.build_temp, ext.name)
-        if not os.path.exists(build_temp):
-            os.makedirs(build_temp)
-
-        subprocess.check_call(["which", "g++"])
-        subprocess.check_call(
-            ["cmake", ext.sourcedir] + cmake_args, cwd=build_temp
-        )
-        subprocess.check_call(
-            ["cmake", "--build", "."] + build_args, cwd=build_temp
-        )
-
-
-# Install the coefficient files in a location outside the python package --
-# <prefix>/share/everbeam, where <prefix> is the python install prefix --
-# which makes them easier to find for other programs.
-data_files = []
-for root, _, files in os.walk("coeffs"):
-    data_files.append(
-        (
-            EVERYBEAM_DATADIR,
-            [os.path.join(root, f) for f in files],
-        )
-    )
-
-# The information here can also be placed in setup.cfg - better separation of
-# logic and declaration, and simpler if you include description/version in a file.
-setup(
-    name="everybeam",
-    version="0.5.1",
-    author="André Offringa",
-    author_email="offringa@astron.nl",
-    description="EveryBeam",
-    long_description=open("README.md", "rt").read(),
-    long_description_content_type="text/markdown",
-    ext_modules=[CMakeExtension("everybeam")],
-    cmdclass={"build_ext": CMakeBuild},
-    zip_safe=False,
-    python_requires=">=3.6",
-    install_requires=["numpy"],
-    url="https://everybeam.readthedocs.io/",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Science/Research",
-        "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
-        "Programming Language :: C++",
-        "Programming Language :: Python :: 3",
-        "Topic :: Scientific/Engineering :: Astronomy",
-    ],
-    data_files=data_files,
-)