diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b0bfffcfddf5541ce1468a779d9c2cef75327365..eda592152de1bf8420791880a28fe1a2ad0569c2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -120,43 +120,6 @@ jobs:
                 (cd examples; rm -f gl_*)
                 run_examples --no-require-main
 
-    wheels:
-        name: Build and upload wheels
-        runs-on: ubuntu-latest
-        strategy:
-            matrix:
-                DOCKER_IMAGE:
-                - quay.io/pypa/manylinux2014_x86_64
-                # Disable i686 builds for now: no binary wheels for cryptography,
-                # source build fails, e.g. https://github.com/inducer/pyopencl/pull/421/checks?check_run_id=1781071632
-                # - quay.io/pypa/manylinux2014_i686
-        steps:
-        -   uses: actions/checkout@v2
-        -   name: "Main Script"
-            env:
-                TWINE_USERNAME: __token__
-                TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
-                DOCKER_IMAGE: ${{ matrix.DOCKER_IMAGE }}
-
-            run: |
-                pwd
-                ls -la
-
-                # Only perform upload for tag builds, otherwise unset TWINE_USERNAME to prevent
-                if ! [[ $GITHUB_REF == refs/tags* ]]; then
-                    echo "Not a tag build, GITHUB_REF is '$GITHUB_REF'. Unsetting TWINE_USERNAME"
-                    unset TWINE_USERNAME
-                fi
-
-                if [[ $DOCKER_IMAGE == *i686* ]]; then
-                    PRE_CMD=linux32
-                else
-                    PRE_CMD=""
-                fi
-
-                docker run --rm -v `pwd`:/io -e TWINE_USERNAME -e TWINE_PASSWORD $DOCKER_IMAGE $PRE_CMD /io/scripts/build-wheels.sh
-                ls wheelhouse/
-
     downstream_tests:
         strategy:
             matrix:
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
new file mode 100644
index 0000000000000000000000000000000000000000..37faac229f8314a38fc738e6c2f00312e5a7a808
--- /dev/null
+++ b/.github/workflows/wheels.yml
@@ -0,0 +1,65 @@
+name: Build and upload to PyPI
+
+# Build on every branch push, tag push, and pull request change:
+on:
+    push:
+        branches:
+        - main
+        tags:
+        - v*
+    pull_request:
+    schedule:
+        - cron:  '17 3 * * 0'
+
+jobs:
+  build_wheels:
+    name: Build wheels on ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-20.04, windows-2019, macos-10.15]
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: 'true'
+
+      - name: Build wheels
+        uses: pypa/cibuildwheel@v2.5.0
+
+      - uses: actions/upload-artifact@v2
+        with:
+          path: ./wheelhouse/*.whl
+
+  build_sdist:
+    name: Build source distribution
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Build sdist
+        run: pipx run build --sdist
+
+      - uses: actions/upload-artifact@v2
+        with:
+          path: dist/*.tar.gz
+
+  upload_pypi:
+    needs: [build_wheels, build_sdist]
+    runs-on: ubuntu-latest
+    # upload to PyPI on every tag starting with 'v'
+    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+    # alternatively, to publish when a GitHub Release is created, use the following rule:
+    # if: github.event_name == 'release' && github.event.action == 'published'
+    steps:
+      - uses: actions/download-artifact@v2
+        with:
+          name: artifact
+          path: dist
+
+      - uses: pypa/gh-action-pypi-publish@v1.4.2
+        with:
+          user: __token__
+          password: ${{ secrets.TWINE_PASSWORD }}
+          # To test: repository_url: https://test.pypi.org/legacy/
diff --git a/.gitignore b/.gitignore
index 103ff507f895518184778eee49a08a5435af0d9a..3700f0a81e467096356b86481ebf248771497452 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,3 +66,5 @@ cffi_build.py
 .cache
 .pytest_cache
 .idea
+
+wheelhouse
diff --git a/aksetup_helper.py b/aksetup_helper.py
index de3ae069e651c178a258a7f2b49890150aaf5b6c..89e66da38b2948db3695a46e27e9429a19750639 100644
--- a/aksetup_helper.py
+++ b/aksetup_helper.py
@@ -1,3 +1,4 @@
+import os
 import sys
 try:
     from setuptools import Extension
@@ -256,7 +257,7 @@ def expand_options(options):
 
 
 class ConfigSchema:
-    def __init__(self, options, conf_file="siteconf.py", conf_dir="."):
+    def __init__(self, options, conf_file="siteconf.py", conf_dir=os.path.dirname(__file__)):
         self.optdict = dict((opt.name, opt) for opt in options)
         self.options = options
         self.conf_dir = conf_dir
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..8733abce9edb82273f5cc7e63fbd62c3318e4a71
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,35 @@
+[build-system]
+# Minimum requirements for the build system to execute.
+requires = ["setuptools>=42.0.0", "wheel>=0.34.2", "Cython", "oldest-supported-numpy", "pybind11>=2.5.0"]  # PEP 508 specifications.
+build-backend = "setuptools.build_meta"
+
+
+[tool.cibuildwheel]
+test-command = "pytest {project}/test"
+test-extras = ["test"]
+
+[tool.cibuildwheel.linux]
+test-command = ""
+before-all = [
+    "yum install -y git openssl-devel ruby",
+    "bash {package}/scripts/build-ocl.sh",
+]
+
+[[tool.cibuildwheel.overrides]]
+select = "*-musllinux*"
+before-all = [
+    "apk add ruby git openssl-dev",
+    "bash {package}/scripts/build-ocl.sh",
+]
+
+[tool.cibuildwheel.macos]
+skip = "pp*"
+before-all = "bash {package}/scripts/build-ocl-macos.sh"
+test-command = "pytest {project}/test/test_array.py" # same limitation as conda-forge
+# https://github.com/conda-forge/pyopencl-feedstock/blob/6f3c5de59b18c9518abba3cb94f6ae92964553f8/recipe/meta.yaml#L62-L63
+
+[tool.cibuildwheel.windows]
+skip = ["*-win32", "pp*"]
+test-command = ""
+before-all = "bash {package}/scripts/build-ocl-windows.sh"
+before-build = "python configure.py --cxxflags=-ID:/a/pyopencl/pyopencl/OpenCL-Headers/install/include --ldflags=\"/LIBPATH:C:/Program Files/OpenCL-ICD-Loader/lib\""
diff --git a/scripts/build-ocl-macos.sh b/scripts/build-ocl-macos.sh
new file mode 100644
index 0000000000000000000000000000000000000000..9277c1641c3f86e3542a58dadf442e8544f43949
--- /dev/null
+++ b/scripts/build-ocl-macos.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+set -o xtrace
+
+git clone --branch v2022.01.04 https://github.com/KhronosGroup/OpenCL-ICD-Loader
+git clone --branch v2022.01.04 https://github.com/KhronosGroup/OpenCL-Headers
+
+
+
+cmake -D CMAKE_INSTALL_PREFIX=./OpenCL-Headers/install -S ./OpenCL-Headers -B ./OpenCL-Headers/build 
+cmake --build ./OpenCL-Headers/build --target install
+
+cmake -D CMAKE_PREFIX_PATH=${PWD}/OpenCL-Headers/install -D OPENCL_ICD_LOADER_HEADERS_DIR=${PWD}/OpenCL-Headers/install/include -D CMAKE_INSTALL_PREFIX=./OpenCL-ICD-Loader/install -S ./OpenCL-ICD-Loader -B ./OpenCL-ICD-Loader/build 
+cmake --build ./OpenCL-ICD-Loader/build --target install --config Release
+
+echo "PyOpenCL wheel includes Khronos Group OpenCL-ICD-Loader which is licensed as below" >> ${SCRIPT_DIR}/../LICENSE
+cat ./OpenCL-ICD-Loader/LICENSE >> ${SCRIPT_DIR}/../LICENSE
\ No newline at end of file
diff --git a/scripts/build-ocl-windows.sh b/scripts/build-ocl-windows.sh
new file mode 100644
index 0000000000000000000000000000000000000000..01b7ef31207d9ca6409ec861f5b31ffdd77cca34
--- /dev/null
+++ b/scripts/build-ocl-windows.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+set -o xtrace
+
+git clone --branch v2022.01.04 https://github.com/KhronosGroup/OpenCL-ICD-Loader
+
+git clone --branch v2022.01.04 https://github.com/KhronosGroup/OpenCL-Headers
+
+
+cmake -D CMAKE_INSTALL_PREFIX=./OpenCL-Headers/install -S ./OpenCL-Headers -B ./OpenCL-Headers/build 
+cmake --build ./OpenCL-Headers/build --target install
+
+# if someone would like to try to create win32 wheels bellow lines may be useful
+# cmake -D CMAKE_PREFIX_PATH=${PWD}/OpenCL-Headers/install -DOPENCL_ICD_LOADER_HEADERS_DIR=${PWD}/OpenCL-Headers/install/include -S ./OpenCL-ICD-Loader -B ./OpenCL-ICD-Loader/build 
+# cmake --build ./OpenCL-ICD-Loader/build --target install --config Release
+
+cmake -D CMAKE_PREFIX_PATH=${PWD}/OpenCL-Headers/install -D OPENCL_ICD_LOADER_HEADERS_DIR=${PWD}/OpenCL-Headers/install/include -S ./OpenCL-ICD-Loader -B ./OpenCL-ICD-Loader/build2 -A x64
+cmake --build ./OpenCL-ICD-Loader/build2 --target install --config Release
+
+echo "PyOpenCL wheel includes Khronos Group OpenCL-ICD-Loader which is licensed as below" >> ${SCRIPT_DIR}/../LICENSE
+cat ./OpenCL-ICD-Loader/LICENSE >> ${SCRIPT_DIR}/../LICENSE
\ No newline at end of file
diff --git a/scripts/build-ocl.sh b/scripts/build-ocl.sh
new file mode 100644
index 0000000000000000000000000000000000000000..c1d97badde6c0e15397fbcd1a575a035655e942e
--- /dev/null
+++ b/scripts/build-ocl.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+set -e -x
+
+mkdir -p ~/deps
+cd ~/deps
+
+git clone --branch v2.3.1 https://github.com/OCL-dev/ocl-icd
+cd ocl-icd
+curl -L -O https://raw.githubusercontent.com/conda-forge/ocl-icd-feedstock/e2c03e3ddb1ff86630ccf80dc7b87a81640025ea/recipe/install-headers.patch
+git apply install-headers.patch
+curl -L -O https://github.com/isuruf/ocl-icd/commit/3862386b51930f95d9ad1089f7157a98165d5a6b.patch
+git apply 3862386b51930f95d9ad1089f7157a98165d5a6b.patch
+autoreconf -i
+chmod +x configure
+./configure --prefix=/usr
+make -j4
+make install
+
+# Bundle license files
+echo "PyOpenCL wheel includes ocl-icd which is licensed as below" >> ${SCRIPT_DIR}/../LICENSE
+cat ~/deps/ocl-icd/COPYING >> ${SCRIPT_DIR}/../LICENSE
\ No newline at end of file
diff --git a/scripts/build-wheels.sh b/scripts/build-wheels.sh
deleted file mode 100755
index 34c8cd714b914738ca44922211409dd79ed8e0d4..0000000000000000000000000000000000000000
--- a/scripts/build-wheels.sh
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/bin/bash
-set -e -x
-
-mkdir -p /deps
-cd /deps
-
-function start_spinner {
-    if [ -n "$SPINNER_PID" ]; then
-        return
-    fi
-
-    >&2 echo "Building libraries..."
-    # Start a process that runs as a keep-alive
-    # to avoid travis quitting if there is no output
-    (while true; do
-        sleep 60
-        >&2 echo "Still building..."
-    done) &
-    SPINNER_PID=$!
-    disown
-}
-
-function stop_spinner {
-    if [ ! -n "$SPINNER_PID" ]; then
-        return
-    fi
-
-    kill $SPINNER_PID
-    unset SPINNER_PID
-
-    >&2 echo "Building libraries finished."
-}
-
-#start_spinner
-
-git config --global --add safe.directory /io
-
-curl https://tiker.net/tmp/.tmux.conf
-yum install -y git yum openssl-devel ruby
-
-git clone --branch v2.3.0 https://github.com/OCL-dev/ocl-icd
-cd ocl-icd
-curl -L -O https://raw.githubusercontent.com/conda-forge/ocl-icd-feedstock/e2c03e3ddb1ff86630ccf80dc7b87a81640025ea/recipe/install-headers.patch
-git apply install-headers.patch
-curl -L -O https://github.com/isuruf/ocl-icd/commit/3862386b51930f95d9ad1089f7157a98165d5a6b.patch
-git apply 3862386b51930f95d9ad1089f7157a98165d5a6b.patch
-autoreconf -i
-chmod +x configure
-./configure --prefix=/usr
-make -j4
-make install
-cd ..
-
-# Bundle license files
-echo "PyOpenCL wheel includes ocl-icd which is licensed as below" >> /io/LICENSE
-cat /deps/ocl-icd/COPYING >> /io/LICENSE
-
-# Compile wheels
-for PYBIN in /opt/python/*/bin; do
-    # Build with the oldest numpy available to be compatible with newer ones
-    "${PYBIN}/pip" install oldest-supported-numpy pybind11 mako
-    "${PYBIN}/pip" wheel /io/ -w wheelhouse/ --no-deps
-done
-
-# Bundle external shared libraries into the wheels
-for whl in wheelhouse/pyopencl*.whl; do
-    auditwheel repair "$whl" -w /io/wheelhouse/ --lib-sdir=/.libs
-done
-
-if [[ "${TWINE_USERNAME}" == "" ]]; then
-    echo "TWINE_USERNAME not set. Skipping uploading wheels"
-    exit 0
-fi
-
-/opt/python/cp39-cp39/bin/pip install twine
-for WHEEL in /io/wheelhouse/pyopencl*.whl; do
-    # dev
-    # /opt/python/cp39-cp39/bin/twine upload \
-    #     --skip-existing \
-    #     --repository-url https://test.pypi.org/legacy/ \
-    #     -u "${TWINE_USERNAME}" -p "${TWINE_PASSWORD}" \
-    #     "${WHEEL}"
-    # prod
-    /opt/python/cp39-cp39/bin/twine upload \
-        --skip-existing \
-        -u "${TWINE_USERNAME}" -p "${TWINE_PASSWORD}" \
-        "${WHEEL}"
-done
-
-#stop_spinner
diff --git a/setup.py b/setup.py
index 31c096c3d1b71cd5f3b06d55cd930eef5c2b3316..00ddcb34951d6528e773bfd84d28417d3e2e1c41 100644
--- a/setup.py
+++ b/setup.py
@@ -31,6 +31,8 @@ import os
 import sys
 from os.path import exists
 
+sys.path.append(os.path.dirname(__file__))
+
 
 def get_config_schema():
     from aksetup_helper import (ConfigSchema, Option,
@@ -161,31 +163,6 @@ def main():
 
     exec(compile(version_file_contents, "pyopencl/version.py", "exec"), ver_dic)
 
-    try:
-        import mako  # noqa
-    except ImportError:
-        print(SEPARATOR)
-        print("Mako is not installed.")
-        print(SEPARATOR)
-        print("That is not a problem, as most of PyOpenCL will be just fine ")
-        print("without it. Some higher-level parts of pyopencl (such as ")
-        print("pyopencl.reduction) will not function without the templating engine ")
-        print("Mako [1] being installed. If you would like this functionality to ")
-        print("work, you might want to install Mako after you finish ")
-        print("installing PyOpenCL.")
-        print("")
-        print("Simply type")
-        print("python -m pip install mako")
-        print("either now or after the installation completes to fix this.")
-        print("")
-        print("[1] http://www.makotemplates.org/")
-        print(SEPARATOR)
-        print("Hit Ctrl-C now if you'd like to think about the situation.")
-        print(SEPARATOR)
-
-        from aksetup_helper import count_down_delay
-        count_down_delay(delay=5)
-
     if not exists("pyopencl/compyte/dtypes.py"):
         print(75 * "-")
         print("You are missing important files from the pyopencl distribution.")
@@ -263,6 +240,7 @@ def main():
             extras_require={
                 "pocl":  ["pocl_binary_distribution>=1.2"],
                 "oclgrind":  ["oclgrind_binary_distribution>=18.3"],
+                "test": ["pytest>=7.0.0", "Mako"],
             },
             include_package_data=True,
             package_data={
diff --git a/test/test_wrapper.py b/test/test_wrapper.py
index ff4ee0cdc909cba5ddca1ed5d03578faaf0bd788..1f94f640b1b5a724439be77d4bb645cb97ad2dc7 100644
--- a/test/test_wrapper.py
+++ b/test/test_wrapper.py
@@ -637,8 +637,8 @@ def test_vector_args(ctx_factory):
 def test_header_dep_handling(ctx_factory):
     context = ctx_factory()
 
-    from os.path import exists
-    assert exists("empty-header.h")  # if this fails, change dir to pyopencl/test
+    from os.path import exists, dirname, join
+    assert exists(join(dirname(__file__), "empty-header.h"))
 
     kernel_src = """
     #include <empty-header.h>
@@ -648,10 +648,8 @@ def test_header_dep_handling(ctx_factory):
     }
     """
 
-    import os
-
-    cl.Program(context, kernel_src).build(["-I", os.getcwd()])
-    cl.Program(context, kernel_src).build(["-I", os.getcwd()])
+    cl.Program(context, kernel_src).build(["-I", dirname(__file__)])
+    cl.Program(context, kernel_src).build(["-I", dirname(__file__)])
 
 
 def test_context_dep_memoize(ctx_factory):