diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0d522c7d5c15fb7f4bf3cd4e45924ae57a9bd750..abce9d7acbb240b6c2d6e0e49a0c273d4683c9b3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -152,7 +152,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
 # in the same CMakeLists.txt in which the target is defined.
 
 # Interface library `arbor-config-defs` collects configure-time defines
-# for arbor, arborenv, arborio and arbornml, of the form ARB_HAVE_XXX. These
+# for arbor, arborenv, arborio, of the form ARB_HAVE_XXX. These
 # defines should _not_ be used in any installed public headers.
 
 add_library(arbor-config-defs INTERFACE)
@@ -171,13 +171,6 @@ add_library(arborenv-private-deps INTERFACE)
 target_link_libraries(arborenv-private-deps INTERFACE arbor-config-defs)
 install(TARGETS arborenv-private-deps EXPORT arbor-targets)
 
-# Interface library `arbornml-private-deps` collects dependencies, options etc.
-# for the arbornml library.
-
-add_library(arbornml-private-deps INTERFACE)
-target_link_libraries(arbornml-private-deps INTERFACE arbor-config-defs)
-install(TARGETS arbornml-private-deps EXPORT arbor-targets)
-
 # Interface library `arborio-private-deps` collects dependencies, options etc.
 # for the arborio library.
 
@@ -192,12 +185,12 @@ install(TARGETS arborio-private-deps EXPORT arbor-targets)
 add_library(arbor-public-deps INTERFACE)
 install(TARGETS arbor-public-deps EXPORT arbor-targets)
 
-# Interface library `arbornml-public-deps` collects requirements for the
-# users of the arbornml library (e.g. xml libs) that will become part
-# of arbornml's PUBLIC interface.
+# Interface library `arborio-public-deps` collects requirements for the
+# users of the arborio library (e.g. xml libs) that will become part
+# of arborio's PUBLIC interface.
 
-add_library(arbornml-public-deps INTERFACE)
-install(TARGETS arbornml-public-deps EXPORT arbornml-targets)
+add_library(arborio-public-deps INTERFACE)
+install(TARGETS arborio-public-deps EXPORT arborio-targets)
 
 # External libraries in `ext` sub-directory: json, tinyopt and randon123.
 # Creates interface libraries `ext-json`, `ext-tinyopt` and `ext-random123`
@@ -218,7 +211,7 @@ set(arbor_export_dependencies)
 
 # Keep track of which 'components' of arbor are included (this is
 # currently just 'MPI' support and 'neuroml' for NeuroML support in
-# libarbornml.)
+# libarborio.)
 
 set(arbor_supported_components)
 
@@ -439,11 +432,6 @@ add_subdirectory(arborenv)
 # arborio, arborio-public-headers:
 add_subdirectory(arborio)
 
-# arbornml, arbornml-public-headers:
-if(ARB_WITH_NEUROML)
-    add_subdirectory(arbornml)
-endif()
-
 # unit, unit-mpi, unit-local, unit-modcc
 add_subdirectory(test)
 
@@ -493,7 +481,6 @@ endif()
 set(arbor_override_import_lang)
 set(arbor_add_import_libs)
 set(arborenv_add_import_libs)
-set(arbornml_add_import_libs)
 set(arborio_add_import_libs)
 
 if(ARB_WITH_GPU)
diff --git a/arbor/include/CMakeLists.txt b/arbor/include/CMakeLists.txt
index ebc48ee4f0b0b2bf97adb790e30175fb15a675da..b61d4a6d45ed4be0176ce18213511a1b00bef474 100644
--- a/arbor/include/CMakeLists.txt
+++ b/arbor/include/CMakeLists.txt
@@ -38,6 +38,10 @@ if(ARB_WITH_GPU)
     # define ARB_GPU_ENABLED in version.hpp
     list(APPEND arb_features GPU)
 endif()
+if(ARB_WITH_NEUROML)
+    # define ARB_NEUROML_ENABLED in version.hpp
+    list(APPEND arb_features NEUROML)
+endif()
 if(ARB_WITH_PROFILING)
     # define ARB_PROFILE_ENABLED in version.hpp
     list(APPEND arb_features PROFILE)
diff --git a/arborio/CMakeLists.txt b/arborio/CMakeLists.txt
index e07ce90d4ad50dcedece2352043487c5aab618d6..4cc0e2a3ac6512f49928d191048d81d2a5974569 100644
--- a/arborio/CMakeLists.txt
+++ b/arborio/CMakeLists.txt
@@ -1,6 +1,16 @@
 set(arborio-sources
     swcio.cpp
 )
+if(ARB_WITH_NEUROML)
+    list(APPEND arborio-sources
+            arbornml.cpp
+            parse_morphology.cpp
+            with_xml.cpp
+            xmlwrap.cpp
+        )
+endif()
+
+find_package(LibXml2 REQUIRED)
 
 add_library(arborio ${arborio-sources})
 
@@ -10,7 +20,16 @@ target_include_directories(arborio-public-headers INTERFACE
     $<INSTALL_INTERFACE:include>
 )
 
-target_link_libraries(arborio PUBLIC arbor arborio-public-headers)
+if(ARB_WITH_NEUROML)
+    target_link_libraries(arborio PUBLIC arbor arborio-public-headers LibXml2::LibXml2)
+    list(APPEND arbor_export_dependencies "LibXml2")
+    set(arbor_export_dependencies "${arbor_export_dependencies}" PARENT_SCOPE)
+    list(APPEND arbor_supported_components "neuroml")
+    set(arbor_supported_components "${arbor_supported_components}" PARENT_SCOPE)
+else ()
+    target_link_libraries(arborio PUBLIC arbor arborio-public-headers)
+endif()
+
 target_link_libraries(arborio PRIVATE arbor-config-defs arborio-private-deps)
 
 install(DIRECTORY include/arborio
diff --git a/arbornml/arbornml.cpp b/arborio/arbornml.cpp
similarity index 60%
rename from arbornml/arbornml.cpp
rename to arborio/arbornml.cpp
index 15c5f5551d4662c438d970d5b708ccdd4e9e9012..0ee14b0814f425eed81c281da00628660ec05fc9 100644
--- a/arbornml/arbornml.cpp
+++ b/arborio/arbornml.cpp
@@ -3,8 +3,7 @@
 #include <string>
 #include <vector>
 
-#include <arbornml/arbornml.hpp>
-#include <arbornml/nmlexcept.hpp>
+#include <arborio/arbornml.hpp>
 
 #include "parse_morphology.hpp"
 #include "xmlwrap.hpp"
@@ -12,7 +11,57 @@
 using std::optional;
 using std::nullopt;
 
-namespace arbnml {
+namespace arborio {
+
+static std::string fmt_error(const char* prefix, const std::string& err, unsigned line) {
+    return prefix + (line==0? err: "line " + std::to_string(line) + ": " + err);
+}
+
+xml_error::xml_error(const std::string& xml_error_msg, unsigned line):
+    neuroml_exception(fmt_error("xml error: ", xml_error_msg, line)),
+    xml_error_msg(xml_error_msg),
+    line(line)
+{}
+
+no_document::no_document():
+    neuroml_exception("no NeuroML document to parse")
+{}
+
+parse_error::parse_error(const std::string& error_msg, unsigned line):
+    neuroml_exception(fmt_error("parse error: ", error_msg, line)),
+    error_msg(error_msg),
+    line(line)
+{}
+
+bad_segment::bad_segment(unsigned long long segment_id, unsigned line):
+    neuroml_exception(
+        fmt_error(
+            "bad morphology segment: ",
+            "segment "+(segment_id+1==0? "unknown": "\""+std::to_string(segment_id)+"\""),
+            line)),
+    segment_id(segment_id),
+    line(line)
+{}
+
+bad_segment_group::bad_segment_group(const std::string& group_id, unsigned line):
+    neuroml_exception(
+        fmt_error(
+            "bad morphology segmentGroup: ",
+            "segmentGroup id "+(group_id.empty()? "unknown": "\""+group_id+"\""),
+            line)),
+    group_id(group_id),
+    line(line)
+{}
+
+cyclic_dependency::cyclic_dependency(const std::string& id, unsigned line):
+    neuroml_exception(
+        fmt_error(
+            "cyclic dependency: ",
+            "element id \""+id+"\"",
+            line)),
+    id(id),
+    line(line)
+{}
 
 struct neuroml_impl {
     xml_doc doc;
@@ -93,4 +142,4 @@ optional<morphology_data> neuroml::cell_morphology(const std::string& cell_id) c
     return M;
 }
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/include/arbornml/arbornml.hpp b/arborio/include/arborio/arbornml.hpp
similarity index 54%
rename from arbornml/include/arbornml/arbornml.hpp
rename to arborio/include/arborio/arbornml.hpp
index b925aaedebdd35176a0d659bf862fdf0c006b7c6..0f3d939e5049450245754201889eeabcd12abfda 100644
--- a/arbornml/include/arbornml/arbornml.hpp
+++ b/arborio/include/arborio/arbornml.hpp
@@ -1,5 +1,7 @@
 #pragma once
 
+#include <cstddef>
+#include <stdexcept>
 #include <optional>
 #include <memory>
 #include <string>
@@ -9,7 +11,58 @@
 #include <arbor/morph/label_dict.hpp>
 #include <arbor/morph/morphology.hpp>
 
-namespace arbnml {
+namespace arborio {
+
+// Common base-class for neuroml run-time errors.
+struct neuroml_exception: std::runtime_error {
+    neuroml_exception(const std::string& what_arg):
+        std::runtime_error(what_arg)
+    {}
+};
+
+// Generic XML error (as reported by libxml2).
+struct xml_error: neuroml_exception {
+    xml_error(const std::string& xml_error_msg, unsigned line = 0);
+    std::string xml_error_msg;
+    unsigned line;
+};
+
+// Can't parse NeuroML if we don't have a document.
+struct no_document: neuroml_exception {
+    no_document();
+};
+
+// Generic error parsing NeuroML data.
+struct parse_error: neuroml_exception {
+    parse_error(const std::string& error_msg, unsigned line = 0);
+    std::string error_msg;
+    unsigned line;
+};
+
+// NeuroML morphology error: improper segment data, e.g. bad id specification,
+// segment parent does not exist, fractionAlong is out of bounds, missing
+// required <proximal> data.
+struct bad_segment: neuroml_exception {
+    bad_segment(unsigned long long segment_id, unsigned line = 0);
+    unsigned long long segment_id;
+    unsigned line;
+};
+
+// NeuroML morphology error: improper segmentGroup data, e.g. malformed
+// element data, missing referenced segments or groups, etc.
+struct bad_segment_group: neuroml_exception {
+    bad_segment_group(const std::string& group_id, unsigned line = 0);
+    std::string group_id;
+    unsigned line;
+};
+
+// A segment or segmentGroup ultimately refers to itself via `parent`
+// or `include` elements respectively.
+struct cyclic_dependency: neuroml_exception {
+    cyclic_dependency(const std::string& id, unsigned line = 0);
+    std::string id;
+    unsigned line;
+};
 
 // Collect and parse morphology elements from XML.
 // No validation is performed against the NeuroML v2 schema.
@@ -71,4 +124,4 @@ private:
     std::unique_ptr<neuroml_impl> impl_;
 };
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/include/arbornml/with_xml.hpp b/arborio/include/arborio/with_xml.hpp
similarity index 80%
rename from arbornml/include/arbornml/with_xml.hpp
rename to arborio/include/arborio/with_xml.hpp
index c308451f7ba5618450cdbb79f3d156216a73ce0d..d176daa00413a182869fcec94aa61d12de45e42a 100644
--- a/arbornml/include/arbornml/with_xml.hpp
+++ b/arborio/include/arborio/with_xml.hpp
@@ -2,11 +2,11 @@
 
 // Wrap initialization and cleanup of libxml2 library.
 //
-// Use of `with_xml` is only necessary if arbornml is being
+// Use of `with_xml` is only necessary if arborio is being
 // used in a multithreaded context and the client code is
 // not managing libxml2 initialization and cleanup.
 
-namespace arbnml {
+namespace arborio {
 
 struct with_xml {
     with_xml();
@@ -21,4 +21,4 @@ struct with_xml {
     bool run_cleanup_;
 };
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/parse_morphology.cpp b/arborio/parse_morphology.cpp
similarity index 99%
rename from arbornml/parse_morphology.cpp
rename to arborio/parse_morphology.cpp
index 3f3cbf556c5903fc0825d4e577f41e18064a7f9b..d7adc67a7a1edc28aa64d0a601322e01ba37ce8b 100644
--- a/arbornml/parse_morphology.cpp
+++ b/arborio/parse_morphology.cpp
@@ -14,8 +14,7 @@
 #include <arbor/morph/stitch.hpp>
 #include <arbor/util/expected.hpp>
 
-#include <arbornml/arbornml.hpp>
-#include <arbornml/nmlexcept.hpp>
+#include <arborio/arbornml.hpp>
 
 #include "parse_morphology.hpp"
 #include "xmlwrap.hpp"
@@ -25,7 +24,7 @@ using arb::region;
 using arb::util::expected;
 using arb::util::unexpected;
 
-namespace arbnml {
+namespace arborio {
 
 // Box is a container of size 0 or 1.
 
@@ -554,4 +553,4 @@ morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph) {
     return M;
 }
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/parse_morphology.hpp b/arborio/parse_morphology.hpp
similarity index 60%
rename from arbornml/parse_morphology.hpp
rename to arborio/parse_morphology.hpp
index 305dcb6b13f694437a9686253bf09ac3235d69a6..0ced56cd683067a4ebc12fc94c9fafdd82fa6354 100644
--- a/arbornml/parse_morphology.hpp
+++ b/arborio/parse_morphology.hpp
@@ -1,10 +1,10 @@
 #pragma once
 
-#include <arbornml/arbornml.hpp>
+#include <arborio/arbornml.hpp>
 #include "xmlwrap.hpp"
 
-namespace arbnml {
+namespace arborio {
 
 morphology_data parse_morphology_element(xml_xpathctx ctx, xml_node morph);
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/with_xml.cpp b/arborio/with_xml.cpp
similarity index 83%
rename from arbornml/with_xml.cpp
rename to arborio/with_xml.cpp
index c7ae41d0278d61053aabb73f3a741ee8d9c2bb0d..269c174d31b6da67a4f2fd841150ad5065079f8a 100644
--- a/arbornml/with_xml.cpp
+++ b/arborio/with_xml.cpp
@@ -1,8 +1,8 @@
-#include <arbornml/with_xml.hpp>
+#include <arborio/with_xml.hpp>
 
 #include <libxml/parser.h>
 
-namespace arbnml {
+namespace arborio {
 
 with_xml::with_xml(): run_cleanup_(true) {
     // Initialize before any multithreaded access by library or client code.
@@ -19,4 +19,4 @@ with_xml::~with_xml() {
     }
 }
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/xmlwrap.cpp b/arborio/xmlwrap.cpp
similarity index 95%
rename from arbornml/xmlwrap.cpp
rename to arborio/xmlwrap.cpp
index c46b129284a106d94cea0dba974a9f2d22b60ca1..126f2ca6f1ff740cfbc4215c7e63e2a9f1efcafb 100644
--- a/arbornml/xmlwrap.cpp
+++ b/arborio/xmlwrap.cpp
@@ -12,11 +12,9 @@
 
 #include <libxml/xmlerror.h>
 
-#include <arbornml/nmlexcept.hpp>
-
 #include "xmlwrap.hpp"
 
-namespace arbnml {
+namespace arborio {
 
 namespace detail {
 
@@ -95,14 +93,14 @@ void throw_on_xml_generic_error(void *, const char* msg, ...) {
     vsnprintf(&err[0], err.size(), msg, vb);
     va_end(vb);
 
-    throw ::arbnml::xml_error(err);
+    throw ::arborio::xml_error(err);
 }
 
 void throw_on_xml_structured_error(void *ctx, xmlErrorPtr errp) {
     if (errp->level!=1) { // ignore warnings!
         std::string msg(errp->message);
         if (!msg.empty() && msg.back()=='\n') msg.pop_back();
-        throw ::arbnml::xml_error(msg, errp->line);
+        throw ::arborio::xml_error(msg, errp->line);
     }
 }
 
@@ -125,4 +123,4 @@ xml_error_scope::~xml_error_scope() {
     xmlStructuredErrorContext = structured_context_;
 }
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/xmlwrap.hpp b/arborio/xmlwrap.hpp
similarity index 98%
rename from arbornml/xmlwrap.hpp
rename to arborio/xmlwrap.hpp
index d914bc69f3009d358bcd948675b7f108a48373e2..0993b9ba0665d5f7002c614ef416a69cea007667 100644
--- a/arbornml/xmlwrap.hpp
+++ b/arborio/xmlwrap.hpp
@@ -14,9 +14,9 @@
 #include <libxml/xpath.h>
 #include <libxml/xpathInternals.h>
 
-#include <arbornml/nmlexcept.hpp>
+#include "arborio/arbornml.hpp"
 
-namespace arbnml {
+namespace arborio {
 
 // `non_negative` represents the corresponding constraint in the schema, which
 // can mean any arbitrarily large non-negtative integer value.
@@ -300,7 +300,7 @@ inline std::string xpath_escape(const std::string& x) {
 // xml_error_scope object will restore the original error handling
 // behaviour on destruction.
 //
-// Errors are turned into arbnml::xml_error exceptions and thrown,
+// Errors are turned into arborio::xml_error exceptions and thrown,
 // while warnings are ignored (libxml2 warnings are highly innocuous).
 
 struct xml_error_scope {
@@ -314,4 +314,4 @@ struct xml_error_scope {
     void* structured_context_;
 };
 
-} // namespace arbnml
+} // namespace arborio
diff --git a/arbornml/CMakeLists.txt b/arbornml/CMakeLists.txt
deleted file mode 100644
index a4d944980f8402f18dc731672caf817a9212eb5e..0000000000000000000000000000000000000000
--- a/arbornml/CMakeLists.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-set(arbornml-sources
-    arbornml.cpp
-    nmlexcept.cpp
-    parse_morphology.cpp
-    with_xml.cpp
-    xmlwrap.cpp
-)
-
-find_package(LibXml2 REQUIRED)
-
-add_library(arbornml ${arbornml-sources})
-
-add_library(arbornml-public-headers INTERFACE)
-target_include_directories(arbornml-public-headers INTERFACE
-    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
-    $<INSTALL_INTERFACE:include>
-)
-
-target_link_libraries(arbornml PUBLIC arbor arbornml-public-headers LibXml2::LibXml2)
-target_link_libraries(arbornml PRIVATE arbor-config-defs arbornml-private-deps)
-
-install(DIRECTORY include/arbornml
-    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
-    FILES_MATCHING PATTERN "*.hpp")
-
-install(TARGETS arbornml-public-headers EXPORT arbor-targets)
-install(TARGETS arbornml EXPORT arbor-targets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
-
-list(APPEND arbor_export_dependencies "LibXml2")
-set(arbor_export_dependencies "${arbor_export_dependencies}" PARENT_SCOPE)
-list(APPEND arbor_supported_components "neuroml")
-set(arbor_supported_components "${arbor_supported_components}" PARENT_SCOPE)
diff --git a/arbornml/include/arbornml/nmlexcept.hpp b/arbornml/include/arbornml/nmlexcept.hpp
deleted file mode 100644
index bba15a80062678fc53846b9437e2c3e1032e9b10..0000000000000000000000000000000000000000
--- a/arbornml/include/arbornml/nmlexcept.hpp
+++ /dev/null
@@ -1,67 +0,0 @@
-#pragma once
-
-#include <cstddef>
-#include <stdexcept>
-#include <string>
-
-namespace arbnml {
-
-// Common base-class for arbnml run-time errors.
-
-struct neuroml_exception: std::runtime_error {
-    neuroml_exception(const std::string& what_arg):
-        std::runtime_error(what_arg)
-    {}
-};
-
-// Generic XML error (as reported by libxml2).
-
-struct xml_error: neuroml_exception {
-    xml_error(const std::string& xml_error_msg, unsigned line = 0);
-    std::string xml_error_msg;
-    unsigned line;
-};
-
-// Can't parse NeuroML if we don't have a document.
-
-struct no_document: neuroml_exception {
-    no_document();
-};
-
-// Generic error parsing NeuroML data.
-
-struct parse_error: neuroml_exception {
-    parse_error(const std::string& error_msg, unsigned line = 0);
-    std::string error_msg;
-    unsigned line;
-};
-
-// NeuroML morphology error: improper segment data, e.g. bad id specification,
-// segment parent does not exist, fractionAlong is out of bounds, missing
-// required <proximal> data.
-
-struct bad_segment: neuroml_exception {
-    bad_segment(unsigned long long segment_id, unsigned line = 0);
-    unsigned long long segment_id;
-    unsigned line;
-};
-
-// NeuroML morphology error: improper segmentGroup data, e.g. malformed
-// element data, missing referenced segments or groups, etc.
-
-struct bad_segment_group: neuroml_exception {
-    bad_segment_group(const std::string& group_id, unsigned line = 0);
-    std::string group_id;
-    unsigned line;
-};
-
-// A segment or segmentGroup ultimately refers to itself via `parent`
-// or `include` elements respectively.
-
-struct cyclic_dependency: neuroml_exception {
-    cyclic_dependency(const std::string& id, unsigned line = 0);
-    std::string id;
-    unsigned line;
-};
-
-} // namespace arbnml
diff --git a/arbornml/nmlexcept.cpp b/arbornml/nmlexcept.cpp
deleted file mode 100644
index 7ad1c8cac289b0d5ea869e8db40173c5b27be392..0000000000000000000000000000000000000000
--- a/arbornml/nmlexcept.cpp
+++ /dev/null
@@ -1,57 +0,0 @@
-#include <string>
-
-#include <arbornml/nmlexcept.hpp>
-
-namespace arbnml {
-
-static std::string fmt_error(const char* prefix, const std::string& err, unsigned line) {
-    return prefix + (line==0? err: "line " + std::to_string(line) + ": " + err);
-}
-
-xml_error::xml_error(const std::string& xml_error_msg, unsigned line):
-    neuroml_exception(fmt_error("xml error: ", xml_error_msg, line)),
-    xml_error_msg(xml_error_msg),
-    line(line)
-{}
-
-no_document::no_document():
-    neuroml_exception("no NeuroML document to parse")
-{}
-
-parse_error::parse_error(const std::string& error_msg, unsigned line):
-    neuroml_exception(fmt_error("parse error: ", error_msg, line)),
-    error_msg(error_msg),
-    line(line)
-{}
-
-bad_segment::bad_segment(unsigned long long segment_id, unsigned line):
-    neuroml_exception(
-        fmt_error(
-            "bad morphology segment: ",
-            "segment "+(segment_id+1==0? "unknown": "\""+std::to_string(segment_id)+"\""),
-            line)),
-    segment_id(segment_id),
-    line(line)
-{}
-
-bad_segment_group::bad_segment_group(const std::string& group_id, unsigned line):
-    neuroml_exception(
-        fmt_error(
-            "bad morphology segmentGroup: ",
-            "segmentGroup id "+(group_id.empty()? "unknown": "\""+group_id+"\""),
-            line)),
-    group_id(group_id),
-    line(line)
-{}
-
-cyclic_dependency::cyclic_dependency(const std::string& id, unsigned line):
-    neuroml_exception(
-        fmt_error(
-            "cyclic dependency: ",
-            "element id \""+id+"\"",
-            line)),
-    id(id),
-    line(line)
-{}
-
-} // namespace arbnml
diff --git a/cmake/arbor-config.cmake.in b/cmake/arbor-config.cmake.in
index d551de69d3108bfc7ff2f8553f0e6ae3bcc256db..d7b056d91738ae3c8fb1f05565c2da9cf906ed1b 100644
--- a/cmake/arbor-config.cmake.in
+++ b/cmake/arbor-config.cmake.in
@@ -21,7 +21,7 @@ endforeach()
 
 set(_override_lang @arbor_override_import_lang@)
 if(_override_lang)
-    foreach(target arbor::arbor arbor::arborenv arbor::arbornml)
+    foreach(target arbor::arbor arbor::arborenv arbor::arborio)
         if(TARGET ${target})
             set_target_properties(${target} PROPERTIES IMPORTED_LINK_INTERFACE_LANGUAGES_@arbor_build_config@ "${_override_lang}")
         endif()
@@ -53,5 +53,5 @@ set(ARB_CXXOPT_ARCH @ARB_CXXOPT_ARCH@)
 
 _append_property(arbor::arbor INTERFACE_LINK_LIBRARIES @arbor_add_import_libs@)
 _append_property(arbor::arborenv INTERFACE_LINK_LIBRARIES @arborenv_add_import_libs@)
-_append_property(arbor::arbornml INTERFACE_LINK_LIBRARIES @arbornml_add_import_libs@)
+_append_property(arbor::arborio INTERFACE_LINK_LIBRARIES @arborio_add_import_libs@)
 
diff --git a/doc/cpp/neuroml.rst b/doc/cpp/neuroml.rst
index d495b6616f3af04cbcc9e0cecb9ef885512772ca..290e74b2e698444640b08b742ccc5c72d6221097 100644
--- a/doc/cpp/neuroml.rst
+++ b/doc/cpp/neuroml.rst
@@ -8,32 +8,31 @@ Arbor offers limited support for models described in
 This is not built by default, but can be enabled by
 providing the `-DARB_NEUROML=ON` argument to CMake at
 configuration time (see :ref:`install-neuroml`). This will
-build the ``arbornml`` libray and defines the corresponding
-``arbor::arbornml`` CMake target.
+build the ``arborio`` libray with neuroml support.
 
-The ``arbornml`` library uses `libxml2 <http://xmlsoft.org/>`_
-for XML parsing. Applications using ``arbornml`` will need to
-link against ``libxml2`` in addition, though this is performed
-implicitly within CMake projects that add ``arbor::arbornml``
+The ``arborio`` library uses `libxml2 <http://xmlsoft.org/>`_
+for XML parsing. Applications using NeuroML through ``arborio``
+will need to link against ``libxml2`` in addition, though this
+is performed implicitly within CMake projects that add ``arbor::arborio``
 as a link library.
 
-All classes and functions provided by the ``arbornml`` library
-are provided in the ``arbnml`` namespace.
+All classes and functions provided by the ``arborio`` library
+are provided in the ``arborio`` namespace.
 
 
 Libxml2 interface
 -----------------
 
 Libxml2 offers threadsafe XML parsing, but not by default. If
-the application uses ``arbornml`` in an unthreaded context, or
-has already explicitly initialized ``libxml2``, nothing more
-needs to be done. Otherwise, the ``libxml2`` function ``xmlInitParser()``
-must be called explicitly.
+the application uses NeuromML support from ``arborio`` in an
+unthreaded context, or has already explicitly initialized ``libxml2``,
+nothing more needs to be done. Otherwise, the ``libxml2`` function
+``xmlInitParser()`` must be called explicitly.
 
-``arbornml`` provides a helper guard object for this purpose, defined
-in ``arbornml/with_xml.hpp``:
+``arborio`` provides a helper guard object for this purpose, defined
+in ``arborio/with_xml.hpp``:
 
-.. cpp:namespace:: arbnml
+.. cpp:namespace:: arborio
 
 .. cpp:class:: with_xml
 
@@ -44,11 +43,11 @@ in ``arbornml/with_xml.hpp``:
 NeuroML 2 morphology support
 ----------------------------
 
-NeuroML documents are represented by the ``arbnml::neuroml`` class,
+NeuroML documents are represented by the ``arborio::neuroml`` class,
 which in turn provides methods for the identification and translation
 of morphology data. ``neuroml`` objects are moveable and move-assignable, but not copyable.
 
-An implementation limitation restrictes valid segment id values to
+An implementation limitation restricts valid segment id values to
 those which can be represented by an ``unsigned long long`` value.
 
 .. cpp:class:: neuroml
@@ -111,14 +110,14 @@ segment group.
 
    .. cpp:member:: std::unordered_map<std::string, std::vector<unsigned long long>> group_segments
 
-   A map from taking each segment group id to its corresponding collection of segments.
+   A map from each segment group id to its corresponding collection of segments.
 
 
 Exceptions
 ----------
 
-All NeuroML-specific exceptions are defined in ``arbornml/nmlexcept.hpp``, and are
-derived from ``arbnml::neuroml_exception`` which in turn is derived from ``std::runtime_error``.
+All NeuroML-specific exceptions are defined in ``arborio/arbornml.hpp``, and are
+derived from ``arborio::neuroml_exception`` which in turn is derived from ``std::runtime_error``.
 With the exception of the ``no_document`` exception, all contain an unsigned member ``line``
 which is intended to identify the problematic construct within the document.
 
diff --git a/doc/install/build_install.rst b/doc/install/build_install.rst
index c1810c3b699d35e8f2504a729f733ee176def754..7181512c400ba5c03c833a99852637511510947c 100644
--- a/doc/install/build_install.rst
+++ b/doc/install/build_install.rst
@@ -129,9 +129,10 @@ NeuroML
 ~~~~~~~
 
 Arbor supports reading cell morphologies defined in NeuroML version 2 through
-an additional NeuroML support library ``arbornml``. This library requires
-`libxml2 <http://xmlsoft.org>`_ for the parsing of NeuroML2 XML. See :ref:`install-neuroml` for
-more information.
+an additional support library ``arborio``. This library requires
+`libxml2 <http://xmlsoft.org>`_ for the parsing of NeuroML2 XML, if it is built
+with NeuroML support enabled.
+See :ref:`install-neuroml` for more information.
 
 
 Documentation
@@ -485,27 +486,27 @@ NeuroML support
 ---------------
 
 Arbor has limited support for NeuroML version 2 through an additional library
-``arbornml``. This library will be built if the option ``-DARB_WITH_NEUROML=ON``
-is passed to CMake at configuration time. ``arbornml`` depends upon the
-the ``libxml2`` library for XML parsing.
+``arborio``. This library will be built with NeuroML support if the option
+``-DARB_WITH_NEUROML=ON`` is passed to CMake at configuration time.
+``arborio`` depends upon the the ``libxml2`` library for XML parsing.
 
-With NeuroML support enabled, Arbor will additionally install the static library
-``libarbornml.a``. Applications using this functionality will need to link
+Arbor will additionally install the static library ``libarborio.a``.
+Applications using this functionality will need to link
 against this library in addition to the main Arbor library and ``libxml2``.
 For example:
 
 .. code-block:: bash
 
-    g++ -std=c++17 -pthread mycode.cpp -larbornml -larbor -lxml2
+    g++ -std=c++17 -pthread mycode.cpp -larborio -larbor -lxml2
 
 For projects using CMake, Arbor NeuroML support can be required with the
-component ``neuroml``. The corresponding CMake library target is ``arbor::arbornml``.
+component ``neuroml``. The corresponding CMake library target is ``arbor::arborio``.
 
 .. code-block:: cmake
 
    find_package(arbor COMPONENTS neuroml)
    # ...
-   target_link_libraries(myapp arbor::arbornml)
+   target_link_libraries(myapp arbor::arborio)
 
 
 .. _install:
diff --git a/doc/python/morphology.rst b/doc/python/morphology.rst
index ef9eabb5be25c60ffb973f76d29bb67f128db841..8ffa546686379541589a8527da5df8e598d37965 100644
--- a/doc/python/morphology.rst
+++ b/doc/python/morphology.rst
@@ -482,3 +482,86 @@ Cell morphology
        Compose the two isometries to form a new isometry that applies *b* and then applies *a*.
        Note that rotations are composed as being with respect to the *intrinsic* coordinate system,
        while translations are always taken to be with respect to the *extrinsic* absolute coordinate system.
+
+
+.. py:class:: neuroml_morph_data
+
+    A :class:`neuroml_morphology_data` object contains a representation of a morphology defined in
+    NeuroML.
+
+
+    .. py:attribute:: cell_id
+       :type: optional<str>
+
+       The id attribute of the cell that was used to find the morphology in the NeuroML document, if any.
+
+    .. py:attribute:: id
+       :type: str
+
+       The id attribute of the morphology.
+
+    .. py:attribute:: group_segments
+       :type: dict[str, list[long]]
+
+       A map from each segment group id to its corresponding collection of segments.
+
+    .. py:method:: segments
+
+       Returns a label dictionary with a region entry for each segment, keyed by the segment id (as a string).
+
+       :rtype: label_dict
+
+    .. py:method:: named_segments
+
+       Returns a label dictionary with a region entry for each name attribute given to one or more segments.
+       The region corresponds to the union of all segments sharing the same name attribute.
+
+       :rtype: label_dict
+
+    .. py:method:: groups
+
+       Returns a label dictionary  with a region entry for each defined segment group.
+
+       :rtype: label_dict
+
+.. py:class:: neuroml
+
+    A :class:`neuroml` object represent NeuroML documents, and provides methods for the identification and
+    translation of morphology data.
+
+    An implementation limitation restricts valid segment id values to those which can be represented by an
+    unsigned long long value.
+
+   .. py:method:: neuroml(filename)
+
+      Build a NeuroML document representation from the supplied file contents.
+
+      :param str filename: the name of the NeuroML file.
+
+   .. py:method:: cell_ids()
+
+      Return the id of each ``<cell>`` element defined in the NeuroML document.
+
+      :rtype: list[str]
+
+   .. py:method:: morphology_ids()
+
+      Return the id of each top-level ``<morphology>`` element defined in the NeuroML document.
+
+      :rtype: list[str]
+
+   .. py:method:: morphology(morph_id)
+
+      Returns a representation of the top-level morphology with the supplied morph_id if it could be found.
+      Parse errors or an inconsistent representation will raise an exception.
+
+      :param str morph_id: ID of the top-level morphology.
+      :rtype: optional(neuroml_morph_data)
+
+   .. py:method:: cell_morphology(cell_id)
+
+      Returns a representation of the morphology associated with the cell with the supplied cell_id if it
+      could be found. Parse errors or an inconsistent representation will raise an exception.
+
+      :param str morph_id: ID of the cell.
+      :rtype: optional(neuroml_morph_data)
\ No newline at end of file
diff --git a/python/cells.cpp b/python/cells.cpp
index bc9d15468b82964bf630ef9d5a9cefb3773e44be..2f610f569ef3e798a922f387b0cd6d56a888d18d 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -28,105 +28,13 @@
 #include "arbor/cv_policy.hpp"
 #include "conversion.hpp"
 #include "error.hpp"
+#include "proxy.hpp"
 #include "pybind11/cast.h"
 #include "pybind11/pytypes.h"
 #include "schedule.hpp"
 #include "strprintf.hpp"
 
 namespace pyarb {
-//
-//  proxies
-//
-
-struct label_dict_proxy {
-    using str_map = std::unordered_map<std::string, std::string>;
-    arb::label_dict dict;
-    str_map cache;
-    std::vector<std::string> locsets;
-    std::vector<std::string> regions;
-
-    label_dict_proxy() = default;
-
-    label_dict_proxy(const str_map& in) {
-        for (auto& i: in) {
-            set(i.first.c_str(), i.second.c_str());
-        }
-    }
-
-    std::size_t size() const  {
-        return locsets.size() + regions.size();
-    }
-
-    void set(const char* name, const char* desc) {
-        using namespace std::string_literals;
-        // The following code takes an input name and a region or locset
-        // description, e.g.:
-        //      name='reg', desc='(tag 4)'
-        //      name='loc', desc='(terminal)'
-        //      name='foo', desc='(join (tag 2) (tag 3))'
-        // Then it parses the description, and tests whether the description
-        // is a region or locset, and updates the label dictionary appropriately.
-        // Errors occur when:
-        //  * a region is described with a name that matches an existing locset
-        //    (and vice versa.)
-        //  * the description is not well formed, e.g. it contains a syntax error.
-        //  * the description is well-formed, but describes neither a region or locset.
-        try{
-            // Evaluate the s-expression to build a region/locset.
-            auto result = arb::parse_label_expression(desc);
-            if (!result) { // an error parsing / evaluating description.
-                throw result.error();
-            }
-            else if (result->type()==typeid(arb::region)) { // describes a region.
-                dict.set(name, std::move(std::any_cast<arb::region&>(*result)));
-                auto it = std::lower_bound(regions.begin(), regions.end(), name);
-                if (it==regions.end() || *it!=name) regions.insert(it, name);
-            }
-            else if (result->type()==typeid(arb::locset)) { // describes a locset.
-                dict.set(name, std::move(std::any_cast<arb::locset&>(*result)));
-                auto it = std::lower_bound(locsets.begin(), locsets.end(), name);
-                if (it==locsets.end() || *it!=name) locsets.insert(it, name);
-            }
-            else {
-                // Successfully parsed an expression that is neither region nor locset.
-                throw util::pprintf("The defninition of '{} = {}' does not define a valid region or locset.", name, desc);
-            }
-            // The entry was added succesfully: store it in the cache.
-            cache[name] = desc;
-        }
-        catch (std::string msg) {
-            const char* base = "\nError adding the label '{}' = '{}'\n{}\n";
-
-            throw std::runtime_error(util::pprintf(base, name, desc, msg));
-        }
-        // Exceptions are thrown in parse or eval if an unexpected error occured.
-        catch (std::exception& e) {
-            const char* msg =
-                "\n----- internal error -------------------------------------------"
-                "\nError parsing the label: '{}' = '{}'"
-                "\n"
-                "\n{}"
-                "\n"
-                "\nPlease file a bug report with this full error message at:"
-                "\n    github.com/arbor-sim/arbor/issues"
-                "\n----------------------------------------------------------------";
-            throw arb::arbor_internal_error(util::pprintf(msg, name, desc, e.what()));
-        }
-    }
-
-    std::string to_string() const {
-        std::string s;
-        s += "(label_dict";
-        for (auto& x: dict.regions()) {
-            s += util::pprintf(" (region  \"{}\" {})", x.first, x.second);
-        }
-        for (auto& x: dict.locsets()) {
-            s += util::pprintf(" (locset \"{}\" {})", x.first, x.second);
-        }
-        s += ")";
-        return s;
-    }
-};
 
 // This isn't pretty. Partly because the information in the global parameters
 // is all over the place.
diff --git a/python/morphology.cpp b/python/morphology.cpp
index 06100d9f41884b1187d07dfa162eac2330d381fb..aa0425b4ba18d46170c3c6118cee63867968b481 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -10,10 +10,16 @@
 #include <arbor/morph/place_pwlin.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/segment_tree.hpp>
+#include <arbor/version.hpp>
 
 #include <arborio/swcio.hpp>
 
+#ifdef ARB_NEUROML_ENABLED
+#include <arborio/arbornml.hpp>
+#endif
+
 #include "error.hpp"
+#include "proxy.hpp"
 #include "strprintf.hpp"
 
 namespace py = pybind11;
@@ -363,6 +369,91 @@ void register_morphology(py::module& m) {
                 [](const arb::morphology& m) {
                     return util::pprintf("<arbor.morphology:\n{}>", m);
                 });
+
+#ifdef ARB_NEUROML_ENABLED
+    // arborio::morphology_data
+    py::class_<arborio::morphology_data> nml_morph_data(m, "neuroml_morph_data");
+    nml_morph_data
+        .def_readonly("cell_id",
+            &arborio::morphology_data::cell_id,
+            "Cell id, or empty if morphology was taken from a top-level <morphology> element.")
+        .def_readonly("id",
+            &arborio::morphology_data::id,
+            "Morphology id.")
+        .def_readonly("morphology",
+            &arborio::morphology_data::morphology,
+            "Morphology constructed from a signle NeuroML <morphology> element.")
+        .def("segments",
+            [](const arborio::morphology_data& md) {return label_dict_proxy(md.segments);},
+            "Label dictionary containing one region expression for each segment id.")
+        .def("named_segments",
+             [](const arborio::morphology_data& md) {return label_dict_proxy(md.named_segments);},
+            "Label dictionary containing one region expression for each name applied to one or more segments.")
+        .def("groups",
+             [](const arborio::morphology_data& md) {return label_dict_proxy(md.groups);},
+            "Label dictionary containing one region expression for each segmentGroup id.")
+        .def_readonly("group_segments",
+            &arborio::morphology_data::group_segments,
+            "Map from segmentGroup ids to their corresponding segment ids.");
+
+    // arborio::neuroml
+    py::class_<arborio::neuroml> neuroml(m, "neuroml");
+    neuroml
+        // constructors
+        .def(py::init(
+            [](std::string fname){
+              std::ifstream fid{fname};
+              if (!fid.good()) {
+                  throw pyarb_error(util::pprintf("can't open file '{}'", fname));
+              }
+              try {
+                  std::string string_data((std::istreambuf_iterator<char>(fid)),
+                                           std::istreambuf_iterator<char>());
+                  return arborio::neuroml(string_data);
+              }
+              catch (arborio::neuroml_exception& e) {
+                  // Try to produce helpful error messages for SWC parsing errors.
+                  throw pyarb_error(
+                      util::pprintf("NeuroML error processing file {}: ", fname, e.what()));
+              }
+            }))
+        .def("cell_ids",
+             [](const arborio::neuroml& nml) {
+                try {
+                    return nml.cell_ids();
+                }
+                catch (arborio::neuroml_exception& e) {
+                    throw util::pprintf("NeuroML error: {}", e.what());
+                }
+             },"Query top-level cells.")
+        .def("morphology_ids",
+             [](const arborio::neuroml& nml) {
+                try {
+                    return nml.morphology_ids();
+                }
+                catch (arborio::neuroml_exception& e) {
+                    throw util::pprintf("NeuroML error: {}", e.what());
+                }
+             },"Query top-level standalone morphologies.")
+        .def("morphology",
+             [](const arborio::neuroml& nml, const std::string& morph_id) {
+                try {
+                    return nml.morphology(morph_id);
+                }
+                catch (arborio::neuroml_exception& e) {
+                    throw util::pprintf("NeuroML error: {}", e.what());
+                }
+             },"morph_id"_a,"Retrieve top-level nml_morph_data associated with morph_id.")
+        .def("cell_morphology",
+             [](const arborio::neuroml& nml, const std::string& cell_id) {
+               try {
+                   return nml.cell_morphology(cell_id);
+               }
+               catch (arborio::neuroml_exception& e) {
+                   throw util::pprintf("NeuroML error: {}", e.what());
+               }
+             },"morph_id"_a,"Retrieve nml_morph_data associated with cell_id.");
+#endif
 }
 
 } // namespace pyarb
diff --git a/python/proxy.hpp b/python/proxy.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d47ab761b745d7947bd80ccd65d1f8d00ff5582d
--- /dev/null
+++ b/python/proxy.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include <any>
+
+#include <arbor/morph/label_dict.hpp>
+#include <arbor/morph/label_parse.hpp>
+
+#include "strprintf.hpp"
+
+namespace pyarb {
+struct label_dict_proxy {
+    using str_map = std::unordered_map<std::string, std::string>;
+    arb::label_dict dict;
+    str_map cache;
+    std::vector<std::string> locsets;
+    std::vector<std::string> regions;
+
+    label_dict_proxy() = default;
+
+    label_dict_proxy(const str_map& in) {
+        for (auto& i: in) {
+            set(i.first.c_str(), i.second.c_str());
+        }
+    }
+
+    label_dict_proxy(const arb::label_dict& label_dict): dict(label_dict) {}
+
+    std::size_t size() const  {
+        return locsets.size() + regions.size();
+    }
+
+    void set(const char* name, const char* desc) {
+        using namespace std::string_literals;
+        // The following code takes an input name and a region or locset
+        // description, e.g.:
+        //      name='reg', desc='(tag 4)'
+        //      name='loc', desc='(terminal)'
+        //      name='foo', desc='(join (tag 2) (tag 3))'
+        // Then it parses the description, and tests whether the description
+        // is a region or locset, and updates the label dictionary appropriately.
+        // Errors occur when:
+        //  * a region is described with a name that matches an existing locset
+        //    (and vice versa.)
+        //  * the description is not well formed, e.g. it contains a syntax error.
+        //  * the description is well-formed, but describes neither a region or locset.
+        try{
+            // Evaluate the s-expression to build a region/locset.
+            auto result = arb::parse_label_expression(desc);
+            if (!result) { // an error parsing / evaluating description.
+                throw result.error();
+            }
+            else if (result->type()==typeid(arb::region)) { // describes a region.
+                dict.set(name, std::move(std::any_cast<arb::region&>(*result)));
+                auto it = std::lower_bound(regions.begin(), regions.end(), name);
+                if (it==regions.end() || *it!=name) regions.insert(it, name);
+            }
+            else if (result->type()==typeid(arb::locset)) { // describes a locset.
+                dict.set(name, std::move(std::any_cast<arb::locset&>(*result)));
+                auto it = std::lower_bound(locsets.begin(), locsets.end(), name);
+                if (it==locsets.end() || *it!=name) locsets.insert(it, name);
+            }
+            else {
+                // Successfully parsed an expression that is neither region nor locset.
+                throw util::pprintf("The defninition of '{} = {}' does not define a valid region or locset.", name, desc);
+            }
+            // The entry was added succesfully: store it in the cache.
+            cache[name] = desc;
+        }
+        catch (std::string msg) {
+            const char* base = "\nError adding the label '{}' = '{}'\n{}\n";
+
+            throw std::runtime_error(util::pprintf(base, name, desc, msg));
+        }
+            // Exceptions are thrown in parse or eval if an unexpected error occured.
+        catch (std::exception& e) {
+            const char* msg =
+                "\n----- internal error -------------------------------------------"
+                "\nError parsing the label: '{}' = '{}'"
+                "\n"
+                "\n{}"
+                "\n"
+                "\nPlease file a bug report with this full error message at:"
+                "\n    github.com/arbor-sim/arbor/issues"
+                "\n----------------------------------------------------------------";
+            throw arb::arbor_internal_error(util::pprintf(msg, name, desc, e.what()));
+        }
+    }
+
+    std::string to_string() const {
+        std::string s;
+        s += "(label_dict";
+        for (auto& x: dict.regions()) {
+            s += util::pprintf(" (region  \"{}\" {})", x.first, x.second);
+        }
+        for (auto& x: dict.locsets()) {
+            s += util::pprintf(" (locset \"{}\" {})", x.first, x.second);
+        }
+        s += ")";
+        return s;
+    }
+};
+}
diff --git a/setup.py b/setup.py
index 8d62906ade5d3d80080539569da8deca9a239eb3..9bb49a0e13633d695252ee8d5dcea61c75b68a78 100644
--- a/setup.py
+++ b/setup.py
@@ -19,6 +19,7 @@ class CL_opt:
                                'gpu': 'none',
                                'vec': False,
                                'arch': 'native',
+                               'neuroml': False,
                                'bundled': True}
 
     def settings(self):
@@ -54,6 +55,7 @@ class install_command(install):
                         'none, cuda, cuda-clang, hip'),
         ('vec',   None, 'enable vectorization'),
         ('arch=', None, 'cpu architecture, e.g. haswell, skylake, armv8.2-a+sve, znver2 (default native).'),
+        ('neuroml', None, 'enable parsing neuroml morphologies in Arbor (requires libxml)')
         ('sysdeps', None, 'don\'t use bundled 3rd party C++ dependencies (pybind11 and json). This flag forces use of dependencies installed on the system.')
     ]
 
@@ -63,6 +65,7 @@ class install_command(install):
         self.gpu  = None
         self.arch = None
         self.vec  = None
+        self.neuroml = None
         self.sysdeps = None
 
     def finalize_options(self):
@@ -79,6 +82,8 @@ class install_command(install):
         opt['vec']  = self.vec is not None
         #   arch : target CPU micro-architecture (string).
         opt['arch'] = "native" if self.arch is None else self.arch
+        #   neuroml : compile with neuroml support for morphologies.
+        opt['neuroml'] = self.neuroml is not None
         #   bundled : use bundled/git-submoduled 3rd party libraries.
         #             By default use bundled libs.
         opt['bundled'] = self.sysdeps is None
@@ -112,6 +117,7 @@ class cmake_build(build_ext):
             '-DARB_VECTORIZE={}'.format('on' if opt['vec'] else 'off'),
             '-DARB_ARCH={}'.format(opt['arch']),
             '-DARB_GPU={}'.format(opt['gpu']),
+            '-DARB_WITH_NEUROML={}'.format( 'on' if opt['neuroml'] else 'off'),
             '-DARB_USE_BUNDLED_LIBS={}'.format('on' if opt['bundled'] else 'off'),
             '-DCMAKE_BUILD_TYPE=Release' # we compile with debug symbols in release mode.
         ]
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index b2474313344aa152b684188db00b5829878cf481..81de2e85a8c13e78144ee8f68feac185ce965f62 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -237,7 +237,3 @@ target_compile_definitions(unit PRIVATE "-DDATADIR=\"${CMAKE_CURRENT_SOURCE_DIR}
 target_compile_definitions(unit PRIVATE "-DLIBDIR=\"${PROJECT_BINARY_DIR}/lib\"")
 target_include_directories(unit PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
 target_link_libraries(unit PRIVATE gtest arbor arborenv arborio arbor-private-headers arbor-sup)
-
-if(ARB_WITH_NEUROML)
-    target_link_libraries(unit PRIVATE arbornml)
-endif()
diff --git a/test/unit/test_nml_morphology.cpp b/test/unit/test_nml_morphology.cpp
index 3c685a0801d8f19ddeaf73f614e15621efeba081..6860aa80543c9dbf8087f9e3491861969a260489 100644
--- a/test/unit/test_nml_morphology.cpp
+++ b/test/unit/test_nml_morphology.cpp
@@ -6,9 +6,8 @@
 #include <arbor/morph/place_pwlin.hpp>
 #include <arbor/morph/primitives.hpp>
 
-#include <arbornml/arbornml.hpp>
-#include <arbornml/nmlexcept.hpp>
-#include <arbornml/with_xml.hpp>
+#include <arborio/arbornml.hpp>
+#include <arborio/with_xml.hpp>
 
 #include "../test/gtest.h"
 #include "morph_pred.hpp"
@@ -18,10 +17,10 @@ using testing::region_eq;
 TEST(neuroml, with_xml) {
     // This (hopefully) will not blow up.
     {
-        arbnml::with_xml scope;
+        arborio::with_xml scope;
     }
     {
-        arbnml::with_xml scope;
+        arborio::with_xml scope;
     }
 }
 
@@ -30,7 +29,7 @@ TEST(neuroml, with_xml) {
 TEST(neuroml, morph_badxml) {
     std::string illformed = "<wha?";
 
-    EXPECT_THROW(arbnml::neuroml{illformed}, arbnml::xml_error);
+    EXPECT_THROW(arborio::neuroml{illformed}, arborio::xml_error);
 }
 
 TEST(neuroml, morph_none) {
@@ -38,13 +37,13 @@ TEST(neuroml, morph_none) {
     {
         std::string empty1 = R"~(<?xml version="1.0" encoding="UTF-8"?><foo/>)~";
 
-        arbnml::neuroml N1(empty1);
+        arborio::neuroml N1(empty1);
         EXPECT_TRUE(N1.cell_ids().empty());
         EXPECT_TRUE(N1.morphology_ids().empty());
 
         std::string empty2 = "<foo/>";
 
-        arbnml::neuroml N2(empty2);
+        arborio::neuroml N2(empty2);
         EXPECT_TRUE(N2.cell_ids().empty());
         EXPECT_TRUE(N2.morphology_ids().empty());
     }
@@ -56,7 +55,7 @@ R"~(<?xml version="1.0" encoding="UTF-8"?>
 <neuroml xmlns="http://www.neuroml.org/schema/neuroml2">
 </neuroml>)~";
 
-        arbnml::neuroml N3(empty3);
+        arborio::neuroml N3(empty3);
         EXPECT_TRUE(N3.cell_ids().empty());
         EXPECT_TRUE(N3.morphology_ids().empty());
     }
@@ -80,7 +79,7 @@ R"~(
 
     using svector = std::vector<std::string>;
 
-    arbnml::neuroml N(doc);
+    arborio::neuroml N(doc);
 
     svector m_ids = N.morphology_ids(); // only top-level!
     std::sort(m_ids.begin(), m_ids.end());
@@ -90,7 +89,7 @@ R"~(
     std::sort(c_ids.begin(), c_ids.end());
     EXPECT_EQ((svector{"c3", "c4"}), c_ids);
 
-    arbnml::morphology_data mdata;
+    arborio::morphology_data mdata;
 
     mdata = N.cell_morphology("c4").value();
     EXPECT_EQ("c4", mdata.cell_id);
@@ -174,10 +173,10 @@ R"~(
 </neuroml>
 )~";
 
-    arbnml::neuroml N(doc);
+    arborio::neuroml N(doc);
 
     {
-        arbnml::morphology_data m1 = N.morphology("m1").value();
+        arborio::morphology_data m1 = N.morphology("m1").value();
         label_dict labels;
         labels.import(m1.segments, "seg:");
         mprovider P(m1.morphology, labels);
@@ -190,7 +189,7 @@ R"~(
     }
 
     {
-        arbnml::morphology_data m2 = N.morphology("m2").value();
+        arborio::morphology_data m2 = N.morphology("m2").value();
         label_dict labels;
         labels.import(m2.segments, "seg:");
         mprovider P(m2.morphology, labels);
@@ -218,7 +217,7 @@ R"~(
     }
 
     {
-        arbnml::morphology_data m3 = N.morphology("m3").value();
+        arborio::morphology_data m3 = N.morphology("m3").value();
         label_dict labels;
         labels.import(m3.segments, "seg:");
         mprovider P(m3.morphology, labels);
@@ -252,7 +251,7 @@ R"~(
     }
     {
         for (const char* m_name: {"m4", "m5"}) {
-            arbnml::morphology_data m4_or_5 = N.morphology(m_name).value();
+            arborio::morphology_data m4_or_5 = N.morphology(m_name).value();
             label_dict labels;
             labels.import(m4_or_5.segments, "seg:");
             mprovider P(m4_or_5.morphology, labels);
@@ -354,14 +353,14 @@ R"~(
 </neuroml>
 )~";
 
-    arbnml::neuroml N(doc);
+    arborio::neuroml N(doc);
 
-    EXPECT_THROW(N.morphology("no-proximal").value(), arbnml::bad_segment);
-    EXPECT_THROW(N.morphology("no-such-parent").value(), arbnml::bad_segment);
-    EXPECT_THROW(N.morphology("cyclic-dependency").value(), arbnml::cyclic_dependency);
-    EXPECT_THROW(N.morphology("duplicate-id").value(), arbnml::bad_segment);
-    EXPECT_THROW(N.morphology("bad-segment-id").value(), arbnml::bad_segment);
-    EXPECT_THROW(N.morphology("another-bad-segment-id").value(), arbnml::bad_segment);
+    EXPECT_THROW(N.morphology("no-proximal").value(), arborio::bad_segment);
+    EXPECT_THROW(N.morphology("no-such-parent").value(), arborio::bad_segment);
+    EXPECT_THROW(N.morphology("cyclic-dependency").value(), arborio::cyclic_dependency);
+    EXPECT_THROW(N.morphology("duplicate-id").value(), arborio::bad_segment);
+    EXPECT_THROW(N.morphology("bad-segment-id").value(), arborio::bad_segment);
+    EXPECT_THROW(N.morphology("another-bad-segment-id").value(), arborio::bad_segment);
 }
 
 TEST(neuroml, simple_groups) {
@@ -441,11 +440,11 @@ R"~(
 </neuroml>
 )~";
 
-    arbnml::neuroml N(doc);
+    arborio::neuroml N(doc);
     using reg::named;
 
     {
-        arbnml::morphology_data m1 = N.morphology("m1").value();
+        arborio::morphology_data m1 = N.morphology("m1").value();
         label_dict labels;
         labels.import(m1.segments);
         labels.import(m1.groups);
@@ -456,7 +455,7 @@ R"~(
         EXPECT_TRUE(region_eq(P, named("group-c"), join(named("2"), named("1"))));
     }
     {
-        arbnml::morphology_data m2 = N.morphology("m2").value();
+        arborio::morphology_data m2 = N.morphology("m2").value();
         label_dict labels;
         labels.import(m2.segments);
         labels.import(m2.groups);
@@ -508,11 +507,11 @@ R"~(
 </neuroml>
 )~";
 
-    arbnml::neuroml N(doc);
+    arborio::neuroml N(doc);
 
-    EXPECT_THROW(N.morphology("no-such-segment").value(), arbnml::bad_segment_group);
-    EXPECT_THROW(N.morphology("no-such-group").value(), arbnml::bad_segment_group);
-    EXPECT_THROW(N.morphology("cyclic-dependency").value(), arbnml::cyclic_dependency);
+    EXPECT_THROW(N.morphology("no-such-segment").value(), arborio::bad_segment_group);
+    EXPECT_THROW(N.morphology("no-such-group").value(), arborio::bad_segment_group);
+    EXPECT_THROW(N.morphology("cyclic-dependency").value(), arborio::cyclic_dependency);
 }
 
 
@@ -612,9 +611,9 @@ R"~(
 </neuroml>
 )~";
 
-    arbnml::neuroml N(doc);
+    arborio::neuroml N(doc);
 
-    arbnml::morphology_data m1 = N.morphology("m1").value();
+    arborio::morphology_data m1 = N.morphology("m1").value();
     label_dict labels;
     labels.import(m1.segments);
     labels.import(m1.groups);