From 04762cf28d68d36274902e0ffcab3095bfbf273b Mon Sep 17 00:00:00 2001
From: Thorsten Hater <24411438+thorstenhater@users.noreply.github.com>
Date: Tue, 20 Sep 2022 10:02:15 +0200
Subject: [PATCH] generate-catalogue is gone (#1975)

This is prep for meson in addition to simplifying (ie reducing) the amount of scripting
we have. In the long run, I'd like to remove `BuildModules.cmake` too, but this might
be superseded by meson and this PR is self-contained.

# Changes
- `modcc` accepts now a list of NMODL files
- likewise, `modcc` is now able to spit out a catalogue.cpp file
- remove the `generate-catalogue` script, simplify a-b-c
- remove some options from `modcc` we never use
- No longer allow external modcc
---
 CMakeLists.txt                                |  15 +-
 arbor/CMakeLists.txt                          |   9 +-
 arbor/include/arbor/mechcat.hpp               |   1 -
 doc/dev/extending_catalogues.rst              |  30 +-
 mechanisms/BuildModules.cmake                 | 181 +++++-------
 mechanisms/CMakeLists.txt                     |  39 +--
 mechanisms/default/pas.mod                    |   1 -
 mechanisms/generate_catalogue                 | 170 -----------
 modcc/CMakeLists.txt                          |  23 +-
 modcc/modcc.cpp                               | 275 ++++++++++++------
 modcc/printer/cprinter.cpp                    |   2 +-
 scripts/build-catalogue.in                    |   7 +-
 test/unit-modcc/CMakeLists.txt                |   4 +-
 test/unit/CMakeLists.txt                      |  80 ++---
 test/unit/test_mechcat.cpp                    |   2 -
 test/unit/{mod => testing}/ca_linear.mod      |   0
 test/unit/{mod => testing}/celsius_test.mod   |   0
 test/unit/{mod => testing}/diam_test.mod      |   0
 .../{mod => testing}/fixed_ica_current.mod    |   0
 test/unit/{mod => testing}/gj0.mod            |   0
 test/unit/{mod => testing}/gj1.mod            |   0
 test/unit/{mod => testing}/linear_ca_conc.mod |   0
 test/unit/{mod => testing}/non_linear.mod     |   0
 test/unit/{mod => testing}/param_as_state.mod |   0
 .../{mod => testing}/point_ica_current.mod    |   0
 .../unit/{mod => testing}/post_events_syn.mod |   0
 test/unit/{mod => testing}/read_cai_init.mod  |   0
 test/unit/{mod => testing}/read_eX.mod        |   0
 .../test0_kin_compartment.mod                 |   0
 .../{mod => testing}/test0_kin_conserve.mod   |   0
 test/unit/{mod => testing}/test0_kin_diff.mod |   0
 .../test0_kin_steadystate.mod                 |   0
 .../test1_kin_compartment.mod                 |   0
 .../{mod => testing}/test1_kin_conserve.mod   |   0
 test/unit/{mod => testing}/test1_kin_diff.mod |   0
 .../test1_kin_steadystate.mod                 |   0
 test/unit/{mod => testing}/test2_kin_diff.mod |   0
 test/unit/{mod => testing}/test3_kin_diff.mod |   0
 .../test4_kin_compartment.mod                 |   0
 .../{mod => testing}/test5_nonlinear_diff.mod |   0
 .../{mod => testing}/test6_nonlinear_diff.mod |   0
 test/unit/{mod => testing}/test_ca.mod        |   0
 .../{mod => testing}/test_ca_read_valence.mod |   0
 .../unit/{mod => testing}/test_cl_valence.mod |   0
 test/unit/{mod => testing}/test_kin1.mod      |   0
 test/unit/{mod => testing}/test_kinlva.mod    |   0
 .../{mod => testing}/test_linear_init.mod     |   0
 .../test_linear_init_shuffle.mod              |   0
 .../{mod => testing}/test_linear_state.mod    |   0
 test/unit/{mod => testing}/write_Xi_Xo.mod    |   0
 .../{mod => testing}/write_cai_breakpoint.mod |   0
 test/unit/{mod => testing}/write_eX.mod       |   0
 .../{mod => testing}/write_multiple_eX.mod    |   0
 test/unit/unit_test_catalogue.cpp             |  89 +-----
 test/unit/unit_test_catalogue.hpp             |   1 +
 55 files changed, 326 insertions(+), 603 deletions(-)
 delete mode 100755 mechanisms/generate_catalogue
 rename test/unit/{mod => testing}/ca_linear.mod (100%)
 rename test/unit/{mod => testing}/celsius_test.mod (100%)
 rename test/unit/{mod => testing}/diam_test.mod (100%)
 rename test/unit/{mod => testing}/fixed_ica_current.mod (100%)
 rename test/unit/{mod => testing}/gj0.mod (100%)
 rename test/unit/{mod => testing}/gj1.mod (100%)
 rename test/unit/{mod => testing}/linear_ca_conc.mod (100%)
 rename test/unit/{mod => testing}/non_linear.mod (100%)
 rename test/unit/{mod => testing}/param_as_state.mod (100%)
 rename test/unit/{mod => testing}/point_ica_current.mod (100%)
 rename test/unit/{mod => testing}/post_events_syn.mod (100%)
 rename test/unit/{mod => testing}/read_cai_init.mod (100%)
 rename test/unit/{mod => testing}/read_eX.mod (100%)
 rename test/unit/{mod => testing}/test0_kin_compartment.mod (100%)
 rename test/unit/{mod => testing}/test0_kin_conserve.mod (100%)
 rename test/unit/{mod => testing}/test0_kin_diff.mod (100%)
 rename test/unit/{mod => testing}/test0_kin_steadystate.mod (100%)
 rename test/unit/{mod => testing}/test1_kin_compartment.mod (100%)
 rename test/unit/{mod => testing}/test1_kin_conserve.mod (100%)
 rename test/unit/{mod => testing}/test1_kin_diff.mod (100%)
 rename test/unit/{mod => testing}/test1_kin_steadystate.mod (100%)
 rename test/unit/{mod => testing}/test2_kin_diff.mod (100%)
 rename test/unit/{mod => testing}/test3_kin_diff.mod (100%)
 rename test/unit/{mod => testing}/test4_kin_compartment.mod (100%)
 rename test/unit/{mod => testing}/test5_nonlinear_diff.mod (100%)
 rename test/unit/{mod => testing}/test6_nonlinear_diff.mod (100%)
 rename test/unit/{mod => testing}/test_ca.mod (100%)
 rename test/unit/{mod => testing}/test_ca_read_valence.mod (100%)
 rename test/unit/{mod => testing}/test_cl_valence.mod (100%)
 rename test/unit/{mod => testing}/test_kin1.mod (100%)
 rename test/unit/{mod => testing}/test_kinlva.mod (100%)
 rename test/unit/{mod => testing}/test_linear_init.mod (100%)
 rename test/unit/{mod => testing}/test_linear_init_shuffle.mod (100%)
 rename test/unit/{mod => testing}/test_linear_state.mod (100%)
 rename test/unit/{mod => testing}/write_Xi_Xo.mod (100%)
 rename test/unit/{mod => testing}/write_cai_breakpoint.mod (100%)
 rename test/unit/{mod => testing}/write_eX.mod (100%)
 rename test/unit/{mod => testing}/write_multiple_eX.mod (100%)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index a05ee6da..4d74fa0f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -228,7 +228,6 @@ install(TARGETS arborio-public-deps EXPORT arborio-targets)
 configure_file(scripts/build-catalogue.in ${CMAKE_CURRENT_BINARY_DIR}/arbor-build-catalogue @ONLY)
 install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/arbor-build-catalogue DESTINATION ${CMAKE_INSTALL_BINDIR})
 install(FILES mechanisms/BuildModules.cmake DESTINATION ${ARB_INSTALL_DATADIR})
-install(FILES mechanisms/generate_catalogue DESTINATION ${ARB_INSTALL_DATADIR} PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
 # External libraries in `ext` sub-directory: json, tinyopt and randon123.
 # Creates interface libraries `ext-json`, `ext-tinyopt` and `ext-random123`
 
@@ -397,20 +396,10 @@ if (ARB_BACKTRACE)
     target_compile_definitions(arbor-private-deps INTERFACE WITH_BACKTRACE)
 endif()
 
-# Build and use modcc unless explicit path given
+# Build modcc flags
 #------------------------------------------------
 
-if(ARB_MODCC)
-    find_program(modcc NAMES ${ARB_MODCC} NO_CMAKE_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH)
-    if(NOT modcc)
-        message(FATAL_ERROR "Unable to find modcc executable.")
-    endif()
-    set(ARB_WITH_EXTERNAL_MODCC TRUE)
-else()
-    set(modcc $<TARGET_FILE:modcc>)
-    set(ARB_WITH_EXTERNAL_MODCC FALSE)
-endif()
-
+set(modcc $<TARGET_FILE:modcc>)
 set(ARB_MODCC_FLAGS)
 if(ARB_VECTORIZE)
     list(APPEND ARB_MODCC_FLAGS "--simd")
diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index 985767ad..0e79aefc 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -117,18 +117,15 @@ install(TARGETS arbor-private-headers EXPORT arbor-targets)
 # directory-local.
 
 add_subdirectory(../mechanisms "${CMAKE_BINARY_DIR}/mechanisms")
-set_source_files_properties(${arbor_mechanism_sources} PROPERTIES GENERATED TRUE)
+set_source_files_properties(${arbor-builtin-mechanisms} PROPERTIES GENERATED TRUE)
 
 if(ARB_WITH_CUDA_CLANG OR ARB_WITH_HIP_CLANG)
     set_source_files_properties(${arbor_sources} PROPERTIES LANGUAGE CXX)
-    set_source_files_properties(${arbor_mechanism_sources} PROPERTIES LANGUAGE CXX)
+    set_source_files_properties(${arbor-builtin-mechanism} PROPERTIES LANGUAGE CXX)
 endif()
 
 # Library target:
-
-add_library(arbor ${arbor_sources} ${arbor_mechanism_sources})
-add_dependencies(arbor build_all_mods)
-
+add_library(arbor ${arbor_sources} ${arbor-builtin-mechanisms})
 target_link_libraries(arbor PRIVATE arbor-private-deps arbor-private-headers)
 target_link_libraries(arbor PUBLIC arbor-public-deps arbor-public-headers)
 
diff --git a/arbor/include/arbor/mechcat.hpp b/arbor/include/arbor/mechcat.hpp
index c8f98bd3..bfe5113a 100644
--- a/arbor/include/arbor/mechcat.hpp
+++ b/arbor/include/arbor/mechcat.hpp
@@ -109,7 +109,6 @@ private:
     void register_impl(arb_backend_kind, const std::string&, mechanism_ptr);
 };
 
-// References to global mechanism catalogues.
 ARB_ARBOR_API const mechanism_catalogue& global_default_catalogue();
 ARB_ARBOR_API const mechanism_catalogue& global_allen_catalogue();
 ARB_ARBOR_API const mechanism_catalogue& global_bbp_catalogue();
diff --git a/doc/dev/extending_catalogues.rst b/doc/dev/extending_catalogues.rst
index d60182ec..aedd6cff 100644
--- a/doc/dev/extending_catalogues.rst
+++ b/doc/dev/extending_catalogues.rst
@@ -20,7 +20,7 @@ produce a catalogue of the same level of integration as the built-in catalogues
 (*default*, *bbp*, and *allen*). The required steps are as follows
 
 1. Go to the Arbor source tree.
-2. Create a new directory under *mechanisms*.
+2. Create a new directory under *mechanisms* with the name of your catalogue
 
    1. Add any ``.mod`` files you wish to integrate.
    2. Add any raw C++ files to be included in the catalogue.
@@ -31,27 +31,13 @@ produce a catalogue of the same level of integration as the built-in catalogues
    .. code-block :: cmake
 
      make_catalogue(
-       NAME default                                                   # Name of your catalogue
-       SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/default"                  # Directory name (added above)
-       OUTPUT "CAT_DEFAULT_SOURCES"                                   # Variable name to store C++ files into (see below)
-       MOD exp2syn expsyn expsyn_stdp hh kamt kdrmt nax nernst pas    # Space separated list of NMODL mechanism names
-       CXX                                                            # Space separated list of raw C++ mechanism names
-       PREFIX "${PROJECT_SOURCE_DIR}/mechanisms"                      # where does 'generate_catalogue' live, do not change
-       STANDALONE FALSE                                               # build as shared object, must be OFF
-       VERBOSE OFF)                                                   # Print debug info at configuration time
-
-5. Add your ``output-name`` to the ``arbor_mechanism_sources`` list.
-
-   .. code-block :: cmake
-
-     set(arbor_mechanism_sources
-       ${CAT_BBP_SOURCES}
-       ${CAT_ALLEN_SOURCES}
-       ${CAT_DEFAULT_SOURCES}                                          # from above
-       PARENT_SCOPE)
-
-6. Add a ``global_NAME_catalogue`` function in ``mechcat.hpp`` and ``mechcat.cpp``
-7. Bind this function in ``python/mechanisms.cpp``.
+       NAME default                                                # Name of your catalogue, must match directory under 2.
+       MOD exp2syn expsyn expsyn_stdp hh kamt kdrmt nax nernst pas # Space separated list of mechanism names
+       CXX                                                         # Optional: list of raw C++ mechanism names
+       VERBOSE  ${ARB_CAT_VERBOSE}                                 # Print debug info at configuration time
+       ADD_DEPS ON)                                                # Must be ON, make catalogue part of arbor
+5. Add a ``global_NAME_catalogue`` function in ``mechcat.hpp``.
+6. Bind this function in ``python/mechanisms.cpp`` to ``NAME-catalogue``.
 
 All steps can be directly adapted from the surrounding code.
 
diff --git a/mechanisms/BuildModules.cmake b/mechanisms/BuildModules.cmake
index 83b8467c..98126088 100644
--- a/mechanisms/BuildModules.cmake
+++ b/mechanisms/BuildModules.cmake
@@ -1,71 +1,59 @@
 include(CMakeParseArguments)
 
-# If a MODCC executable is explicitly provided, don't make the in-tree modcc a dependency.
+function("make_catalogue")
+  cmake_parse_arguments(MK_CAT "" "NAME;VERBOSE;ADD_DEPS" "MOD;CXX" ${ARGN})
+  set(MK_CAT_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/${MK_CAT_NAME}")
+  file(MAKE_DIRECTORY "${MK_CAT_OUT_DIR}")
+  set(MK_CAT_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/${MK_CAT_NAME}")
 
-function(build_modules)
-    cmake_parse_arguments(build_modules "" "MODCC;TARGET;SOURCE_DIR;DEST_DIR;MECH_SUFFIX" "MODCC_FLAGS;GENERATES" ${ARGN})
+  if(MK_CAT_VERBOSE)
+    message("Catalogue name:       ${MK_CAT_NAME}")
+    message("Catalogue mechanisms: ${MK_CAT_MOD}")
+    message("Extra cxx files:      ${MK_CAT_CXX}")
+    message("Catalogue sources:    ${MK_CAT_SOURCES}")
+    message("Catalogue output:     ${MK_CAT_OUT_DIR}")
+  endif()
 
-    if("${build_modules_SOURCE_DIR}" STREQUAL "")
-        set(build_modules_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
-    endif()
+  set(mk_cat_modcc_flags -t cpu ${ARB_MODCC_FLAGS} -N arb -c ${MK_CAT_NAME} -o ${MK_CAT_OUT_DIR})
+  if(ARB_WITH_GPU)
+    set(mk_cat_modcc_flags, -t gpu ${mk_cat_modcc_flags})
+  endif()
+
+  list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${MK_CAT_NAME}_catalogue.cpp)
 
-    if("${build_modules_DEST_DIR}" STREQUAL "")
-        set(build_modules_SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}")
+  foreach(mech ${MK_CAT_MOD})
+    list(APPEND catalogue_${MK_CAT_NAME}_mods ${MK_CAT_SOURCES}/${mech}.mod)
+    list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_cpu.cpp)
+    if(ARB_WITH_GPU)
+      list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_gpu.cpp ${MK_CAT_OUT_DIR}/${mech}_gpu.cu)
     endif()
-    file(MAKE_DIRECTORY "${build_modules_DEST_DIR}")
-
-    set(all_generated)
-    foreach(mech ${build_modules_UNPARSED_ARGUMENTS})
-        set(mod "${build_modules_SOURCE_DIR}/${mech}.mod")
-        set(out "${build_modules_DEST_DIR}/${mech}")
-        set(generated)
-        foreach (suffix ${build_modules_GENERATES})
-            list(APPEND generated ${out}${suffix})
-        endforeach()
-
-        set(depends "${mod}")
-        if(build_modules_MODCC)
-            set(modcc_bin ${build_modules_MODCC})
-        else()
-            list(APPEND depends modcc)
-            set(modcc_bin $<TARGET_FILE:modcc>)
-        endif()
-
-        set(flags ${build_modules_MODCC_FLAGS} -o "${out}")
-        if(build_modules_MECH_SUFFIX)
-            list(APPEND flags -m "${mech}${build_modules_MECH_SUFFIX}")
-        endif()
-
-        add_custom_command(
-            OUTPUT ${generated}
-            DEPENDS ${depends}
-            WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
-            COMMAND ${modcc_bin} ${flags} ${mod}
-            COMMENT "modcc generating: ${generated}"
-        )
-        set_source_files_properties(${generated} PROPERTIES GENERATED TRUE)
-        list(APPEND all_generated ${generated})
-    endforeach()
-
-    # Fake target to always trigger .mod -> .hpp/.cu dependencies because CMake
-    if (build_modules_TARGET)
-        set(depends ${all_generated})
-        if(NOT build_modules_MODCC)
-            list(APPEND depends modcc)
-        endif()
-        add_custom_target(${build_modules_TARGET} DEPENDS ${depends})
+  endforeach()
+
+  foreach(mech ${MK_CAT_CXX})
+    list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_cpu.cpp)
+    if(ARB_WITH_GPU)
+      list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_gpu.cpp ${MK_CAT_OUT_DIR}/${mech}_gpu.cu)
     endif()
+  endforeach()
+
+  add_custom_command(OUTPUT            ${catalogue_${MK_CAT_NAME}_source}
+                     DEPENDS           ${modcc} ${catalogue_${MK_CAT_NAME}_mods}
+                     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+                     COMMAND ${modcc} ${mk_cat_modcc_flags} ${catalogue_${MK_CAT_NAME}_mods}
+                     COMMENT "modcc generating: ${catalogue_${MK_CAT_NAME}_source}")
+  add_custom_target(catalogue-${MK_CAT_NAME}-target DEPENDS ${catalogue_${MK_CAT_NAME}_source})
+  if (MK_CAT_ADD_DEPS)
+    add_dependencies(arbor-public-deps catalogue-${MK_CAT_NAME}-target)
+    set(arbor-builtin-mechanisms ${arbor-builtin-mechanisms} ${catalogue_${MK_CAT_NAME}_source} PARENT_SCOPE)
+  else()
+    set(catalogue-${MK_CAT_NAME}-mechanisms ${catalogue_${MK_CAT_NAME}_source} PARENT_SCOPE)
+  endif()
 endfunction()
 
-function("make_catalogue")
-  cmake_parse_arguments(MK_CAT "" "NAME;SOURCES;OUTPUT;PREFIX;STANDALONE;VERBOSE" "CXX_FLAGS_TARGET;MOD;CXX" ${ARGN})
+function("make_catalogue_standalone")
+  cmake_parse_arguments(MK_CAT "" "NAME;SOURCES;VERBOSE" "CXX_FLAGS_TARGET;MOD;CXX" ${ARGN})
   set(MK_CAT_OUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/${MK_CAT_NAME}")
-
-  # Need to set ARB_WITH_EXTERNAL_MODCC *and* modcc
-  set(external_modcc)
-  if(ARB_WITH_EXTERNAL_MODCC)
-    set(external_modcc MODCC ${modcc})
-  endif()
+  file(MAKE_DIRECTORY "${MK_CAT_OUT_DIR}")
 
   if(MK_CAT_VERBOSE)
     message("Catalogue name:       ${MK_CAT_NAME}")
@@ -73,50 +61,32 @@ function("make_catalogue")
     message("Extra cxx files:      ${MK_CAT_CXX}")
     message("Catalogue sources:    ${MK_CAT_SOURCES}")
     message("Catalogue output:     ${MK_CAT_OUT_DIR}")
-    message("Build as standalone:  ${MK_CAT_STANDALONE}")
     message("Arbor cxx flags:      ${MK_CAT_CXX_FLAGS_TARGET}")
     message("Arbor cxx compiler:   ${ARB_CXX}")
-    message("Script prefix:        ${MK_CAT_PREFIX}")
     message("Current cxx compiler: ${CMAKE_CXX_COMPILER}")
   endif()
 
-  file(MAKE_DIRECTORY "${MK_CAT_OUT_DIR}")
-
-  if (NOT TARGET build_all_mods)
-    add_custom_target(build_all_mods)
-  endif()
-
-  build_modules(
-    ${MK_CAT_MOD}
-    SOURCE_DIR "${MK_CAT_SOURCES}"
-    DEST_DIR "${MK_CAT_OUT_DIR}"
-    ${external_modcc} # NB: expands to 'MODCC <binary>' to add an optional argument
-    MODCC_FLAGS -t cpu -t gpu ${ARB_MODCC_FLAGS} -N arb::${MK_CAT_NAME}_catalogue
-    GENERATES .hpp _cpu.cpp _gpu.cpp _gpu.cu
-    TARGET build_catalogue_${MK_CAT_NAME}_mods)
-
-  set(catalogue_${MK_CAT_NAME}_source ${CMAKE_CURRENT_BINARY_DIR}/${MK_CAT_NAME}_catalogue.cpp)
-  set(catalogue_${MK_CAT_NAME}_options -A arbor -I ${MK_CAT_OUT_DIR} -o ${catalogue_${MK_CAT_NAME}_source} -B multicore -C ${MK_CAT_NAME} -N arb::${MK_CAT_NAME}_catalogue)
+  set(mk_cat_modcc_flags -t cpu ${ARB_MODCC_FLAGS} -N arb -c ${MK_CAT_NAME} -o ${MK_CAT_OUT_DIR})
   if(ARB_WITH_GPU)
-    list(APPEND catalogue_${MK_CAT_NAME}_options -B gpu)
+    set(mk_cat_modcc_flags, -t gpu ${mk_cat_modcc_flags})
   endif()
 
-  add_custom_command(
-    OUTPUT  ${catalogue_${MK_CAT_NAME}_source}
-    COMMAND ${MK_CAT_PREFIX}/generate_catalogue ${catalogue_${MK_CAT_NAME}_options} ${MK_CAT_MOD} ${MK_CAT_CXX}
-    COMMENT "Building catalogue ${MK_CAT_NAME}"
-    DEPENDS ${MK_CAT_PREFIX}/generate_catalogue)
-
-  add_custom_target(${MK_CAT_NAME}_catalogue_cpp_target DEPENDS ${catalogue_${MK_CAT_NAME}_source})
-  add_dependencies(build_catalogue_${MK_CAT_NAME}_mods ${MK_CAT_NAME}_catalogue_cpp_target)
-  add_dependencies(build_all_mods build_catalogue_${MK_CAT_NAME}_mods)
+  set(catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${MK_CAT_NAME}_catalogue.cpp)
 
   foreach(mech ${MK_CAT_MOD})
+    list(APPEND catalogue_${MK_CAT_NAME}_mods ${MK_CAT_SOURCES}/${mech}.mod)
     list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_cpu.cpp)
     if(ARB_WITH_GPU)
       list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_gpu.cpp ${MK_CAT_OUT_DIR}/${mech}_gpu.cu)
     endif()
   endforeach()
+
+  add_custom_command(OUTPUT            ${catalogue_${MK_CAT_NAME}_source}
+                     DEPENDS           ${catalogue_${MK_CAT_NAME}_mods}
+                     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+                     COMMAND ${modcc} ${mk_cat_modcc_flags} ${catalogue_${MK_CAT_NAME}_mods}
+                     COMMENT "modcc generating: ${catalogue_${MK_CAT_NAME}_source}")
+
   foreach(mech ${MK_CAT_CXX})
     list(APPEND catalogue_${MK_CAT_NAME}_source ${MK_CAT_OUT_DIR}/${mech}_cpu.cpp)
     if(ARB_WITH_GPU)
@@ -124,28 +94,23 @@ function("make_catalogue")
     endif()
   endforeach()
 
-  set(${MK_CAT_OUTPUT} ${catalogue_${MK_CAT_NAME}_source} PARENT_SCOPE)
-
-  if(${MK_CAT_STANDALONE})
-    add_library(${MK_CAT_NAME}-catalogue SHARED ${catalogue_${MK_CAT_NAME}_source})
-    target_compile_definitions(${MK_CAT_NAME}-catalogue PUBLIC STANDALONE=1)
+  add_library(${MK_CAT_NAME}-catalogue SHARED ${catalogue_${MK_CAT_NAME}_source})
+  target_compile_definitions(${MK_CAT_NAME}-catalogue PUBLIC STANDALONE=1)
 
-    if(ARB_WITH_GPU)
-      target_compile_definitions(${MK_CAT_NAME}-catalogue PUBLIC ARB_GPU_ENABLED)
-    endif()
+  if(ARB_WITH_GPU)
+    target_compile_definitions(${MK_CAT_NAME}-catalogue PUBLIC ARB_GPU_ENABLED)
+  endif()
 
-    target_compile_options(${MK_CAT_NAME}-catalogue PUBLIC ${MK_CAT_CXX_FLAGS_TARGET})
-    set_target_properties(${MK_CAT_NAME}-catalogue
-      PROPERTIES
-      SUFFIX ".so"
-      PREFIX ""
-      CXX_STANDARD 17)
-    target_include_directories(${MK_CAT_NAME}-catalogue PUBLIC "${MK_CAT_ARBOR}/arbor/")
-
-    if(TARGET arbor)
-      target_link_libraries(${MK_CAT_NAME}-catalogue PRIVATE arbor)
-    else()
-      target_link_libraries(${MK_CAT_NAME}-catalogue PRIVATE arbor::arbor)
-    endif()
+  target_compile_options(${MK_CAT_NAME}-catalogue PUBLIC ${MK_CAT_CXX_FLAGS_TARGET})
+  set_target_properties(${MK_CAT_NAME}-catalogue
+    PROPERTIES
+    SUFFIX ".so"
+    PREFIX ""
+    CXX_STANDARD 17)
+
+  if(TARGET arbor)
+    target_link_libraries(${MK_CAT_NAME}-catalogue PRIVATE arbor)
+  else()
+    target_link_libraries(${MK_CAT_NAME}-catalogue PRIVATE arbor::arbor)
   endif()
 endfunction()
diff --git a/mechanisms/CMakeLists.txt b/mechanisms/CMakeLists.txt
index 97dbf7e3..9f1c14f5 100644
--- a/mechanisms/CMakeLists.txt
+++ b/mechanisms/CMakeLists.txt
@@ -1,46 +1,29 @@
 include(BuildModules.cmake)
 
+set(arbor-builtin-mechanisms)
+
 # Define catalogues
 make_catalogue(
   NAME bbp
-  SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/bbp"
-  OUTPUT "CAT_BBP_SOURCES"
   MOD CaDynamics_E2 Ca_HVA Ca_LVAst Ih Im K_Pst K_Tst Nap_Et2 NaTa_t NaTs2_t SK_E2 SKv3_1
-  CXX
-  PREFIX "${PROJECT_SOURCE_DIR}/mechanisms"
-  CXX_FLAGS_TARGET "${ARB_CXX_FLAGS_TARGET_FULL}"
-  STANDALONE FALSE
-  VERBOSE ${ARB_CAT_VERBOSE})
+  VERBOSE ${ARB_CAT_VERBOSE}
+  ADD_DEPS ON)
 
 make_catalogue(
   NAME allen
-  SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/allen"
-  OUTPUT "CAT_ALLEN_SOURCES"
   MOD CaDynamics Ca_HVA Ca_LVA Ih Im Im_v2 K_P K_T Kd Kv2like Kv3_1 NaTa NaTs NaV Nap SK
-  CXX
-  PREFIX "${PROJECT_SOURCE_DIR}/mechanisms"
-  CXX_FLAGS_TARGET "${ARB_CXX_FLAGS_TARGET_FULL}"
-  STANDALONE FALSE
-  VERBOSE ${ARB_CAT_VERBOSE})
+  VERBOSE ${ARB_CAT_VERBOSE}
+  ADD_DEPS ON)
 
 make_catalogue(
   NAME default
-  SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/default"
-  OUTPUT "CAT_DEFAULT_SOURCES"
   MOD exp2syn expsyn expsyn_curr expsyn_stdp hh kamt kdrmt nax nernst pas gj decay inject
-  CXX
-  PREFIX "${PROJECT_SOURCE_DIR}/mechanisms"
-  CXX_FLAGS_TARGET "${ARB_CXX_FLAGS_TARGET_FULL}"
-  STANDALONE FALSE
-  VERBOSE ${ARB_CAT_VERBOSE})
+  VERBOSE ${ARB_CAT_VERBOSE}
+  ADD_DEPS ON)
 
-# Join sources
-set(arbor_mechanism_sources
-  ${CAT_BBP_SOURCES}
-  ${CAT_ALLEN_SOURCES}
-  ${CAT_DEFAULT_SOURCES}
-  PARENT_SCOPE)
+# This re-exports
+set(arbor-builtin-mechanisms ${arbor-builtin-mechanisms} PARENT_SCOPE)
 
 if(ARB_WITH_CUDA_CLANG OR ARB_WITH_HIP_CLANG)
-    set_source_files_properties(${arbor_mechanism_sources} PROPERTIES LANGUAGE CXX)
+    set_source_files_properties(${arbor-builtin-mechs} PROPERTIES LANGUAGE CXX)
 endif()
diff --git a/mechanisms/default/pas.mod b/mechanisms/default/pas.mod
index d7ffb028..7b085c5b 100644
--- a/mechanisms/default/pas.mod
+++ b/mechanisms/default/pas.mod
@@ -10,7 +10,6 @@ UNITS {
     (S) = (siemens)
 }
 
-
 INITIAL {}
 
 PARAMETER {
diff --git a/mechanisms/generate_catalogue b/mechanisms/generate_catalogue
deleted file mode 100755
index 2a63f7d1..00000000
--- a/mechanisms/generate_catalogue
+++ /dev/null
@@ -1,170 +0,0 @@
-#!/usr/bin/env python3
-
-# Note: compatible with Python2.7 and Python3.
-
-from __future__ import print_function
-import sys
-import string
-import argparse
-
-def parse_arguments():
-    def append_slash(s):
-        return s+'/' if s and not s.endswith('/') else s
-
-    class ConciseHelpFormatter(argparse.HelpFormatter):
-        def __init__(self, **kwargs):
-            super(ConciseHelpFormatter, self).__init__(max_help_position=20, **kwargs)
-
-        def _format_action_invocation(self, action):
-            if not action.option_strings:
-                return super(ConciseHelpFormatter, self)._format_action_invocation(action)
-            else:
-                optstr = ', '.join(action.option_strings)
-                if action.nargs==0:
-                    return optstr
-                else:
-                    return optstr+' '+self._format_args(action, action.dest.upper())
-
-    parser = argparse.ArgumentParser(
-        description = 'Generate global default catalogue source for Arbor build.',
-        usage = '%(prog)s [options] [module...]',
-        add_help = False,
-        formatter_class = ConciseHelpFormatter)
-
-    parser.add_argument(
-        'modules',
-        nargs = '*',
-        help = argparse.SUPPRESS)
-
-    group = parser.add_argument_group('Options')
-
-    group.add_argument(
-        '-I', '--module-prefix',
-        default = 'mechanisms',
-        metavar = 'PATH',
-        dest = 'modpfx',
-        type = append_slash,
-        help = 'directory prefix for module includes, default "%(default)s"')
-
-    group.add_argument(
-        '-A', '--arbor-prefix',
-        default = '',
-        metavar = 'PATH',
-        dest = 'arbpfx',
-        type = append_slash,
-        help = 'directory prefix for arbor includes, default "%(default)s"')
-
-    group.add_argument(
-        '-B', '--backend',
-        default = [],
-        action = 'append',
-        dest = 'backends',
-        metavar = 'BACKEND',
-        help = 'register implementations for back-end %(metavar)s')
-
-    group.add_argument(
-        '-N', '--namespace',
-        default = [],
-        action = 'append',
-        dest = 'namespaces',
-        metavar = 'NAMESPACE',
-        help = 'add %(metavar)s to list of implicitly included namespaces')
-
-    group.add_argument(
-        '-C', '--catalogue',
-        default = 'default',
-        dest = 'catalogue',
-        help = 'catalogue name, default "%(default)s"')
-
-    group.add_argument(
-        '-o', '--output',
-        default = [],
-        dest = 'output',
-        metavar = 'FILE',
-        help = 'save output to %(metavar)s (default is to print to stdout)')
-
-    group.add_argument(
-        '-h', '--help',
-        action = 'help',
-        help = 'display this help and exit')
-
-    return vars(parser.parse_args())
-
-
-def generate(catalogue, modpfx='', arbpfx='', modules=[], backends=[], namespaces=[], **rest):
-    src = string.Template(\
-r'''// Automatically generated by:
-// $cmdline
-
-#include <${arbpfx}mechanism_abi.h>
-
-$module_includes
-
-#ifdef STANDALONE
-extern "C" {
-    [[gnu::visibility("default")]] const void* get_catalogue(int* n) {
-        static arb_mechanism cat[${n_modules}] = {
-            ${insert_modules}
-        };
-        *n = ${n_modules};
-        return (void*)cat;
-    }
-}
-#else
-
-#include <${arbpfx}mechcat.hpp>
-#include <${arbpfx}mechanism.hpp>
-#include <${arbpfx}assert.hpp>
-
-namespace arb {
-mechanism_catalogue build_${catalogue}_catalogue() {
-    mechanism_catalogue cat;
-
-#define ADD(make) do {                                                                  \
-    auto mech = make();                                                                 \
-    auto ty = mech.type();                                                              \
-    auto nm = ty.name;                                                                  \
-    auto ig = mech.i_gpu();                                                             \
-    auto ic = mech.i_cpu();                                                             \
-    arb_assert(ic || ig);                                                               \
-    cat.add(nm, ty);                                                                    \
-    if (ic) cat.register_implementation(nm, std::make_unique<arb::mechanism>(ty, *ic)); \
-    if (ig) cat.register_implementation(nm, std::make_unique<arb::mechanism>(ty, *ig)); \
-} while (false)
-
-    $add_modules
-
-#undef ADD
-
-    return cat;
-}
-
-ARB_ARBOR_API const mechanism_catalogue& global_${catalogue}_catalogue() {
-    static mechanism_catalogue cat = build_${catalogue}_catalogue();
-    return cat;
-}
-} // namespace arb
-#endif
-''')
-
-    def indent(n, lines):
-        return '{{:<{0!s}}}'.format(n+1).format('\n').join(lines)
-
-    return src.safe_substitute(dict(
-        cmdline=" ".join(sys.argv),
-        arbpfx=arbpfx,
-        catalogue=catalogue,
-        module_includes=indent(0,
-            ['#include "{}{}.hpp"'.format(modpfx, m) for m in modules]),
-        add_modules=indent(4, [f'ADD(make_arb_{catalogue}_catalogue_{mod});' for mod in modules]),
-        n_modules=len(modules),
-        insert_modules=indent(12, [f'make_arb_{catalogue}_catalogue_{mod}(),' for mod in modules]),
-        ))
-
-
-args = parse_arguments()
-code = generate(**args)
-if args['output']:
-    print(code, file = open(args['output'],'w'))
-else:
-    print(code)
diff --git a/modcc/CMakeLists.txt b/modcc/CMakeLists.txt
index 62fb49f5..102b397f 100644
--- a/modcc/CMakeLists.txt
+++ b/modcc/CMakeLists.txt
@@ -56,10 +56,25 @@ export_visibility(libmodcc)
 
 
 add_executable(modcc ${modcc_sources})
-target_link_libraries(modcc PRIVATE libmodcc ext-tinyopt)
-set_target_properties(modcc libmodcc PROPERTIES EXCLUDE_FROM_ALL ${ARB_WITH_EXTERNAL_MODCC})
 
-if (NOT ARB_WITH_EXTERNAL_MODCC)
-    install(TARGETS modcc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
+if (ARB_USE_BUNDLED_FMT)
+    target_include_directories(modcc
+        PUBLIC
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
+        PRIVATE
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../ext/fmt/include>)
+
+    target_compile_definitions(modcc PRIVATE FMT_HEADER_ONLY)
+else()
+    target_include_directories(modcc
+        PUBLIC
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+            $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>)
+    find_package(fmt REQUIRED)
+    target_link_libraries(modcc PRIVATE fmt::fmt-header-only)
 endif()
 
+target_link_libraries(modcc PRIVATE libmodcc ext-tinyopt)
+set_target_properties(modcc libmodcc PROPERTIES EXCLUDE_FROM_ALL OFF)
+install(TARGETS modcc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/modcc/modcc.cpp b/modcc/modcc.cpp
index 3eea843b..0934c21a 100644
--- a/modcc/modcc.cpp
+++ b/modcc/modcc.cpp
@@ -3,6 +3,7 @@
 #include <stdexcept>
 #include <unordered_map>
 #include <unordered_set>
+#include <regex>
 
 #include <tinyopt/tinyopt.h>
 
@@ -19,6 +20,8 @@
 #include "io/bulkio.hpp"
 #include "io/pprintf.hpp"
 
+#include <fmt/format.h>
+
 using std::cout;
 using std::cerr;
 
@@ -66,8 +69,9 @@ auto key_by_value(const Map& map, const V& v) -> decltype(map.begin()->first) {
 
 struct Options {
     std::string outprefix;
-    std::string modfile;
+    std::vector<std::string> modfiles;
     std::string modulename;
+    std::string catalogue;
     bool verbose = false;
     bool analysis = false;
     std::unordered_set<targetKind> targets;
@@ -97,12 +101,14 @@ std::ostream& operator<<(std::ostream& out, const Options& opt) {
         targets += " "+key_by_value(targetKindMap, t);
     }
 
-    return out <<
-        table_prefix{"file"} << opt.modfile << line_end <<
-        table_prefix{"output"} << (opt.outprefix.empty()? "-": opt.outprefix) << line_end <<
+    for (const auto& f: opt.modfiles) {
+        out << table_prefix{"file"} << f << line_end;
+    }
+    out << table_prefix{"output"} << (opt.outprefix.empty()? "-": opt.outprefix) << line_end <<
         table_prefix{"verbose"} << noyes[opt.verbose] << line_end <<
         table_prefix{"targets"} << targets << line_end <<
         table_prefix{"analysis"} << noyes[opt.analysis] << line_end;
+    return out;
 }
 
 std::ostream& operator<<(std::ostream& out, const printer_options& popt) {
@@ -139,7 +145,7 @@ const char* usage_str =
         "-V|--verbose           [Toggle verbose mode]\n"
         "-A|--analyse           [Toggle analysis mode]\n"
         "-T|--trace-codegen     [Leave trace marks in generated source]\n"
-        "<filename>             [File to be compiled]\n";
+        "<filenames>            [Files to be compiled]\n";
 
 int main(int argc, char **argv) {
     using namespace to;
@@ -159,16 +165,14 @@ int main(int argc, char **argv) {
             }
         };
 
-        auto add_target = [&opt](targetKind t) {
-            opt.targets.insert(t);
-        };
+        auto add_target = [&opt](targetKind t) { opt.targets.insert(t); };
 
         to::option options[] = {
-                { opt.modfile,  to::mandatory},
-                { opt.outprefix,                                         "-o", "--output" },
+                { to::push_back(opt.modfiles)},
+                { opt.outprefix,                                         "-o", "--output-dir" },
                 { to::set(opt.verbose),  to::flag,                       "-V", "--verbose" },
                 { to::set(opt.analysis), to::flag,                       "-A", "--analyse" },
-                { opt.modulename,                                        "-m", "--module" },
+                { opt.catalogue,                                         "-c", "--catalogue"},
                 { popt.cpp_namespace,                                    "-N", "--namespace" },
                 { to::action(enable_simd), to::flag,                     "-s", "--simd" },
                 { popt.simd,                                             "-S", "--simd-abi" },
@@ -184,109 +188,192 @@ int main(int argc, char **argv) {
         return 1;
     }
 
-    try {
-        auto emit_header = [&opt](const char* h) {
-            if (opt.verbose) {
-                cout << green("[") << h << green("]") << "\n";
-            }
-        };
+    if (!opt.catalogue.empty()) popt.cpp_namespace += "::" + opt.catalogue + "_catalogue";
 
-        if (opt.verbose) {
-            static const std::string tableline = cyan("."+std::string(60, '-')+".")+"\n";
-            cout << tableline;
-            cout << opt;
-            cout << popt;
-            cout << tableline;
-        }
+    std::vector<std::string> modules;
 
-        // Load module file and initialize Module object.
-
-        Module m(io::read_all(opt.modfile), opt.modfile);
+    for (const auto& modfile: opt.modfiles) {
+        try {
+            auto emit_header = [&opt](const char* h) {
+                if (opt.verbose) {
+                    cout << green("[") << h << green("]") << "\n";
+                }
+            };
 
-        if (m.empty()) {
-            return report_error("empty file: "+opt.modfile);
-        }
+            if (opt.verbose) {
+                static const std::string tableline = cyan("."+std::string(60, '-')+".")+"\n";
+                cout << tableline;
+                cout << opt;
+                cout << popt;
+                cout << tableline;
+            }
 
-        if (!opt.modulename.empty()) {
-            m.module_name(opt.modulename);
-        }
+            // Load module file and initialize Module object.
+            Module m(io::read_all(modfile), modfile);
 
-        // Perform parsing and semantic analysis passes.
+            if (m.empty()) {
+                return report_error("empty file: "+modfile);
+            }
 
-        emit_header("parsing");
-        Parser p(m, false);
-        if (!p.parse()) {
-            // Parser::parse() writes its own errors to stderr.
-            return 1;
-        }
+            // Perform parsing and semantic analysis passes.
 
-        emit_header("semantic analysis");
-        m.semantic();
-        if (m.has_warning()) {
-            cerr << yellow("Warnings:\n");
-            cerr << m.warning_string() << "\n";
-        }
-        if (m.has_error()) {
-            return report_error(m.error_string());
-        }
+            emit_header("parsing");
+            Parser p(m, false);
+            if (!p.parse()) {
+                // Parser::parse() writes its own errors to stderr.
+                return 1;
+            }
 
-        // Generate backend-specific sources for each backend provided.
+            emit_header("semantic analysis");
+            m.semantic();
+            if (m.has_warning()) {
+                cerr << yellow("Warnings:\n");
+                cerr << m.warning_string() << "\n";
+            }
+            if (m.has_error()) {
+                return report_error(m.error_string());
+            }
 
-        emit_header("code generation");
+            // Generate backend-specific sources for each backend provided.
 
-        // If no output prefix given, use the module name.
-        std::string prefix = opt.outprefix.empty()? m.module_name(): opt.outprefix;
+            emit_header("code generation");
 
-        bool have_cpu = opt.targets.find(targetKind::cpu) != opt.targets.end();
-        bool have_gpu = opt.targets.find(targetKind::gpu) != opt.targets.end();
+            std::string prefix = m.module_name();
+            if (!opt.outprefix.empty()) {
+                if (opt.outprefix.back() != '/') opt.outprefix += "/";
+                prefix = opt.outprefix + prefix;
+            }
 
-        io::write_all(build_info_header(m, popt, have_cpu, have_gpu), prefix+".hpp");
-        for (targetKind target: opt.targets) {
-            std::string outfile = prefix;
-            switch (target) {
-            case targetKind::gpu:
-                io::write_all(emit_gpu_cpp_source(m, popt), outfile+"_gpu.cpp");
-                io::write_all(emit_gpu_cu_source(m, popt), outfile+"_gpu.cu");
-                break;
-            case targetKind::cpu:
-                io::write_all(emit_cpp_source(m, popt), outfile+"_cpu.cpp");
-                break;
+            bool have_cpu = opt.targets.find(targetKind::cpu) != opt.targets.end();
+            bool have_gpu = opt.targets.find(targetKind::gpu) != opt.targets.end();
+
+            io::write_all(build_info_header(m, popt, have_cpu, have_gpu), prefix+".hpp");
+            for (targetKind target: opt.targets) {
+                std::string outfile = prefix;
+                switch (target) {
+                    case targetKind::gpu:
+                        io::write_all(emit_gpu_cpp_source(m, popt), outfile+"_gpu.cpp");
+                        io::write_all(emit_gpu_cu_source(m, popt), outfile+"_gpu.cu");
+                        break;
+                    case targetKind::cpu:
+                        io::write_all(emit_cpp_source(m, popt), outfile+"_cpu.cpp");
+                        break;
+                }
             }
-        }
 
-        // Optional analysis report.
+            // Optional analysis report.
 
-        if (opt.analysis) {
-            cout << green("performance analysis\n");
-            for (auto &symbol: m.symbols()) {
-                if (auto method = symbol.second->is_api_method()) {
-                    cout << white("-------------------------\n");
-                    cout << yellow("method " + method->name()) << "\n";
-                    cout << white("-------------------------\n");
+            if (opt.analysis) {
+                cout << green("performance analysis\n");
+                for (auto &symbol: m.symbols()) {
+                    if (auto method = symbol.second->is_api_method()) {
+                        cout << white("-------------------------\n");
+                        cout << yellow("method " + method->name()) << "\n";
+                        cout << white("-------------------------\n");
 
-                    FlopVisitor flops;
-                    method->accept(&flops);
-                    cout << white("FLOPS\n") << flops.print() << "\n";
+                        FlopVisitor flops;
+                        method->accept(&flops);
+                        cout << white("FLOPS\n") << flops.print() << "\n";
 
-                    MemOpVisitor memops;
-                    method->accept(&memops);
-                    cout << white("MEMOPS\n") << memops.print() << "\n";
+                        MemOpVisitor memops;
+                        method->accept(&memops);
+                        cout << white("MEMOPS\n") << memops.print() << "\n";
+                    }
                 }
             }
+
+            modules.push_back(m.module_name());
+        }
+        catch (io::bulkio_error& e) {
+            return report_error(e.what());
+        }
+        catch (compiler_exception& e) {
+            return report_ice(pprintf("% @ %", e.what(), e.location()));
+        }
+        catch (std::exception& e) {
+            return report_ice(e.what());
+        }
+        catch (...) {
+            return report_ice("");
         }
     }
-    catch (io::bulkio_error& e) {
-        return report_error(e.what());
-    }
-    catch (compiler_exception& e) {
-        return report_ice(pprintf("% @ %", e.what(), e.location()));
-    }
-    catch (std::exception& e) {
-        return report_ice(e.what());
-    }
-    catch (...) {
-        return report_ice("");
-    }
 
-    return 0;
+    if (!opt.catalogue.empty()) {
+        const auto prefix = std::regex_replace(popt.cpp_namespace, std::regex{"::"}, "_");
+        {
+            std::ofstream out(opt.outprefix + opt.catalogue + "_catalogue.cpp");
+            out << "// Automatically generated by modcc\n"
+                "\n"
+                "#include <arbor/mechanism_abi.h>\n"
+                "\n";
+
+            for (const auto& mod: modules) {
+                out << fmt::format("#include \"{}.hpp\"\n", mod);
+            }
+
+            out << "\n"
+                "#ifdef STANDALONE\n"
+                "extern \"C\" {\n"
+                "    [[gnu::visibility(\"default\")]] const void* get_catalogue(int* n) {\n";
+            out << fmt::format("        *n = {0};\n"
+                               "        static arb_mechanism cat[{0}] = {{\n",
+                               opt.modfiles.size());
+            for (const auto& mod: modules) {
+                out << fmt::format("            make_{}_{}(),\n", prefix, mod);
+            }
+            out << "        };\n"
+                "        return (void*)cat;\n"
+                "    }\n"
+                "}\n"
+                "\n"
+                "#else\n"
+                "\n"
+                "#include <arbor/mechanism.hpp>\n"
+                "#include <arbor/assert.hpp>\n"
+                "\n";
+            out << fmt::format("#include \"{0}_catalogue.hpp\"\n"
+                               "\n"
+                               "namespace arb {{\n"
+                               "mechanism_catalogue build_{0}_catalogue() {{\n"
+                               "    mechanism_catalogue cat;\n",
+                               opt.catalogue);
+            for (const auto& mod: modules) {
+                out << fmt::format("    {{\n"
+                                   "        auto mech = make_{}_{}();\n"
+                                   "        auto ty = mech.type();\n"
+                                   "        auto nm = ty.name;\n"
+                                   "        auto ig = mech.i_gpu();\n"
+                                   "        auto ic = mech.i_cpu();\n"
+                                   "        arb_assert(ic || ig);\n"
+                                   "        cat.add(nm, ty);\n"
+                                   "        if (ic) cat.register_implementation(nm, std::make_unique<arb::mechanism>(ty, *ic));\n"
+                                   "        if (ig) cat.register_implementation(nm, std::make_unique<arb::mechanism>(ty, *ig));\n"
+                                   "    }}\n",
+                                   prefix, mod);
+            }
+
+            out << "    return cat;\n"
+                "}\n"
+                "\n";
+            out << fmt::format("ARB_ARBOR_API const mechanism_catalogue& global_{0}_catalogue() {{\n"
+                               "    static mechanism_catalogue cat = build_{0}_catalogue();\n"
+                               "    return cat;\n"
+                               "}}\n",
+                               opt.catalogue);
+            out << "} // namespace arb\n"
+                "#endif\n";
+        }
+        {
+        std::ofstream out(opt.outprefix + opt.catalogue + "_catalogue.hpp");
+        out << fmt::format("#pragma once\n"
+                           "\n"
+                           "#include <arbor/mechcat.hpp>\n"
+                           "#include <arbor/export.hpp>\n"
+                           "\n"
+                           "namespace arb {{\n"
+                           "ARB_ARBOR_API const mechanism_catalogue& global_{0}_catalogue();\n"
+                           "}}\n",
+                           opt.catalogue);
+        }
+    }
 }
diff --git a/modcc/printer/cprinter.cpp b/modcc/printer/cprinter.cpp
index 8eb2fc21..fd1ecc15 100644
--- a/modcc/printer/cprinter.cpp
+++ b/modcc/printer/cprinter.cpp
@@ -427,7 +427,7 @@ ARB_LIBMODCC_API std::string emit_cpp_source(const Module& module_, const printe
                                    "    result.write_ions = {3}write_ions;\n"
                                    "    result.post_event = {3}post_event;\n"
                                    "    return &result;\n"
-                                   "  }}"
+                                   "  }}\n"
                                    "}}\n\n"),
                        std::regex_replace(opt.cpp_namespace, std::regex{"::"}, "_"),
                        name,
diff --git a/scripts/build-catalogue.in b/scripts/build-catalogue.in
index 0dc73ce7..6deea74e 100755
--- a/scripts/build-catalogue.in
+++ b/scripts/build-catalogue.in
@@ -131,7 +131,6 @@ exec_path = this_path.resolve()
 
 for path in [
     exec_path / "modcc",
-    data_path / "generate_catalogue",
     data_path / "BuildModules.cmake",
     pack_path / "arbor-config.cmake",
 ]:
@@ -155,15 +154,12 @@ include(BuildModules.cmake)
 set(ARB_WITH_EXTERNAL_MODCC true)
 find_program(modcc NAMES modcc PATHS {exec_path})
 
-make_catalogue(
+make_catalogue_standalone(
   NAME {name}
   SOURCES "${{CMAKE_CURRENT_SOURCE_DIR}}/mod"
-  OUTPUT "CAT_{name.upper()}_SOURCES"
   MOD {' '.join(mods)}
   CXX {' '.join(raw)}
-  PREFIX {data_path}
   CXX_FLAGS_TARGET ${{ARB_CXX_FLAGS_TARGET}}
-  STANDALONE ON
   VERBOSE {"ON" if verbose else "OFF"})
 """
 
@@ -215,7 +211,6 @@ with TemporaryDirectory() as tmp:
     with open(tmp / "CMakeLists.txt", "w") as fd:
         fd.write(cmake)
     shutil.copy2(f"{data_path}/BuildModules.cmake", tmp)
-    shutil.copy2(f"{data_path}/generate_catalogue", tmp)
 
     out = tmp / "build" / "generated" / name
     os.makedirs(out, exist_ok=True)
diff --git a/test/unit-modcc/CMakeLists.txt b/test/unit-modcc/CMakeLists.txt
index 5f000613..0f78f2c8 100644
--- a/test/unit-modcc/CMakeLists.txt
+++ b/test/unit-modcc/CMakeLists.txt
@@ -23,9 +23,7 @@ set(unit-modcc_sources
 )
 
 add_executable(unit-modcc EXCLUDE_FROM_ALL ${unit-modcc_sources})
-if(NOT ARB_WITH_EXTERNAL_MODCC)
-    add_dependencies(tests unit-modcc)
-endif()
+add_dependencies(tests unit-modcc)
 
 target_link_libraries(unit-modcc PRIVATE libmodcc gtest)
 target_compile_definitions(unit-modcc PRIVATE "DATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"")
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index db708766..d89a72c2 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -43,47 +43,11 @@ set(test_mechanisms
 
 include(${PROJECT_SOURCE_DIR}/mechanisms/BuildModules.cmake)
 
-set(external_modcc)
-if(ARB_WITH_EXTERNAL_MODCC)
-    set(external_modcc MODCC ${modcc})
-endif()
-set(test_mech_dir ${CMAKE_CURRENT_BINARY_DIR}/mechanisms)
-
-build_modules(
-    ${test_mechanisms}
-    SOURCE_DIR mod
-    DEST_DIR "${test_mech_dir}"
-    ${external_modcc}
-    MODCC_FLAGS -t cpu -t gpu ${ARB_MODCC_FLAGS} -N testing
-    GENERATES .hpp _cpu.cpp _gpu.cpp _gpu.cu
-    TARGET build_test_mods
-)
-
-set(test_mech_sources)
-foreach(mech ${test_mechanisms})
-    list(APPEND test_mech_sources ${test_mech_dir}/${mech}_cpu.cpp)
-    if(ARB_WITH_GPU)
-        list(APPEND test_mech_sources ${test_mech_dir}/${mech}_gpu.cpp)
-        list(APPEND test_mech_sources ${test_mech_dir}/${mech}_gpu.cu)
-    endif()
-endforeach()
-
-# TODO: test_mechanism and mechanism prototype comparisons must
-# be re-jigged.
-
-# set(proto_mechanisms pas hh expsyn exp2syn test_kin1 test_kinlva test_ca)
-# set(mech_proto_dir "${CMAKE_CURRENT_BINARY_DIR}/mech_proto")
-# file(MAKE_DIRECTORY "${mech_proto_dir}")
-#
-# build_modules(
-#     ${proto_mechanisms}
-#     SOURCE_DIR "${PROJECT_SOURCE_DIR}/mechanisms/mod"
-#     DEST_DIR "${mech_proto_dir}"
-#     MECH_SUFFIX _proto
-#     MODCC_FLAGS -t cpu
-#     GENERATES _cpu.hpp
-#     TARGET build_test_mods
-# )
+make_catalogue(
+    NAME    testing
+    MOD     ${test_mechanisms}
+    VERBOSE ${ARB_CAT_VERBOSE}
+    ADD_DEPS OFF)
 
 # Unit test sources
 
@@ -190,7 +154,6 @@ if(ARB_WITH_GPU)
         test_gpu_stack.cu
         test_multi_event_stream_gpu.cu
         test_reduce_by_key.cu
-
         test_matrix_cpuvsgpu.cpp
         test_matrix_gpu.cpp
         test_mc_cell_group_gpu.cpp
@@ -201,10 +164,7 @@ if(ARB_WITH_GPU)
 endif()
 
 if(ARB_WITH_NEUROML)
-    list(APPEND unit_sources
-
-        test_nml_morphology.cpp
-    )
+    list(APPEND unit_sources test_nml_morphology.cpp)
 endif()
 
 if(ARB_WITH_CUDA_CLANG OR ARB_WITH_HIP_CLANG)
@@ -212,36 +172,33 @@ if(ARB_WITH_CUDA_CLANG OR ARB_WITH_HIP_CLANG)
     set_source_files_properties(${test_mech_sources} PROPERTIES LANGUAGE CXX)
 endif()
 
-add_executable(unit EXCLUDE_FROM_ALL ${unit_sources} ${test_mech_sources})
-add_dependencies(unit build_test_mods)
+add_executable(unit EXCLUDE_FROM_ALL ${unit_sources} ${catalogue-testing-mechanisms})
+add_dependencies(unit catalogue-testing-target)
 add_dependencies(tests unit)
 
-if(${CMAKE_POSITION_INDEPENDENT_CODE})
-  make_catalogue(
+
+make_catalogue_standalone(
     NAME dummy
     SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/dummy"
-    OUTPUT "CAT_DUMMY_SOURCES"
     MOD dummy
     CXX
-    PREFIX "${PROJECT_SOURCE_DIR}/mechanisms"
     CXX_FLAGS_TARGET ${ARB_CXX_FLAGS_TARGET_FULL}
-    STANDALONE ON
     VERBOSE ON)
-  target_compile_definitions(unit PRIVATE USE_DYNAMIC_CATALOGUES)
-  if(ARB_WITH_NVCC)
+
+if(ARB_WITH_NVCC)
     target_compile_options(dummy-catalogue PRIVATE -DARB_CUDA)
-  endif()
-  if(ARB_WITH_CUDA_CLANG)
+endif()
+
+if(ARB_WITH_CUDA_CLANG)
     set(clang_options_ -DARB_CUDA -xcuda --cuda-gpu-arch=sm_60 --cuda-path=${CUDA_TOOLKIT_ROOT_DIR})
     target_compile_options(unit PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${clang_options_}>)
-  endif()
+endif()
 
-  if(ARB_WITH_HIP_CLANG)
+if(ARB_WITH_HIP_CLANG)
     set(clang_options_ -DARB_HIP -xhip --amdgpu-target=gfx906 --amdgpu-target=gfx900)
     target_compile_options(unit PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${clang_options_}>)
-  endif()
-  add_dependencies(unit dummy-catalogue)
 endif()
+add_dependencies(unit dummy-catalogue)
 
 if(ARB_WITH_GPU)
   target_compile_definitions(unit PRIVATE ARB_GPU_ENABLED)
@@ -265,4 +222,5 @@ target_compile_options(unit PRIVATE ${ARB_CXX_FLAGS_TARGET_FULL})
 target_compile_definitions(unit PRIVATE "-DDATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}/../swc\"")
 target_compile_definitions(unit PRIVATE "-DLIBDIR=\"${PROJECT_BINARY_DIR}/lib\"")
 target_include_directories(unit PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
+target_include_directories(unit PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/generated/testing")
 target_link_libraries(unit PRIVATE gtest arbor arborenv arborio arborio-private-headers arbor-private-headers arbor-sup)
diff --git a/test/unit/test_mechcat.cpp b/test/unit/test_mechcat.cpp
index edd75498..a70306aa 100644
--- a/test/unit/test_mechcat.cpp
+++ b/test/unit/test_mechcat.cpp
@@ -278,7 +278,6 @@ TEST(mechcat, names) {
     }
 }
 
-#ifdef USE_DYNAMIC_CATALOGUES
 TEST(mechcat, loading) {
     EXPECT_THROW(load_catalogue(LIBDIR "/does-not-exist-catalogue.so"), file_not_found_error);
 #if defined(ARB_ARBOR_SHARED_LIBRARY)
@@ -293,7 +292,6 @@ TEST(mechcat, loading) {
     EXPECT_EQ(std::vector<std::string>{"dummy"}, cat.mechanism_names());
 #endif
 }
-#endif
 
 TEST(mechcat, derived_info) {
     auto cat = build_fake_catalogue();
diff --git a/test/unit/mod/ca_linear.mod b/test/unit/testing/ca_linear.mod
similarity index 100%
rename from test/unit/mod/ca_linear.mod
rename to test/unit/testing/ca_linear.mod
diff --git a/test/unit/mod/celsius_test.mod b/test/unit/testing/celsius_test.mod
similarity index 100%
rename from test/unit/mod/celsius_test.mod
rename to test/unit/testing/celsius_test.mod
diff --git a/test/unit/mod/diam_test.mod b/test/unit/testing/diam_test.mod
similarity index 100%
rename from test/unit/mod/diam_test.mod
rename to test/unit/testing/diam_test.mod
diff --git a/test/unit/mod/fixed_ica_current.mod b/test/unit/testing/fixed_ica_current.mod
similarity index 100%
rename from test/unit/mod/fixed_ica_current.mod
rename to test/unit/testing/fixed_ica_current.mod
diff --git a/test/unit/mod/gj0.mod b/test/unit/testing/gj0.mod
similarity index 100%
rename from test/unit/mod/gj0.mod
rename to test/unit/testing/gj0.mod
diff --git a/test/unit/mod/gj1.mod b/test/unit/testing/gj1.mod
similarity index 100%
rename from test/unit/mod/gj1.mod
rename to test/unit/testing/gj1.mod
diff --git a/test/unit/mod/linear_ca_conc.mod b/test/unit/testing/linear_ca_conc.mod
similarity index 100%
rename from test/unit/mod/linear_ca_conc.mod
rename to test/unit/testing/linear_ca_conc.mod
diff --git a/test/unit/mod/non_linear.mod b/test/unit/testing/non_linear.mod
similarity index 100%
rename from test/unit/mod/non_linear.mod
rename to test/unit/testing/non_linear.mod
diff --git a/test/unit/mod/param_as_state.mod b/test/unit/testing/param_as_state.mod
similarity index 100%
rename from test/unit/mod/param_as_state.mod
rename to test/unit/testing/param_as_state.mod
diff --git a/test/unit/mod/point_ica_current.mod b/test/unit/testing/point_ica_current.mod
similarity index 100%
rename from test/unit/mod/point_ica_current.mod
rename to test/unit/testing/point_ica_current.mod
diff --git a/test/unit/mod/post_events_syn.mod b/test/unit/testing/post_events_syn.mod
similarity index 100%
rename from test/unit/mod/post_events_syn.mod
rename to test/unit/testing/post_events_syn.mod
diff --git a/test/unit/mod/read_cai_init.mod b/test/unit/testing/read_cai_init.mod
similarity index 100%
rename from test/unit/mod/read_cai_init.mod
rename to test/unit/testing/read_cai_init.mod
diff --git a/test/unit/mod/read_eX.mod b/test/unit/testing/read_eX.mod
similarity index 100%
rename from test/unit/mod/read_eX.mod
rename to test/unit/testing/read_eX.mod
diff --git a/test/unit/mod/test0_kin_compartment.mod b/test/unit/testing/test0_kin_compartment.mod
similarity index 100%
rename from test/unit/mod/test0_kin_compartment.mod
rename to test/unit/testing/test0_kin_compartment.mod
diff --git a/test/unit/mod/test0_kin_conserve.mod b/test/unit/testing/test0_kin_conserve.mod
similarity index 100%
rename from test/unit/mod/test0_kin_conserve.mod
rename to test/unit/testing/test0_kin_conserve.mod
diff --git a/test/unit/mod/test0_kin_diff.mod b/test/unit/testing/test0_kin_diff.mod
similarity index 100%
rename from test/unit/mod/test0_kin_diff.mod
rename to test/unit/testing/test0_kin_diff.mod
diff --git a/test/unit/mod/test0_kin_steadystate.mod b/test/unit/testing/test0_kin_steadystate.mod
similarity index 100%
rename from test/unit/mod/test0_kin_steadystate.mod
rename to test/unit/testing/test0_kin_steadystate.mod
diff --git a/test/unit/mod/test1_kin_compartment.mod b/test/unit/testing/test1_kin_compartment.mod
similarity index 100%
rename from test/unit/mod/test1_kin_compartment.mod
rename to test/unit/testing/test1_kin_compartment.mod
diff --git a/test/unit/mod/test1_kin_conserve.mod b/test/unit/testing/test1_kin_conserve.mod
similarity index 100%
rename from test/unit/mod/test1_kin_conserve.mod
rename to test/unit/testing/test1_kin_conserve.mod
diff --git a/test/unit/mod/test1_kin_diff.mod b/test/unit/testing/test1_kin_diff.mod
similarity index 100%
rename from test/unit/mod/test1_kin_diff.mod
rename to test/unit/testing/test1_kin_diff.mod
diff --git a/test/unit/mod/test1_kin_steadystate.mod b/test/unit/testing/test1_kin_steadystate.mod
similarity index 100%
rename from test/unit/mod/test1_kin_steadystate.mod
rename to test/unit/testing/test1_kin_steadystate.mod
diff --git a/test/unit/mod/test2_kin_diff.mod b/test/unit/testing/test2_kin_diff.mod
similarity index 100%
rename from test/unit/mod/test2_kin_diff.mod
rename to test/unit/testing/test2_kin_diff.mod
diff --git a/test/unit/mod/test3_kin_diff.mod b/test/unit/testing/test3_kin_diff.mod
similarity index 100%
rename from test/unit/mod/test3_kin_diff.mod
rename to test/unit/testing/test3_kin_diff.mod
diff --git a/test/unit/mod/test4_kin_compartment.mod b/test/unit/testing/test4_kin_compartment.mod
similarity index 100%
rename from test/unit/mod/test4_kin_compartment.mod
rename to test/unit/testing/test4_kin_compartment.mod
diff --git a/test/unit/mod/test5_nonlinear_diff.mod b/test/unit/testing/test5_nonlinear_diff.mod
similarity index 100%
rename from test/unit/mod/test5_nonlinear_diff.mod
rename to test/unit/testing/test5_nonlinear_diff.mod
diff --git a/test/unit/mod/test6_nonlinear_diff.mod b/test/unit/testing/test6_nonlinear_diff.mod
similarity index 100%
rename from test/unit/mod/test6_nonlinear_diff.mod
rename to test/unit/testing/test6_nonlinear_diff.mod
diff --git a/test/unit/mod/test_ca.mod b/test/unit/testing/test_ca.mod
similarity index 100%
rename from test/unit/mod/test_ca.mod
rename to test/unit/testing/test_ca.mod
diff --git a/test/unit/mod/test_ca_read_valence.mod b/test/unit/testing/test_ca_read_valence.mod
similarity index 100%
rename from test/unit/mod/test_ca_read_valence.mod
rename to test/unit/testing/test_ca_read_valence.mod
diff --git a/test/unit/mod/test_cl_valence.mod b/test/unit/testing/test_cl_valence.mod
similarity index 100%
rename from test/unit/mod/test_cl_valence.mod
rename to test/unit/testing/test_cl_valence.mod
diff --git a/test/unit/mod/test_kin1.mod b/test/unit/testing/test_kin1.mod
similarity index 100%
rename from test/unit/mod/test_kin1.mod
rename to test/unit/testing/test_kin1.mod
diff --git a/test/unit/mod/test_kinlva.mod b/test/unit/testing/test_kinlva.mod
similarity index 100%
rename from test/unit/mod/test_kinlva.mod
rename to test/unit/testing/test_kinlva.mod
diff --git a/test/unit/mod/test_linear_init.mod b/test/unit/testing/test_linear_init.mod
similarity index 100%
rename from test/unit/mod/test_linear_init.mod
rename to test/unit/testing/test_linear_init.mod
diff --git a/test/unit/mod/test_linear_init_shuffle.mod b/test/unit/testing/test_linear_init_shuffle.mod
similarity index 100%
rename from test/unit/mod/test_linear_init_shuffle.mod
rename to test/unit/testing/test_linear_init_shuffle.mod
diff --git a/test/unit/mod/test_linear_state.mod b/test/unit/testing/test_linear_state.mod
similarity index 100%
rename from test/unit/mod/test_linear_state.mod
rename to test/unit/testing/test_linear_state.mod
diff --git a/test/unit/mod/write_Xi_Xo.mod b/test/unit/testing/write_Xi_Xo.mod
similarity index 100%
rename from test/unit/mod/write_Xi_Xo.mod
rename to test/unit/testing/write_Xi_Xo.mod
diff --git a/test/unit/mod/write_cai_breakpoint.mod b/test/unit/testing/write_cai_breakpoint.mod
similarity index 100%
rename from test/unit/mod/write_cai_breakpoint.mod
rename to test/unit/testing/write_cai_breakpoint.mod
diff --git a/test/unit/mod/write_eX.mod b/test/unit/testing/write_eX.mod
similarity index 100%
rename from test/unit/mod/write_eX.mod
rename to test/unit/testing/write_eX.mod
diff --git a/test/unit/mod/write_multiple_eX.mod b/test/unit/testing/write_multiple_eX.mod
similarity index 100%
rename from test/unit/mod/write_multiple_eX.mod
rename to test/unit/testing/write_multiple_eX.mod
diff --git a/test/unit/unit_test_catalogue.cpp b/test/unit/unit_test_catalogue.cpp
index a87b0529..66b89f50 100644
--- a/test/unit/unit_test_catalogue.cpp
+++ b/test/unit/unit_test_catalogue.cpp
@@ -1,50 +1,14 @@
 #include <arbor/mechcat.hpp>
 #include <arbor/version.hpp>
 
+#include "testing_catalogue.hpp"
+
 #ifdef ARB_GPU_ENABLED
 #include "backends/gpu/fvm.hpp"
 #endif
 #include "backends/multicore/fvm.hpp"
 
 #include "unit_test_catalogue.hpp"
-#include "mechanisms/ca_linear.hpp"
-#include "mechanisms/celsius_test.hpp"
-#include "mechanisms/diam_test.hpp"
-#include "mechanisms/gj0.hpp"
-#include "mechanisms/gj1.hpp"
-#include "mechanisms/non_linear.hpp"
-#include "mechanisms/param_as_state.hpp"
-#include "mechanisms/post_events_syn.hpp"
-#include "mechanisms/test0_kin_diff.hpp"
-#include "mechanisms/test_linear_state.hpp"
-#include "mechanisms/test_linear_init.hpp"
-#include "mechanisms/test_linear_init_shuffle.hpp"
-#include "mechanisms/test0_kin_conserve.hpp"
-#include "mechanisms/test0_kin_steadystate.hpp"
-#include "mechanisms/test0_kin_compartment.hpp"
-#include "mechanisms/test1_kin_compartment.hpp"
-#include "mechanisms/test1_kin_diff.hpp"
-#include "mechanisms/test1_kin_conserve.hpp"
-#include "mechanisms/test2_kin_diff.hpp"
-#include "mechanisms/test3_kin_diff.hpp"
-#include "mechanisms/test4_kin_compartment.hpp"
-#include "mechanisms/test5_nonlinear_diff.hpp"
-#include "mechanisms/test6_nonlinear_diff.hpp"
-#include "mechanisms/test1_kin_steadystate.hpp"
-#include "mechanisms/fixed_ica_current.hpp"
-#include "mechanisms/point_ica_current.hpp"
-#include "mechanisms/linear_ca_conc.hpp"
-#include "mechanisms/test_cl_valence.hpp"
-#include "mechanisms/test_ca_read_valence.hpp"
-#include "mechanisms/read_eX.hpp"
-#include "mechanisms/write_Xi_Xo.hpp"
-#include "mechanisms/write_multiple_eX.hpp"
-#include "mechanisms/write_eX.hpp"
-#include "mechanisms/read_cai_init.hpp"
-#include "mechanisms/write_cai_breakpoint.hpp"
-#include "mechanisms/test_ca.hpp"
-#include "mechanisms/test_kin1.hpp"
-#include "mechanisms/test_kinlva.hpp"
 
 #include "../gtest.h"
 
@@ -57,50 +21,9 @@
     }                                                                   \
     } while (false)
 
-using namespace arb;
-
-mechanism_catalogue make_unit_test_catalogue(const mechanism_catalogue& from) {
-    mechanism_catalogue cat = from;
-
-    ADD_MECH(cat, gj0);
-    ADD_MECH(cat, gj1);
-    ADD_MECH(cat, test_ca);
-    ADD_MECH(cat, test_kin1);
-    ADD_MECH(cat, test_kinlva);
-    ADD_MECH(cat, ca_linear);
-    ADD_MECH(cat, celsius_test);
-    ADD_MECH(cat, diam_test);
-    ADD_MECH(cat, param_as_state);
-    ADD_MECH(cat, post_events_syn);
-    ADD_MECH(cat, test_linear_state);
-    ADD_MECH(cat, test_linear_init);
-    ADD_MECH(cat, test_linear_init_shuffle);
-    ADD_MECH(cat, test0_kin_diff);
-    ADD_MECH(cat, test0_kin_conserve);
-    ADD_MECH(cat, test0_kin_steadystate);
-    ADD_MECH(cat, test0_kin_compartment);
-    ADD_MECH(cat, test1_kin_diff);
-    ADD_MECH(cat, test1_kin_conserve);
-    ADD_MECH(cat, test2_kin_diff);
-    ADD_MECH(cat, test3_kin_diff);
-    ADD_MECH(cat, test1_kin_steadystate);
-    ADD_MECH(cat, test1_kin_compartment);
-    ADD_MECH(cat, test4_kin_compartment);
-    ADD_MECH(cat, test5_nonlinear_diff);
-    ADD_MECH(cat, test6_nonlinear_diff);
-    ADD_MECH(cat, fixed_ica_current);
-    ADD_MECH(cat, non_linear);
-    ADD_MECH(cat, point_ica_current);
-    ADD_MECH(cat, linear_ca_conc);
-    ADD_MECH(cat, test_cl_valence);
-    ADD_MECH(cat, test_ca_read_valence);
-    ADD_MECH(cat, read_eX);
-    ADD_MECH(cat, write_Xi_Xo);
-    ADD_MECH(cat, write_multiple_eX);
-    ADD_MECH(cat, write_eX);
-    ADD_MECH(cat, read_cai_init);
-    ADD_MECH(cat, write_cai_breakpoint);
-
-    return cat;
+arb::mechanism_catalogue make_unit_test_catalogue(const arb::mechanism_catalogue& from) {
+    auto result = from;
+    result.import(arb::global_testing_catalogue(), "");
+    return result;
 }
 
diff --git a/test/unit/unit_test_catalogue.hpp b/test/unit/unit_test_catalogue.hpp
index 68435023..a378132b 100644
--- a/test/unit/unit_test_catalogue.hpp
+++ b/test/unit/unit_test_catalogue.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <arbor/export.hpp>
 #include <arbor/mechcat.hpp>
 
 arb::mechanism_catalogue make_unit_test_catalogue(const arb::mechanism_catalogue& from = {});
-- 
GitLab