diff --git a/install_spack_env.sh b/install_spack_env.sh
index 536696f8a4fa342bad3f9975bc2ff57f96d62add..86e62145c8eeaac083ac175428d7fa3f3d018846 100644
--- a/install_spack_env.sh
+++ b/install_spack_env.sh
@@ -76,7 +76,7 @@ cp /tmp/spack.yaml $SPACK_ROOT/var/spack/environments/$EBRAINS_SPACK_ENV/
 rm $SPACK_ROOT/var/spack/environments/$EBRAINS_SPACK_ENV/spack.lock || echo "No spack.lock file"
 
 # activate environment
-spack env activate $EBRAINS_SPACK_ENV
+spack env activate --without-view $EBRAINS_SPACK_ENV
 
 # fetch all sources
 spack concretize --fresh --test root
diff --git a/packages/build-brainscales/package.py b/packages/build-brainscales/package.py
new file mode 100644
index 0000000000000000000000000000000000000000..a162821d8753e6cb8c9dd17e8e5cea7713ea1c03
--- /dev/null
+++ b/packages/build-brainscales/package.py
@@ -0,0 +1,136 @@
+# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import os
+import unittest.mock
+import xml.etree.ElementTree as ET
+
+from spack import *
+from spack.util.environment import EnvironmentModifications
+import spack.build_environment
+
+
+class BuildBrainscales(WafPackage):
+    """Common stuff for BrainScaleS packages..."""
+
+    def do_fetch(self, mirror_only=False):
+        """Setup the project."""
+
+        self.stage.create()
+        self.stage.fetch(mirror_only)
+
+        # if fetcher didn't do anything, it's cached already
+        if not os.path.exists(self.stage.source_path):
+            return
+
+        with working_dir(self.stage.source_path):
+            python = which('python3')
+            if self.spec.satisfies('@:7'):
+                python('./waf', 'setup', '--repo-db-url=https://github.com/electronicvisions/projects',
+                    '--clone-depth=2',
+                    '--without-munge',
+                    '--without-hxcomm-hostarq',
+                    '--without-hxcomm-extoll',
+                    '--project=' + str(self.spec.name),
+                    '--release-branch=ebrains-' + str(self.spec.version)
+                )
+            else:
+                python('./waf', 'setup', '--repo-db-url=https://github.com/electronicvisions/projects',
+                    '--clone-depth=2',
+                    '--without-munge',
+                    '--without-hxcomm-hostarq',
+                    '--project=' + str(self.spec.name),
+                    '--release-branch=ebrains-' + str(self.spec.version)
+                )
+
+
+        # in the configure step, we need access to all archived .git folders
+        def custom_archive(self, destination):
+            super(spack.fetch_strategy.GitFetchStrategy, self).archive(destination)
+        with unittest.mock.patch('spack.fetch_strategy.GitFetchStrategy.archive', new=custom_archive):
+            self.stage.cache_local()
+
+    def _setup_common_env(self, env):
+        # grenade needs to find some libraries for the JIT-compilation of
+        # programs for BrainScaleS-2's embedded processor.
+        ppu_include_dirs = []
+        ppu_dep_names = ['bitsery', 'boost', 'cereal']
+        for ppu_dep_name in ppu_dep_names:
+            dep = self.spec[ppu_dep_name]
+            dep_include_dirs = set(dep.headers.directories)
+            ppu_include_dirs.extend(list(dep_include_dirs))
+        for dir in reversed(ppu_include_dirs):
+            env.prepend_path("C_INCLUDE_PATH", dir)
+            env.prepend_path("CPLUS_INCLUDE_PATH", dir)
+
+    def setup_build_environment(self, env):
+        my_envmod = EnvironmentModifications(env)
+        spack.build_environment.set_wrapper_variables(self, my_envmod)
+        my_env = {}
+        my_envmod.apply_modifications(my_env)
+
+        def get_path(env, name):
+            path = env.get(name, "").strip()
+            if path:
+                return path.split(os.pathsep)
+            return []
+
+        # spack tries to find headers and libraries by itself (i.e. it's not
+        # relying on the compiler to find it); we explicitly expose the
+        # spack-provided env vars that contain include and library paths
+        if 'SPACK_INCLUDE_DIRS' in my_env:
+            for dir in reversed(get_path(my_env, "SPACK_INCLUDE_DIRS")):
+                env.prepend_path("C_INCLUDE_PATH", dir)
+                env.prepend_path("CPLUS_INCLUDE_PATH", dir)
+        if 'SPACK_LINK_DIRS' in my_env:
+            for dir in reversed(get_path(my_env, "SPACK_LINK_DIRS")):
+                env.prepend_path("LIBRARY_PATH", dir)
+                env.prepend_path("LD_LIBRARY_PATH", dir)
+        for dir in reversed(self.compiler.implicit_rpaths()):
+            env.prepend_path("LIBRARY_PATH", dir)
+            # technically this is probably not needed for the non-configure steps
+            env.prepend_path("LD_LIBRARY_PATH", dir)
+
+    def setup_dependent_build_environment(self, env, dependent_spec):
+        self._setup_common_env(env)
+
+    def setup_run_environment(self, env):
+        self._setup_common_env(env)
+
+    def setup_dependent_run_environment(self, env, dependent_spec):
+        self._setup_common_env(env)
+
+    # override configure step as we perform a project setup first
+    def configure(self, spec, prefix):
+        """Configure the project."""
+
+        args = ['--prefix={0}'.format(self.prefix)]
+        args += self.configure_args()
+        self.waf('configure', '--build-profile=release', '--disable-doxygen', *args)
+
+    def build_args(self):
+        args = ['--keep', '--test-execnone', '-v']
+        return args
+
+    def build_test(self):
+        self.builder.waf('build', '--test-execall')
+        copy_tree('build/test_results', join_path(self.prefix, '.build'))
+        copy_tree('build/test_results', join_path(self.stage.path, ".install_time_tests"))
+        # propagate failures from junit output to spack
+        tree = ET.parse('build/test_results/summary.xml')
+        for testsuite in tree.getroot():
+            for testcase in testsuite:
+                if (testcase.get('name').startswith("pycodestyle") or
+                    testcase.get('name').startswith("pylint")):
+                    continue
+                for elem in testcase:
+                    if (elem.tag == 'failure') and not (
+                            elem.get('message').startswith("pylint:") or
+                            elem.get('message').startswith("pycodestyle:") or
+                            ("OK" in elem.get('message') and "Segmentation fault" in elem.get('message'))):
+                        raise RuntimeError("Failed test found: {}".format(testcase.get('name')))
+
+    def install_args(self):
+        args = ['--test-execnone']
+        return args
diff --git a/packages/hxtorch/package.py b/packages/hxtorch/package.py
index bff7e733e9fa3226d760decfc0cd2280718ce3ec..f9751d98611bb07b3afb9313834ac6a8680ae5d3 100644
--- a/packages/hxtorch/package.py
+++ b/packages/hxtorch/package.py
@@ -1,18 +1,20 @@
-# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
+# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
 # Spack Project Developers. See the top-level COPYRIGHT file for details.
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 import os
 import unittest.mock
-import sys
 import xml.etree.ElementTree as ET
 
 from spack import *
 from spack.util.environment import EnvironmentModifications
 import spack.build_environment
 
+import importlib
+build_brainscales = importlib.import_module("spack.pkg.ebrains-spack-builds.build_brainscales")
 
-class Hxtorch(WafPackage):
+
+class Hxtorch(build_brainscales.BuildBrainscales):
     """hxtorch --- a PyTorch-based toplevel for the BrainScaleS-2 neuromorphic hardware systems"""
 
     homepage = "https://github.com/electronicvisions/hxtorch"
@@ -77,123 +79,6 @@ class Hxtorch(WafPackage):
     depends_on('yaml-cpp+shared', type=('build', 'link', 'run'))
     extends('python')
 
-    def do_fetch(self, mirror_only=False):
-        """Setup the project."""
-
-        self.stage.create()
-        self.stage.fetch(mirror_only)
-
-        # if fetcher didn't do anything, it's cached already
-        if not os.path.exists(self.stage.source_path):
-            return
-
-        with working_dir(self.stage.source_path):
-            python = which('python3')
-            if self.spec.satisfies('@:7'):
-                python('./waf', 'setup', '--repo-db-url=https://github.com/electronicvisions/projects',
-                    '--clone-depth=2',
-                    '--without-munge',
-                    '--without-hxcomm-hostarq',
-                    '--without-hxcomm-extoll',
-                    '--project=hxtorch',
-                    '--release-branch=ebrains-' + str(self.spec.version)
-                )
-            else:
-                python('./waf', 'setup', '--repo-db-url=https://github.com/electronicvisions/projects',
-                    '--clone-depth=2',
-                    '--without-munge',
-                    '--without-hxcomm-hostarq',
-                    '--project=hxtorch',
-                    '--release-branch=ebrains-' + str(self.spec.version)
-                )
-
-        # in the configure step, we need access to all archived .git folders
-        def custom_archive(self, destination):
-            super(spack.fetch_strategy.GitFetchStrategy, self).archive(destination)
-        with unittest.mock.patch('spack.fetch_strategy.GitFetchStrategy.archive', new=custom_archive):
-            self.stage.cache_local()
-
-    def _setup_common_env(self, env):
-        # grenade needs to find some libraries for the JIT-compilation of
-        # programs for BrainScaleS-2's embedded processor.
-        ppu_include_dirs = []
-        ppu_dep_names = ['bitsery', 'boost', 'cereal']
-        for ppu_dep_name in ppu_dep_names:
-            dep = self.spec[ppu_dep_name]
-            dep_include_dirs = set(dep.headers.directories)
-            ppu_include_dirs.extend(list(dep_include_dirs))
-        for dir in reversed(ppu_include_dirs):
-            env.prepend_path("C_INCLUDE_PATH", dir)
-            env.prepend_path("CPLUS_INCLUDE_PATH", dir)
-
-    def setup_build_environment(self, env):
-        my_envmod = EnvironmentModifications(env)
-        spack.build_environment.set_wrapper_variables(self, my_envmod)
-        my_env = {}
-        my_envmod.apply_modifications(my_env)
-
-        def get_path(env, name):
-            path = env.get(name, "").strip()
-            if path:
-                return path.split(os.pathsep)
-            return []
-
-        # spack tries to find headers and libraries by itself (i.e. it's not
-        # relying on the compiler to find it); we explicitly expose the
-        # spack-provided env vars that contain include and library paths
-        if 'SPACK_INCLUDE_DIRS' in my_env:
-            for dir in reversed(get_path(my_env, "SPACK_INCLUDE_DIRS")):
-                env.prepend_path("C_INCLUDE_PATH", dir)
-                env.prepend_path("CPLUS_INCLUDE_PATH", dir)
-        if 'SPACK_LINK_DIRS' in my_env:
-            for dir in reversed(get_path(my_env, "SPACK_LINK_DIRS")):
-                env.prepend_path("LIBRARY_PATH", dir)
-                env.prepend_path("LD_LIBRARY_PATH", dir)
-        for dir in reversed(self.compiler.implicit_rpaths()):
-            env.prepend_path("LIBRARY_PATH", dir)
-            # technically this is probably not needed for the non-configure steps
-            env.prepend_path("LD_LIBRARY_PATH", dir)
-
-    def setup_dependent_build_environment(self, env, dependent_spec):
-        self._setup_common_env(env)
-
-    def setup_run_environment(self, env):
-        self._setup_common_env(env)
-
-    def setup_dependent_run_environment(self, env, dependent_spec):
-        self._setup_common_env(env)
-
-    # override configure step as we perform a project setup first
-    def configure(self, spec, prefix):
-        """Configure the project."""
-
-        args = ['--prefix={0}'.format(self.prefix)]
-        args += self.configure_args()
-        self.waf('configure', '--build-profile=release', '--disable-doxygen', *args)
-
-    def build_args(self):
-        args = ['--keep', '--test-execnone', '-v']
-        return args
-
-    def build_test(self):
-        self.builder.waf('build', '--test-execall')
-        copy_tree('build/test_results', join_path(self.prefix, ".build"))
-        copy_tree('build/test_results', join_path(self.stage.path, ".install_time_tests"))
-        # propagate failures from junit output to spack
-        tree = ET.parse('build/test_results/summary.xml')
-        for testsuite in tree.getroot():
-            for testcase in testsuite:
-                for elem in testcase:
-                    if (elem.tag == 'failure') and not (
-                            elem.get('message').startswith("pylint:") or
-                            elem.get('message').startswith("pycodestyle:") or
-                            ("OK" in elem.get('message') and "Segmentation fault" in elem.get('message'))):
-                        raise RuntimeError("Failed test found: {}".format(testcase.get('name')))
-
-    def install_args(self):
-        args = ['--test-execnone']
-        return args
-
     def install_test(self):
         with working_dir('spack-test', create=True):
             old_pythonpath = os.environ.get('PYTHONPATH', '')
diff --git a/packages/jaxsnn/package.py b/packages/jaxsnn/package.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd66a37746f6c30760e1818e278a51e751fc2709
--- /dev/null
+++ b/packages/jaxsnn/package.py
@@ -0,0 +1,50 @@
+# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import os
+import unittest.mock
+import xml.etree.ElementTree as ET
+
+from spack import *
+from spack.util.environment import EnvironmentModifications
+import spack.build_environment
+
+import importlib
+build_brainscales = importlib.import_module("spack.pkg.ebrains-spack-builds.build_brainscales")
+
+
+class Jaxsnn(build_brainscales.BuildBrainscales):
+    """jaxsnn is an event-based approach to machine-learning-inspired training
+    and simulation of SNNs, including support for the BrainScaleS-2
+    neuromorphic backend."""
+
+    homepage = "https://github.com/electronicvisions/jaxsnn"
+    # This repo provides a custom waf binary used for the build below
+    git      = "https://github.com/electronicvisions/pynn-brainscales.git"
+
+    maintainers = ['emuller']
+
+    version('8.0-a3', tag='jaxsnn-8.0-a3')
+    version('8.0-a2', tag='jaxsnn-8.0-a2')
+    version('8.0-a1', tag='jaxsnn-8.0-a1')
+
+    # for now, this is still "on top" of hxtorch…
+    depends_on('hxtorch@8.0-a3', when='@8.0-a3', type=('build', 'link', 'run', 'test'))
+    depends_on('hxtorch@8.0-a2', when='@8.0-a2', type=('build', 'link', 'run', 'test'))
+    depends_on('hxtorch@8.0-a1', when='@8.0-a1', type=('build', 'link', 'run', 'test'))
+
+    # main dependencies w/o hxtorch.core dependencies (those come via hxtorch above)
+    depends_on('py-jax@0.4.13:', type=('build', 'link', 'run'))
+    depends_on('py-matplotlib', type=('build', 'link', 'run'))
+    depends_on('py-optax', type=('build', 'link', 'run'))
+    depends_on('py-tree-math', type=('build', 'link', 'run'))
+    extends('python')
+
+    def install_test(self):
+        with working_dir('spack-test', create=True):
+            old_pythonpath = os.environ.get('PYTHONPATH', '')
+            os.environ['PYTHONPATH'] = ':'.join([str(self.prefix.lib), old_pythonpath])
+            bash = which("bash")
+            # ignore segfaults for now (exit code 139)
+            bash('-c', '(python -c "import jaxsnn; print(jaxsnn.__file__)" || ( test $? -eq 139 && echo "segfault")) || exit $?')
diff --git a/packages/nest/package.py b/packages/nest/package.py
index f88e8f3ef172e99f4c8a290ccfcb698e4b4e9a86..e1593d4bfcbbad9b8566fdda5a2aada527b6e8ce 100644
--- a/packages/nest/package.py
+++ b/packages/nest/package.py
@@ -28,6 +28,7 @@ class Nest(CMakePackage):
     maintainers = ['terhorst']
 
     version('master', branch='master')
+    version('3.7_rc1',    sha256='6f5948ac717d4b66c84ed53ba5c9b0e44e4e9cc3c1f3d15d8d8f21024313061e')
     version('3.6',    sha256='68d6b11791e1284dc94fef35d84c08dd7a11322c0f1e1fc9b39c5e6882284922')
     patch('nest-simulator-3.6-p1-CxxRealPath.patch', when='@3.6')
     version('3.5',    sha256='3cdf5720854a4d8a7d359f9de9d2fb3619a0be2e36932028d6940360741547bd')
@@ -93,7 +94,8 @@ class Nest(CMakePackage):
     depends_on('boost',             when="@2.16:+boost", type='build')
     depends_on('py-setuptools@:44.99.99',     when='@:2.15.99+python', type='build')
     depends_on('py-h5py',           when='@3.4.99:+sonata', type=('test', 'build', 'run'))
-    depends_on('hdf5+cxx',          when='@3.4.99:+sonata', type=('build', 'link', 'run'))
+    depends_on('hdf5+cxx+mpi',      when='@3.4.99:+sonata+mpi', type=('build', 'run'))
+    depends_on('hdf5+cxx~mpi',      when='@3.4.99:+sonata~mpi', type=('build', 'run'))
     depends_on('py-pandas',         when='@3.4.99:+sonata', type=('build', 'run'))
 
     depends_on('py-nose',           when='@:2.99.99+python+testsuite', type='test')
diff --git a/packages/py-chex/package.py b/packages/py-chex/package.py
new file mode 100644
index 0000000000000000000000000000000000000000..7e41890f4588105fc58e040d90b9f4a1fa84540c
--- /dev/null
+++ b/packages/py-chex/package.py
@@ -0,0 +1,36 @@
+# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+
+from spack.package import *
+
+
+# EBRAINS: based on spack/0.21.2
+class PyChex(PythonPackage):
+    """Chex is a library of utilities for helping to write reliable JAX code."""
+
+    homepage = "https://github.com/deepmind/chex"
+    pypi = "chex/chex-0.1.0.tar.gz"
+
+    # begin EBRAINS (added): bring upstream
+    version("0.1.7", sha256="74ed49799ac4d229881456d468136f1b19a9f9839e3de72b058824e2a4f4dedd")
+    version("0.1.5", sha256="686858320f8f220c82a6c7eeb54dcdcaa4f3d7f66690dacd13a24baa1ee8299e")
+    # end EBRAINS
+    version("0.1.0", sha256="9e032058f5fed2fc1d5e9bf8e12ece5910cf6a478c12d402b6d30984695f2161")
+
+    depends_on("python@3.7:", type=("build", "run"))
+    depends_on("py-setuptools", type="build")
+    depends_on("py-absl-py@0.9.0:", type=("build", "run"))
+    # begin EBRAINS (added): bring upstream
+    depends_on("py-typing-extensions@4.2.0:", when="@0.1.6: ^python@:3.10", type=("build", "run"))
+    # end EBRAINS
+    depends_on("py-dm-tree@0.1.5:", type=("build", "run"))
+    depends_on("py-jax@0.1.55:", type=("build", "run"))
+    # begin EBRAINS (added): bring upstream
+    depends_on("py-jax@0.4.6:", when="@0.1.7:", type=("build", "run"))
+    # end EBRAINS
+    depends_on("py-jaxlib@0.1.37:", type=("build", "run"))
+    depends_on("py-numpy@1.18.0:", type=("build", "run"))
+    depends_on("py-toolz@0.9.0:", type=("build", "run"))
diff --git a/packages/py-jax/package.py b/packages/py-jax/package.py
new file mode 100644
index 0000000000000000000000000000000000000000..233de5b59b40af38d3d4e604477eb91e2b683b3a
--- /dev/null
+++ b/packages/py-jax/package.py
@@ -0,0 +1,72 @@
+# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+
+from spack.package import *
+
+
+# EBRAINS: based on spack/0.21.2
+class PyJax(PythonPackage):
+    """JAX is Autograd and XLA, brought together for high-performance
+    machine learning research. With its updated version of Autograd,
+    JAX can automatically differentiate native Python and NumPy
+    functions. It can differentiate through loops, branches,
+    recursion, and closures, and it can take derivatives of
+    derivatives of derivatives. It supports reverse-mode
+    differentiation (a.k.a. backpropagation) via grad as well as
+    forward-mode differentiation, and the two can be composed
+    arbitrarily to any order."""
+
+    homepage = "https://github.com/google/jax"
+    pypi = "jax/jax-0.2.25.tar.gz"
+
+    # begin EBRAINS (added): bring upstream
+    version("0.4.13", sha256="03bfe6749dfe647f16f15f6616638adae6c4a7ca7167c75c21961ecfd3a3baaa")
+    # end EBRAINS
+    version("0.4.3", sha256="d43f08f940aa30eb339965cfb3d6bee2296537b0dc2f0c65ccae3009279529ae")
+    version("0.3.23", sha256="bff436e15552a82c0ebdef32737043b799e1e10124423c57a6ae6118c3a7b6cd")
+    version("0.2.25", sha256="822e8d1e06257eaa0fdc4c0a0686c4556e9f33647fa2a766755f984786ae7446")
+
+    # begin EBRAINS (modified): bring upstream
+    depends_on("python@3.7:", type=("build", "run"))
+    depends_on("python@3.8:", when="@0.4:", type=("build", "run"))
+    depends_on("python@3.9:", when="@0.4.14:", type=("build", "run"))
+    depends_on("py-setuptools", type="build")
+    depends_on("py-numpy@1.22:", when="@0.4.14:", type=("build", "run"))
+    depends_on("py-numpy@1.21:", when="@0.4.9:", type=("build", "run"))
+    depends_on("py-numpy@1.20:", when="@0.3:", type=("build", "run"))
+    depends_on("py-numpy@1.18:", type=("build", "run"))
+    depends_on("py-opt-einsum", type=("build", "run"))
+    depends_on("py-scipy@1.2.1:", type=("build", "run"))
+    depends_on("py-scipy@1.5:", when="@0.3:", type=("build", "run"))
+    depends_on("py-scipy@1.7:", when="@0.4.7:", type=("build", "run"))
+    depends_on("py-ml-dtypes@0.2.0:", when="@0.4.14:", type=("build", "run"))
+    depends_on("py-ml-dtypes@0.1.0:", when="@0.4.9:", type=("build", "run"))
+    depends_on("py-ml-dtypes@0.0.3:", when="@0.4.7:", type=("build", "run"))
+    depends_on("py-importlib-metadata@4.6:", when="@0.4.11: ^python@:3.9", type="run")
+    # end EBRAINS
+
+    # See _minimum_jaxlib_version in jax/version.py
+    # begin EBRAINS (modified): bring upstream
+    jax_to_jaxlib = {
+        "0.4.14": "0.4.14",
+        "0.4.13": "0.4.13",
+        "0.4.3": "0.4.2",
+        "0.3.23": "0.3.15",
+        "0.2.25": "0.1.69",
+    }
+    # end EBRAINS
+
+    for jax, jaxlib in jax_to_jaxlib.items():
+        # begin EBRAINS (modified): bring upstream
+        depends_on(f"py-jaxlib@{jaxlib}", when=f"@{jax}", type=("build", "run"))
+        # end EBRAINS
+
+    # Historical dependencies
+    depends_on("py-absl-py", when="@:0.3", type=("build", "run"))
+    depends_on("py-typing-extensions", when="@:0.3", type=("build", "run"))
+    # begin EBRAINS (deleted):
+    # depends_on("py-etils+epath", when="@0.3", type=("build", "run"))
+    # end EBRAINS
diff --git a/packages/py-jaxlib/package.py b/packages/py-jaxlib/package.py
new file mode 100644
index 0000000000000000000000000000000000000000..af04f737eceb3cfa00f7538da14dd8d5043df7cb
--- /dev/null
+++ b/packages/py-jaxlib/package.py
@@ -0,0 +1,109 @@
+# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+import tempfile
+
+from spack.package import *
+
+
+# EBRAINS: based on spack/0.21.2
+class PyJaxlib(PythonPackage, CudaPackage):
+    """XLA library for Jax"""
+
+    homepage = "https://github.com/google/jax"
+    url = "https://github.com/google/jax/archive/refs/tags/jaxlib-v0.1.74.tar.gz"
+
+    tmp_path = ""
+    buildtmp = ""
+
+    # begin EBRAINS (added): bring upstream
+    version("0.4.13", sha256="45766238b57b992851763c64bc943858aebafe4cad7b3df6cde844690bc34293")
+    # end EBRAINS
+    version("0.4.3", sha256="2104735dc22be2b105e5517bd5bc6ae97f40e8e9e54928cac1585c6112a3d910")
+    version("0.3.22", sha256="680a6f5265ba26d5515617a95ae47244005366f879a5c321782fde60f34e6d0d")
+    version("0.1.74", sha256="bbc78c7a4927012dcb1b7cd135c7521f782d7dad516a2401b56d3190f81afe35")
+
+    # begin EBRAINS (deleted): Variant with default=False is provided by CudaPackage
+    # variant("cuda", default=True, description="Build with CUDA")
+    # end EBRAINS
+
+    # jaxlib/setup.py
+    # begin EBRAINS (modified): bring upstream
+    depends_on("python@3.9:", when="@0.4.14:", type=("build", "run"))
+    depends_on("python@3.8:", when="@0.4:", type=("build", "run"))
+    depends_on("python@3.7:", type=("build", "run"))
+    depends_on("py-setuptools", type="build")
+    depends_on("py-numpy@1.22:", when="@0.4.14:", type=("build", "run"))
+    depends_on("py-numpy@1.21:", when="@0.4.9:", type=("build", "run"))
+    depends_on("py-numpy@1.20:", when="@0.3:", type=("build", "run"))
+    depends_on("py-numpy@1.18:", type=("build", "run"))
+    depends_on("py-scipy@1.5:", type=("build", "run"))
+    depends_on("py-scipy@1.7:", when="@0.4.7:", type=("build", "run"))
+    depends_on("py-ml-dtypes@0.2.0:", when="@0.4.14:", type=("build", "run"))
+    depends_on("py-ml-dtypes@0.1.0:", when="@0.4.9:", type=("build", "run"))
+    depends_on("py-ml-dtypes@0.0.3:", when="@0.4.7:", type=("build", "run"))
+    # end EBRAINS
+
+    # .bazelversion
+    depends_on("bazel@5.1.1:5.9", when="@0.3:", type="build")
+    # https://github.com/google/jax/issues/8440
+    depends_on("bazel@4.1:4", when="@0.1", type="build")
+
+    # README.md
+    # begin EBRAINS (added): bring upstream
+    depends_on("cuda@11.8:", when="@0.4.8:+cuda")
+    # end EBRAINS
+    depends_on("cuda@11.4:", when="@0.4:+cuda")
+    depends_on("cuda@11.1:", when="@0.3+cuda")
+    # https://github.com/google/jax/issues/12614
+    depends_on("cuda@11.1:11.7.0", when="@0.1+cuda")
+    depends_on("cudnn@8.2:", when="@0.4:+cuda")
+    depends_on("cudnn@8.0.5:", when="+cuda")
+
+    # Historical dependencies
+    depends_on("py-absl-py", when="@:0.3", type=("build", "run"))
+    depends_on("py-flatbuffers@1.12:2", when="@0.1", type=("build", "run"))
+
+    def patch(self):
+        self.tmp_path = tempfile.mkdtemp(prefix="spack")
+        self.buildtmp = tempfile.mkdtemp(prefix="spack")
+        filter_file(
+            'f"--output_path={output_path}",',
+            'f"--output_path={output_path}",'
+            f' "--sources_path={self.tmp_path}",'
+            ' "--nohome_rc",'
+            ' "--nosystem_rc",'
+            f' "--jobs={make_jobs}",',
+            "build/build.py",
+            string=True,
+        )
+        filter_file(
+            "args = parser.parse_args()",
+            "args, junk = parser.parse_known_args()",
+            "build/build_wheel.py",
+            string=True,
+        )
+
+    def install(self, spec, prefix):
+        args = []
+        args.append("build/build.py")
+        if "+cuda" in spec:
+            args.append("--enable_cuda")
+            args.append("--cuda_path={0}".format(self.spec["cuda"].prefix))
+            args.append("--cudnn_path={0}".format(self.spec["cudnn"].prefix))
+            capabilities = ",".join(
+                "{0:.1f}".format(float(i) / 10.0) for i in spec.variants["cuda_arch"].value
+            )
+            args.append("--cuda_compute_capabilities={0}".format(capabilities))
+        args.append(
+            "--bazel_startup_options="
+            "--output_user_root={0}".format(self.wrapped_package_object.buildtmp)
+        )
+        python(*args)
+        with working_dir(self.wrapped_package_object.tmp_path):
+            args = std_pip_args + ["--prefix=" + self.prefix, "."]
+            pip(*args)
+        remove_linked_tree(self.wrapped_package_object.tmp_path)
+        remove_linked_tree(self.wrapped_package_object.buildtmp)
diff --git a/packages/py-optax/package.py b/packages/py-optax/package.py
new file mode 100644
index 0000000000000000000000000000000000000000..c5cf8840ec3e0fb1add31267cbba8879b1521d89
--- /dev/null
+++ b/packages/py-optax/package.py
@@ -0,0 +1,24 @@
+# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+from spack import *
+
+
+class PyOptax(PythonPackage):
+    """A gradient processing and optimization library in JAX."""
+
+    homepage = "https://github.com/deepmind/optax"
+    pypi = "optax/optax-0.1.4.tar.gz"
+
+    version("0.1.4", sha256="fb7a0550d57a6636164a3de25986a8a19be8ff6431fcdf1225b4e05175810f22")
+
+    depends_on("python@3.8:", type=("build", "run"))
+    depends_on("py-setuptools", type="build")
+
+    depends_on("py-absl-py@0.7.1:", type=("build", "run"))
+    depends_on("py-chex@0.1.5:", type=("build", "run"))
+    depends_on("py-jax@0.1.55:", type=("build", "run"))
+    depends_on("py-jaxlib@0.1.37:", type=("build", "run"))
+    depends_on("py-numpy@1.18:", type=("build", "run"))
diff --git a/packages/pynn-brainscales/package.py b/packages/pynn-brainscales/package.py
index e61b7b61fde63c2b60d7a1a0b6d4dec6b06294d3..39f07d8129ee0c8d0b8f151cceb746d0dd762dc1 100644
--- a/packages/pynn-brainscales/package.py
+++ b/packages/pynn-brainscales/package.py
@@ -1,18 +1,20 @@
-# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
+# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
 # Spack Project Developers. See the top-level COPYRIGHT file for details.
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 import os
 import unittest.mock
-import sys
 import xml.etree.ElementTree as ET
 
 from spack import *
 from spack.util.environment import EnvironmentModifications
 import spack.build_environment
 
+import importlib
+build_brainscales = importlib.import_module("spack.pkg.ebrains-spack-builds.build_brainscales")
 
-class PynnBrainscales(WafPackage):
+
+class PynnBrainscales(build_brainscales.BuildBrainscales):
     """PyNN toplevel for the BrainScaleS-2 neuromorphic hardware systems"""
 
     homepage = "https://github.com/electronicvisions/pynn-brainscales"
@@ -74,124 +76,6 @@ class PynnBrainscales(WafPackage):
     depends_on('yaml-cpp+shared', type=('build', 'link', 'run'))
     extends('python')
 
-    def do_fetch(self, mirror_only=False):
-        """Setup the project."""
-
-        self.stage.create()
-        self.stage.fetch(mirror_only)
-
-        # if fetcher didn't do anything, it's cached already
-        if not os.path.exists(self.stage.source_path):
-            return
-
-        with working_dir(self.stage.source_path):
-            python = which('python3')
-            if self.spec.satisfies('@:7'):
-                python('./waf', 'setup', '--repo-db-url=https://github.com/electronicvisions/projects',
-                    '--clone-depth=2',
-                    '--without-munge',
-                    '--without-hxcomm-hostarq',
-                    '--without-hxcomm-extoll',
-                    '--project=pynn-brainscales',
-                    '--release-branch=ebrains-' + str(self.spec.version)
-                )
-            else:
-                python('./waf', 'setup', '--repo-db-url=https://github.com/electronicvisions/projects',
-                    '--clone-depth=2',
-                    '--without-munge',
-                    '--without-hxcomm-hostarq',
-                    '--project=pynn-brainscales',
-                    '--release-branch=ebrains-' + str(self.spec.version)
-                )
-
-
-        # in the configure step, we need access to all archived .git folders
-        def custom_archive(self, destination):
-            super(spack.fetch_strategy.GitFetchStrategy, self).archive(destination)
-        with unittest.mock.patch('spack.fetch_strategy.GitFetchStrategy.archive', new=custom_archive):
-            self.stage.cache_local()
-
-    def _setup_common_env(self, env):
-        # grenade needs to find some libraries for the JIT-compilation of
-        # programs for BrainScaleS-2's embedded processor.
-        ppu_include_dirs = []
-        ppu_dep_names = ['bitsery', 'boost', 'cereal']
-        for ppu_dep_name in ppu_dep_names:
-            dep = self.spec[ppu_dep_name]
-            dep_include_dirs = set(dep.headers.directories)
-            ppu_include_dirs.extend(list(dep_include_dirs))
-        for dir in reversed(ppu_include_dirs):
-            env.prepend_path("C_INCLUDE_PATH", dir)
-            env.prepend_path("CPLUS_INCLUDE_PATH", dir)
-
-    def setup_build_environment(self, env):
-        my_envmod = EnvironmentModifications(env)
-        spack.build_environment.set_wrapper_variables(self, my_envmod)
-        my_env = {}
-        my_envmod.apply_modifications(my_env)
-
-        def get_path(env, name):
-            path = env.get(name, "").strip()
-            if path:
-                return path.split(os.pathsep)
-            return []
-
-        # spack tries to find headers and libraries by itself (i.e. it's not
-        # relying on the compiler to find it); we explicitly expose the
-        # spack-provided env vars that contain include and library paths
-        if 'SPACK_INCLUDE_DIRS' in my_env:
-            for dir in reversed(get_path(my_env, "SPACK_INCLUDE_DIRS")):
-                env.prepend_path("C_INCLUDE_PATH", dir)
-                env.prepend_path("CPLUS_INCLUDE_PATH", dir)
-        if 'SPACK_LINK_DIRS' in my_env:
-            for dir in reversed(get_path(my_env, "SPACK_LINK_DIRS")):
-                env.prepend_path("LIBRARY_PATH", dir)
-                env.prepend_path("LD_LIBRARY_PATH", dir)
-        for dir in reversed(self.compiler.implicit_rpaths()):
-            env.prepend_path("LIBRARY_PATH", dir)
-            # technically this is probably not needed for the non-configure steps
-            env.prepend_path("LD_LIBRARY_PATH", dir)
-
-    def setup_dependent_build_environment(self, env, dependent_spec):
-        self._setup_common_env(env)
-
-    def setup_run_environment(self, env):
-        self._setup_common_env(env)
-
-    def setup_dependent_run_environment(self, env, dependent_spec):
-        self._setup_common_env(env)
-
-    # override configure step as we perform a project setup first
-    def configure(self, spec, prefix):
-        """Configure the project."""
-
-        args = ['--prefix={0}'.format(self.prefix)]
-        args += self.configure_args()
-        self.waf('configure', '--build-profile=release', '--disable-doxygen', *args)
-
-    def build_args(self):
-        args = ['--keep', '--test-execnone', '-v']
-        return args
-
-    def build_test(self):
-        self.builder.waf('build', '--test-execall')
-        copy_tree('build/test_results', join_path(self.prefix, ".build"))
-        copy_tree('build/test_results', join_path(self.stage.path, ".install_time_tests"))
-        # propagate failures from junit output to spack
-        tree = ET.parse('build/test_results/summary.xml')
-        for testsuite in tree.getroot():
-            for testcase in testsuite:
-                for elem in testcase:
-                    if (elem.tag == 'failure') and not (
-                            elem.get('message').startswith("pylint:") or
-                            elem.get('message').startswith("pycodestyle:") or
-                            ("OK" in elem.get('message') and "Segmentation fault" in elem.get('message'))):
-                        raise RuntimeError("Failed test found: {}".format(testcase.get('name')))
-
-    def install_args(self):
-        args = ['--test-execnone']
-        return args
-
     def install_test(self):
         with working_dir('spack-test', create=True):
             old_pythonpath = os.environ.get('PYTHONPATH', '')
diff --git a/packages/wf-multi-area-model/package.py b/packages/wf-multi-area-model/package.py
index 76db0cf2097ab9dab37df5f290378ba74535d8d3..bdfc5a549feab284b2d5b671b99ea5fde3739373 100644
--- a/packages/wf-multi-area-model/package.py
+++ b/packages/wf-multi-area-model/package.py
@@ -28,6 +28,7 @@ class WfMultiAreaModel(Package):
     depends_on("py-elephant", type=("run", "test"))
     depends_on("r-aod", type=("run", "test"))
     depends_on("py-notebook", type=("run", "test"))
+    depends_on("py-python-louvain", type=("run", "test"))
 
     def install(self, spec, prefix):
         install_tree(".", join_path(prefix, "notebooks"))
diff --git a/spack.yaml b/spack.yaml
index 726a38bc6b4b43582c064fcba2078aecbf370e34..9fc9e2c3f6737948b11f1ce1e7bfa127eb2be5f5 100644
--- a/spack.yaml
+++ b/spack.yaml
@@ -13,8 +13,9 @@ spack:
     - biobb-structure-checking@3.12.1
     - biobb-structure-utils@4.0.0
     - hxtorch@8.0-a3
-    - nest@3.6 +sonata
+    - nest@3.7_rc1 +sonata
     - neuron@8.2.3 +mpi
+    - jaxsnn@8.0-a3
     - py-bluepyefe@2.2.18
     - py-bluepymm@0.7.65
     - py-bluepyopt@1.13.86