diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt
index 60a0f5d48fb7ea425858504105e1cce5f9c3e244..485e2df91c1d0c00a351f9dbdb149c84ae8da9e3 100644
--- a/cmake/CMakeLists.txt
+++ b/cmake/CMakeLists.txt
@@ -9,6 +9,7 @@ set(SOVERSION 0)
 get_filename_component(LAMMPS_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src ABSOLUTE)
 get_filename_component(LAMMPS_LIB_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../lib ABSOLUTE)
 get_filename_component(LAMMPS_LIB_BINARY_DIR ${CMAKE_BINARY_DIR}/lib ABSOLUTE)
+get_filename_component(LAMMPS_DOC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../doc ABSOLUTE)
 
 
 # To avoid conflicts with the conventional Makefile build system, we build everything here
@@ -900,6 +901,80 @@ if(BUILD_EXE)
   endif()
 endif()
 
+###############################################################################
+# Build documentation
+###############################################################################
+option(BUILD_DOC "Build LAMMPS documentation" OFF)
+if(BUILD_DOC)
+  include(ProcessorCount)
+  ProcessorCount(NPROCS)
+  find_package(PythonInterp 3 REQUIRED)
+
+  set(VIRTUALENV ${PYTHON_EXECUTABLE} -m virtualenv)
+
+  file(GLOB DOC_SOURCES ${LAMMPS_DOC_DIR}/src/*.txt)
+  file(GLOB PDF_EXTRA_SOURCES ${LAMMPS_DOC_DIR}/src/lammps_commands*.txt ${LAMMPS_DOC_DIR}/src/lammps_support.txt ${LAMMPS_DOC_DIR}/src/lammps_tutorials.txt)
+  list(REMOVE_ITEM DOC_SOURCES ${PDF_EXTRA_SOURCES})
+
+  add_custom_command(
+    OUTPUT docenv
+    COMMAND ${VIRTUALENV} docenv
+  )
+
+  set(DOCENV_BINARY_DIR ${CMAKE_BINARY_DIR}/docenv/bin)
+
+  add_custom_command(
+    OUTPUT requirements.txt
+    DEPENDS docenv
+    COMMAND ${CMAKE_COMMAND} -E copy ${LAMMPS_DOC_DIR}/utils/requirements.txt requirements.txt
+    COMMAND ${DOCENV_BINARY_DIR}/pip install -r requirements.txt --upgrade
+    COMMAND ${DOCENV_BINARY_DIR}/pip install --upgrade ${LAMMPS_DOC_DIR}/utils/converters
+  )
+
+  set(RST_FILES "")
+  set(RST_DIR ${CMAKE_BINARY_DIR}/rst)
+  file(MAKE_DIRECTORY ${RST_DIR})
+  foreach(TXT_FILE ${DOC_SOURCES})
+    get_filename_component(FILENAME ${TXT_FILE} NAME_WE)
+    set(RST_FILE ${RST_DIR}/${FILENAME}.rst)
+    list(APPEND RST_FILES ${RST_FILE})
+    add_custom_command(
+      OUTPUT ${RST_FILE}
+      DEPENDS requirements.txt docenv ${TXT_FILE}
+      COMMAND ${DOCENV_BINARY_DIR}/txt2rst -o ${RST_DIR} ${TXT_FILE}
+    )
+  endforeach()
+
+  add_custom_command(
+    OUTPUT html
+    DEPENDS ${RST_FILES}
+    COMMAND ${CMAKE_COMMAND} -E copy_directory ${LAMMPS_DOC_DIR}/src ${RST_DIR}
+    COMMAND ${DOCENV_BINARY_DIR}/sphinx-build -j ${NPROCS} -b html -c ${LAMMPS_DOC_DIR}/utils/sphinx-config -d ${CMAKE_BINARY_DIR}/doctrees ${RST_DIR} html
+  )
+
+  add_custom_target(
+    doc ALL
+    DEPENDS html
+    SOURCES ${LAMMPS_DOC_DIR}/utils/requirements.txt ${DOC_SOURCES}
+  )
+
+  install(DIRECTORY ${CMAKE_BINARY_DIR}/html DESTINATION ${CMAKE_INSTALL_DOCDIR})
+endif()
+
+###############################################################################
+# Install potential files in data directory
+###############################################################################
+set(LAMMPS_POTENTIALS_DIR ${CMAKE_INSTALL_FULL_DATADIR}/lammps/potentials)
+install(DIRECTORY ${LAMMPS_SOURCE_DIR}/../potentials DESTINATION ${CMAKE_INSTALL_DATADIR}/lammps/potentials)
+
+configure_file(etc/profile.d/lammps.sh.in ${CMAKE_BINARY_DIR}/etc/profile.d/lammps.sh @ONLY)
+configure_file(etc/profile.d/lammps.csh.in ${CMAKE_BINARY_DIR}/etc/profile.d/lammps.csh @ONLY)
+install(
+  FILES ${CMAKE_BINARY_DIR}/etc/profile.d/lammps.sh
+        ${CMAKE_BINARY_DIR}/etc/profile.d/lammps.csh
+  DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/profile.d
+)
+
 ###############################################################################
 # Testing
 #
diff --git a/cmake/README.md b/cmake/README.md
index bafd440a64b1a30e376d704d788ddacf446aeaaa..b6644ffda959c45faec16e89e0a6a3275725c796 100644
--- a/cmake/README.md
+++ b/cmake/README.md
@@ -275,6 +275,16 @@ cmake -C ../cmake/presets/std_nolib.cmake ../cmake -DPKG_GPU=on
   </dl>
   </td>
 </tr>
+<tr>
+  <td><code>BUILD_DOC</code></td>
+  <td>control whether to build LAMMPS documentation</td>
+  <td>
+  <dl>
+    <dt><code>off</code> (default)</dt>
+    <dt><code>on</code></dt>
+  </dl>
+  </td>
+</tr>
 <tr>
   <td><code>LAMMPS_LONGLONG_TO_LONG</code></td>
   <td>Workaround if your system or MPI version does not recognize <code>long long</code> data types</td>
diff --git a/cmake/etc/profile.d/lammps.csh.in b/cmake/etc/profile.d/lammps.csh.in
new file mode 100644
index 0000000000000000000000000000000000000000..def49bf75c0112503f7286dbb68843a258e993f3
--- /dev/null
+++ b/cmake/etc/profile.d/lammps.csh.in
@@ -0,0 +1,2 @@
+# set environment for LAMMPS executables to find potential files
+if ( "$?LAMMPS_POTENTIALS" == 0 ) setenv LAMMPS_POTENTIALS @LAMMPS_POTENTIALS_DIR@
diff --git a/cmake/etc/profile.d/lammps.sh.in b/cmake/etc/profile.d/lammps.sh.in
new file mode 100644
index 0000000000000000000000000000000000000000..acd75fa0cff7bae7013d8c32d8453e2083dc217d
--- /dev/null
+++ b/cmake/etc/profile.d/lammps.sh.in
@@ -0,0 +1,2 @@
+# set environment for LAMMPS executables to find potential files
+export LAMMPS_POTENTIALS=${LAMMPS_POTENTIALS-@LAMMPS_POTENTIALS_DIR@}
diff --git a/doc/Makefile b/doc/Makefile
index c4bc80e7bd5de665a857a5488842cd8be63e51cf..81f362349950be2123d2da4bd14255e7f9f27739 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -157,7 +157,7 @@ $(RSTDIR)/%.rst : src/%.txt $(TXT2RST)
 	@(\
 		mkdir -p $(RSTDIR) ; \
 		. $(VENV)/bin/activate ;\
-		txt2rst $< > $@ ;\
+		txt2rst -v $< > $@ ;\
 		deactivate ;\
 	)
 
diff --git a/doc/utils/converters/lammpsdoc/txt2html.py b/doc/utils/converters/lammpsdoc/txt2html.py
index 79a75d72f6e4be7796e685e7025adbef0d9eb5ff..ed9f47a4e4ae3cfe4c6363a34ab9b7deb42b8c0c 100755
--- a/doc/utils/converters/lammpsdoc/txt2html.py
+++ b/doc/utils/converters/lammpsdoc/txt2html.py
@@ -662,14 +662,15 @@ class TxtConverter:
         parser = self.get_argument_parser()
         parsed_args = parser.parse_args(args)
 
-        write_to_files = len(parsed_args.files) > 1
+        write_to_files = parsed_args.output_dir or (len(parsed_args.files) > 1)
 
         for filename in parsed_args.files:
             if parsed_args.skip_files and filename in parsed_args.skip_files:
                 continue
 
             with open(filename, 'r') as f:
-                print("Converting", filename, "...", file=err)
+                if parsed_args.verbose:
+                    print("Converting", filename, "...", file=err)
                 content = f.read()
                 converter = self.create_converter(parsed_args)
 
@@ -683,7 +684,10 @@ class TxtConverter:
                     result = msg
 
                 if write_to_files:
-                    output_filename = self.get_output_filename(filename)
+                    if parsed_args.output_dir:
+                        output_filename = os.path.join(parsed_args.output_dir, os.path.basename(self.get_output_filename(filename)))
+                    else:
+                        output_filename = self.get_output_filename(filename)
                     with open(output_filename, "w+t") as outfile:
                         outfile.write(result)
                 else:
@@ -698,6 +702,8 @@ class Txt2HtmlConverter(TxtConverter):
                                                                               'HTML file. useful when set of HTML files'
                                                                               ' will be converted to PDF')
         parser.add_argument('-x', metavar='file-to-skip', dest='skip_files', action='append')
+        parser.add_argument('--verbose', '-v', dest='verbose', action='store_true')
+        parser.add_argument('--output-directory', '-o', dest='output_dir')
         parser.add_argument('--generate-title', dest='create_title', action='store_true', help='add HTML head page'
                                                                                                'title based on first '
                                                                                                'h1,h2,h3,h4... element')
diff --git a/doc/utils/converters/lammpsdoc/txt2rst.py b/doc/utils/converters/lammpsdoc/txt2rst.py
index 17d0916157a4f2d9b07ec655522ac6999b6978cb..8119ad3a7869c5345ffad166536f98ffa7bd20ba 100755
--- a/doc/utils/converters/lammpsdoc/txt2rst.py
+++ b/doc/utils/converters/lammpsdoc/txt2rst.py
@@ -395,6 +395,8 @@ class Txt2RstConverter(TxtConverter):
         parser = argparse.ArgumentParser(description='converts a text file with simple formatting & markup into '
                                                      'Restructured Text for Sphinx.')
         parser.add_argument('-x', metavar='file-to-skip', dest='skip_files', action='append')
+        parser.add_argument('--verbose', '-v', dest='verbose', action='store_true')
+        parser.add_argument('--output-directory', '-o', dest='output_dir')
         parser.add_argument('files',  metavar='file', nargs='+', help='one or more files to convert')
         return parser
 
diff --git a/doc/utils/requirements.txt b/doc/utils/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2806c164989aa9137a6c1860716ae9abccef75ad
--- /dev/null
+++ b/doc/utils/requirements.txt
@@ -0,0 +1 @@
+Sphinx