doc: fix extract_content.py dependency tracking

The logic which copies source documentation files into the build
directory could use some improvements to its dependency management, so
that when a source file changes, extract_content.py gets re-run.

Make these changes as follows:

- Add an --outputs flag to extract_content.py, so that the
  sources it depends on and generates can be saved into a file and
  thus made known to the build system

- Change the way the sources and destination are specified in the
  extract_content.py command line so that the entire job can be done
  in a single command, rather than multiple (to avoid having to
  collate multiple --outputs files in CMake)

- Extract the content at configure time with execute_process(),
  tracking all inputs and outputs within the build system itself. Use
  this information to make sure that each individual output depends on
  just its exact input file, ensuring updated inputs produce updated
  outputs without having to call extract_content.py again.

- Ensure that the "content" build system target depends on all the
  outputs, transitively triggering a rebuild any time an input
  file (e.g. .rst documentation file or included image/source file)
  changes.

Signed-off-by: Marti Bolivar <marti@foundries.io>
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
index 03b3c67..1dc6847 100644
--- a/doc/CMakeLists.txt
+++ b/doc/CMakeLists.txt
@@ -68,9 +68,76 @@
 set(DOXY_LOG ${CMAKE_CURRENT_BINARY_DIR}/doxy.log)
 set(SPHINX_LOG ${CMAKE_CURRENT_BINARY_DIR}/sphinx.log)
 set(DOC_WARN ${CMAKE_CURRENT_BINARY_DIR}/doc.warnings)
+set(CONTENT_OUTPUTS ${CMAKE_CURRENT_BINARY_DIR}/extracted-content.txt)
 
 configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY)
 
+# This command is used to copy all documentation source files into the
+# build directory. Sphinx requires a single documentation root, but
+# Zephyr's documentation is scattered around the tree in samples/,
+# boards/, and doc/, so we need to copy those files into a single
+# place in the build directory.
+set(EXTRACT_CONTENT_COMMAND
+  ${CMAKE_COMMAND} -E env
+  ${PYTHON_EXECUTABLE} scripts/extract_content.py
+  # Save the outputs for processing by this list file.
+  --outputs ${CONTENT_OUTPUTS}
+  # Ignore any files in the output directory.
+  --ignore ${CMAKE_CURRENT_BINARY_DIR}
+  # Copy all files in doc to the rst folder.
+  "*:doc:${RST_OUT}"
+  # Copy the .rst files in samples/ and boards/ to the rst folder, and
+  # also the doc folder inside rst.
+  #
+  # Some files refer to items in samples/ and boards/ relative to
+  # their actual position in the Zephyr tree. For example, in
+  # subsystems/sensor.rst:
+  #
+  # .. literalinclude:: ../../samples/sensor/mcp9808/src/main.c
+  #
+  # We make an additional copy as a hackaround so these references
+  # work.
+  "*.rst:samples:${RST_OUT}" "*.rst:boards:${RST_OUT}"
+  "*.rst:samples:${RST_OUT}/doc" "*.rst:boards:${RST_OUT}/doc")
+
+# Run the content extraction command at configure time in order to
+# produce lists of input and output files, which are respectively its
+# dependencies and outputs. This also causes the content files
+# to be copied for the first time.
+execute_process(
+  COMMAND ${EXTRACT_CONTENT_COMMAND}
+  WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
+  RESULT_VARIABLE RET)
+
+if (NOT RET EQUAL 0)
+  message(FATAL_ERROR "Cannot prepare documentation content extraction")
+endif()
+
+# Load the outputs listing from extract_content.py. This is a file
+# where each pair of lines is a source file in the Zephyr tree and a
+# destination file in the build directory.
+file(STRINGS ${CONTENT_OUTPUTS} EXTRACT_CONTENT_RAW)
+
+# Build up a list of output files for later use in target/command
+# DEPENDS. Make sure each output file gets updated if the
+# corresponding input file changes.
+set(EXTRACT_CONTENT_OUTPUTS)
+while(EXTRACT_CONTENT_RAW)
+  list(GET EXTRACT_CONTENT_RAW 0 SRC)
+  list(GET EXTRACT_CONTENT_RAW 1 DST)
+
+  add_custom_command(
+    COMMAND ${CMAKE_COMMAND} -E copy ${SRC} ${DST}
+    DEPENDS ${SRC}
+    OUTPUT ${DST}
+    )
+
+  list(REMOVE_AT EXTRACT_CONTENT_RAW 0 1)
+  list(APPEND EXTRACT_CONTENT_OUTPUTS ${DST})
+endwhile()
+
+add_custom_target(content DEPENDS ${EXTRACT_CONTENT_OUTPUTS})
+
 set(ARGS ${DOXYFILE_OUT})
 
 add_custom_target(
@@ -89,20 +156,6 @@
   COMMAND ${CMAKE_COMMAND} -P ${ZEPHYR_BASE}/cmake/pristine.cmake
 )
 
-add_custom_target(
-  content
-  # Copy all files in doc/ to the rst folder
-  COMMAND ${CMAKE_COMMAND} -E env
-  ${PYTHON_EXECUTABLE} scripts/extract_content.py --ignore ${CMAKE_CURRENT_BINARY_DIR} -a ${RST_OUT} doc
-  # Copy the .rst files in samples/ and boards/ to the rst folder
-  COMMAND ${CMAKE_COMMAND} -E env
-  ${PYTHON_EXECUTABLE} scripts/extract_content.py --ignore ${CMAKE_CURRENT_BINARY_DIR} ${RST_OUT} samples boards
-  # Copy the .rst files in samples/ and boards/ to the doc folder inside rst
-  COMMAND ${CMAKE_COMMAND} -E env
-  ${PYTHON_EXECUTABLE} scripts/extract_content.py --ignore ${CMAKE_CURRENT_BINARY_DIR} ${RST_OUT}/doc samples boards
-  WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
-)
-
 if(WIN32)
   set(SEP ;)
 else()