diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d53f8f3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,48 @@ +# MPark.WG21 +# +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt) +# +# This CMake framework for mpark/wg21 provides the function wg21_add_paper +# (see cmake/MparkWg21Helpers.cmake). +# +# This top-level file either configures installation of mpark/wg21 for +# consumption using find_package(MparkWg21) or configures the project to be +# consumed via add_subdirectory(). + +cmake_minimum_required(VERSION 3.21) + +project(MparkWg21 + VERSION 0.1.0 + DESCRIPTION "Pandoc-based framework for authoring and reviewing WG21 papers" + HOMEPAGE_URL "https://github.com/mpark/wg21" + LANGUAGES NONE) + +if(PROJECT_IS_TOP_LEVEL) + include(GNUInstallDirs) + + install(DIRECTORY data/ + DESTINATION "${CMAKE_INSTALL_DATADIR}/mpark-wg21/data" + USE_SOURCE_PERMISSIONS) # pandoc runs the filters via their shebang; + # keep them executable + install(FILES + cmake/FindMparkWg21.cmake + cmake/MparkWg21Helpers.cmake + cmake/FindPandoc.cmake + cmake/render.cmake + DESTINATION "${CMAKE_INSTALL_DATADIR}/mpark-wg21/cmake") + + include(CMakePackageConfigHelpers) + write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/MparkWg21ConfigVersion.cmake" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT) + install(FILES + cmake/MparkWg21Config.cmake + "${CMAKE_CURRENT_BINARY_DIR}/MparkWg21ConfigVersion.cmake" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/MparkWg21") +else() + set(MparkWg21_DATA_SRC "${CMAKE_CURRENT_SOURCE_DIR}/data") + set(MparkWg21_RENDER_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/render.cmake") + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/MparkWg21Helpers.cmake") +endif() diff --git a/cmake/FindMparkWg21.cmake b/cmake/FindMparkWg21.cmake new file mode 100644 index 0000000..1002172 --- /dev/null +++ b/cmake/FindMparkWg21.cmake @@ -0,0 +1,47 @@ +# FindMparkWg21 -- locate the framework's data/ tree and render.cmake, then +# include MparkWg21Helpers.cmake to define the build API (wg21_add_paper() and +# the wg21_data target). Sets MparkWg21_FOUND / MparkWg21_DATA_SRC / +# MparkWg21_RENDER_SCRIPT. Self-locates relative to this file in both the +# source-checkout and install layouts; MparkWg21_ROOT overrides. + +find_path(MparkWg21_DATA_SRC + NAMES metadata.yaml + HINTS "${CMAKE_CURRENT_LIST_DIR}/../data" + ${MparkWg21_ROOT} ENV MparkWg21_ROOT + PATH_SUFFIXES data share/mpark-wg21/data + NO_DEFAULT_PATH + DOC "MPark/WG21 source data directory") + +find_file(MparkWg21_RENDER_SCRIPT + NAMES render.cmake + HINTS "${CMAKE_CURRENT_LIST_DIR}" + ${MparkWg21_ROOT} ENV MparkWg21_ROOT + PATH_SUFFIXES cmake share/mpark-wg21/cmake + NO_DEFAULT_PATH + DOC "MPark/WG21 render.cmake") + +# The helpers find the tools (Pandoc, Python3 + modules) and, on success, +# define the build API. Tool failures land in MparkWg21_TOOLS_MISSING and are +# reported below through FPHSA, which honors REQUIRED/QUIET. +set(MparkWg21_TOOLS_MISSING "") +if(MparkWg21_DATA_SRC AND MparkWg21_RENDER_SCRIPT) + set(_MparkWg21_from_find_module TRUE) + include("${CMAKE_CURRENT_LIST_DIR}/MparkWg21Helpers.cmake") + unset(_MparkWg21_from_find_module) +endif() + +if(MparkWg21_TOOLS_MISSING) + set(MparkWg21_TOOLS "MparkWg21_TOOLS-NOTFOUND") + # Join without semicolons: the reason passes through FPHSA's arg parsing. + list(JOIN MparkWg21_TOOLS_MISSING " and " _MparkWg21_reason) +else() + set(MparkWg21_TOOLS TRUE) + set(_MparkWg21_reason "") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(MparkWg21 + REQUIRED_VARS MparkWg21_DATA_SRC MparkWg21_RENDER_SCRIPT MparkWg21_TOOLS + REASON_FAILURE_MESSAGE "${_MparkWg21_reason}") + +mark_as_advanced(MparkWg21_DATA_SRC MparkWg21_RENDER_SCRIPT) diff --git a/cmake/FindPandoc.cmake b/cmake/FindPandoc.cmake new file mode 100644 index 0000000..80816b2 --- /dev/null +++ b/cmake/FindPandoc.cmake @@ -0,0 +1,38 @@ +# FindPandoc -- locate the pandoc executable (it ships no CMake config) and +# probe its version. Sets Pandoc_FOUND / Pandoc_EXECUTABLE / Pandoc_VERSION and +# a GLOBAL imported target Pandoc::pandoc. Hint: Pandoc_ROOT (or a system +# pandoc on PATH). + +find_program(Pandoc_EXECUTABLE + NAMES pandoc + HINTS ${Pandoc_ROOT} ENV Pandoc_ROOT + PATH_SUFFIXES bin + DOC "Path to the pandoc executable") + +if(Pandoc_EXECUTABLE) + execute_process( + COMMAND "${Pandoc_EXECUTABLE}" --version + OUTPUT_VARIABLE _pandoc_version_output + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _pandoc_version_rc) + if(_pandoc_version_rc EQUAL 0 AND + _pandoc_version_output MATCHES "pandoc[^0-9]*([0-9]+(\\.[0-9]+)+)") + set(Pandoc_VERSION "${CMAKE_MATCH_1}") + endif() + unset(_pandoc_version_output) + unset(_pandoc_version_rc) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Pandoc + REQUIRED_VARS Pandoc_EXECUTABLE + VERSION_VAR Pandoc_VERSION) + +if(Pandoc_FOUND AND NOT TARGET Pandoc::pandoc) + add_executable(Pandoc::pandoc IMPORTED GLOBAL) + set_target_properties(Pandoc::pandoc PROPERTIES + IMPORTED_LOCATION "${Pandoc_EXECUTABLE}") +endif() + +mark_as_advanced(Pandoc_EXECUTABLE) diff --git a/cmake/MparkWg21Config.cmake b/cmake/MparkWg21Config.cmake new file mode 100644 index 0000000..91b2559 --- /dev/null +++ b/cmake/MparkWg21Config.cmake @@ -0,0 +1,23 @@ +# MPark.WG21 +# +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt) +# +# Config-mode entry point, so `cmake --install` + CMAKE_PREFIX_PATH works with +# no CMAKE_MODULE_PATH setup. Delegates to the find module, which does the +# real discovery and defines the build API. Works from both the install +# layout (share/cmake/MparkWg21/, with the module under +# share/mpark-wg21/cmake/) and a source checkout (side by side in cmake/). + +foreach(_mparkwg21_dir + "${CMAKE_CURRENT_LIST_DIR}" + "${CMAKE_CURRENT_LIST_DIR}/../../mpark-wg21/cmake") + if(EXISTS "${_mparkwg21_dir}/FindMparkWg21.cmake") + include("${_mparkwg21_dir}/FindMparkWg21.cmake") + break() + endif() +endforeach() + +if(NOT MparkWg21_FOUND) + set(MparkWg21_NOT_FOUND_MESSAGE "${_MparkWg21_reason}") +endif() diff --git a/cmake/MparkWg21Helpers.cmake b/cmake/MparkWg21Helpers.cmake new file mode 100644 index 0000000..f460bb0 --- /dev/null +++ b/cmake/MparkWg21Helpers.cmake @@ -0,0 +1,180 @@ +# MPark.WG21 +# +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt) +# +# Defines wg21_add_paper() + the shared staging/generation targets. Included by +# FindMparkWg21.cmake. Set before including: +# MparkWg21_DATA_SRC - the framework's source data/ directory +# MparkWg21_RENDER_SCRIPT - path to render.cmake + +# find_package() re-runs its module on each call; define everything once. +if(COMMAND wg21_add_paper) + return() +endif() + +# FindPandoc.cmake is shipped alongside this file. +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") + +# Transitive tools (raw find_package; the caller satisfies them). The Python +# modules are asserted, not provisioned -- that's the caller's job. Failures +# are collected in MparkWg21_TOOLS_MISSING: when included from FindMparkWg21 +# (which sets _MparkWg21_from_find_module) they become a standard not-found +# result honoring REQUIRED/QUIET; a direct add_subdirectory fails here. +if(MparkWg21_FIND_QUIETLY) + set(_mparkwg21_quiet QUIET) +else() + set(_mparkwg21_quiet) +endif() + +set(MparkWg21_TOOLS_MISSING "") +set(_mparkwg21_py_err "") + +find_package(Python3 3.10 ${_mparkwg21_quiet} COMPONENTS Interpreter) +if(Python3_FOUND) + set(_mparkwg21_modules panflute bs4 lxml requests yaml) + list(JOIN _mparkwg21_modules ", " _mparkwg21_imports) + execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import ${_mparkwg21_imports}" + RESULT_VARIABLE _mparkwg21_py_rc + ERROR_VARIABLE _mparkwg21_py_err) + if(NOT _mparkwg21_py_rc EQUAL 0) + # No semicolons here: the string passes through FPHSA's argument parsing. + list(APPEND MparkWg21_TOOLS_MISSING + "Python modules for ${Python3_EXECUTABLE} (need ${_mparkwg21_imports} -- \ +try `pip install panflute beautifulsoup4 lxml requests pyyaml`)") + endif() +else() + list(APPEND MparkWg21_TOOLS_MISSING "Python3 >= 3.10") +endif() + +find_package(Pandoc 3.9.0.2 ${_mparkwg21_quiet}) +if(NOT Pandoc_FOUND) + list(APPEND MparkWg21_TOOLS_MISSING "Pandoc >= 3.9.0.2 (hint: Pandoc_ROOT)") +endif() + +if(MparkWg21_TOOLS_MISSING) + if(_MparkWg21_from_find_module) + return() + endif() + list(JOIN MparkWg21_TOOLS_MISSING "\n " _mparkwg21_missing) + message(FATAL_ERROR + "MparkWg21: missing tools:\n ${_mparkwg21_missing}\n${_mparkwg21_py_err}") +endif() + +set(MparkWg21_RENDER_SCRIPT "${MparkWg21_RENDER_SCRIPT}" CACHE INTERNAL "") +# Unlike find_program results, FindPython3's Python3_EXECUTABLE is scope-local; +# snapshot it so wg21_add_paper sees it from any call site. +set(MparkWg21_PYTHON "${Python3_EXECUTABLE}" CACHE INTERNAL "") + +# Staged data dir in the build tree: pandoc's --data-dir must hold the +# committed data/ files plus the generated reference files. Staging there keeps +# the source/install read-only; one shared location means one stage + one fetch +# per build. +set(MparkWg21_STAGED_DATA_DIR "${CMAKE_BINARY_DIR}/mpark-wg21/data" + CACHE INTERNAL "wg21 staged data dir") +set(_sentinel "${MparkWg21_STAGED_DATA_DIR}/metadata.yaml") +set(_csl "${MparkWg21_STAGED_DATA_DIR}/csl.json") +set(_srefs "${MparkWg21_STAGED_DATA_DIR}/srefs.json") +set(_srefs_defs "${MparkWg21_STAGED_DATA_DIR}/srefs.defs") + +# The copied metadata.yaml is the sentinel for the bulk copy. Deliberately no +# DEPENDS: staging runs once, edits under data/ need a fresh build tree. +add_custom_command( + OUTPUT "${_sentinel}" + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${MparkWg21_DATA_SRC}" "${MparkWg21_STAGED_DATA_DIR}" + COMMENT "Staging wg21 data directory" + VERBATIM) + +# Generated reference data (network-backed; fetched once). Paths go in as +# positional args ($1..) to keep them out of the `bash -c` string. +add_custom_command( + OUTPUT "${_csl}" + COMMAND bash -c "\"$1\" \"$2\" > \"$3\"" + wg21 "${Python3_EXECUTABLE}" "${MparkWg21_DATA_SRC}/refs.py" "${_csl}" + DEPENDS "${MparkWg21_DATA_SRC}/refs.py" "${_sentinel}" + COMMENT "Fetching citation reference data (network)" + VERBATIM) + +add_custom_command( + OUTPUT "${_srefs}" + COMMAND bash -c "\"$1\" \"$2\" > \"$3\"" + wg21 "${Python3_EXECUTABLE}" "${MparkWg21_DATA_SRC}/srefs.py" "${_srefs}" + DEPENDS "${MparkWg21_DATA_SRC}/srefs.py" "${_sentinel}" + COMMENT "Fetching stable-name reference data (network)" + VERBATIM) + +add_custom_command( + OUTPUT "${_srefs_defs}" + COMMAND bash -c "\"$1\" \"$2\" < \"$3\" > \"$4\"" + wg21 "${Python3_EXECUTABLE}" "${MparkWg21_DATA_SRC}/srefs-md.py" + "${_srefs}" "${_srefs_defs}" + DEPENDS "${MparkWg21_DATA_SRC}/srefs-md.py" "${_srefs}" "${_sentinel}" + COMMENT "Generating stable-name markdown" + VERBATIM) + +add_custom_target(wg21_data DEPENDS "${_sentinel}" "${_csl}" "${_srefs}" "${_srefs_defs}") + +# wg21_add_paper( +# [SOURCE ] default: .md in the caller's dir +# [FORMATS ...] default: html +# [OUTDIR ] default: caller's CMAKE_CURRENT_BINARY_DIR +# [ALL]) attach to the default build target +function(wg21_add_paper name) + cmake_parse_arguments(PARSE_ARGV 1 ARG + "ALL" + "SOURCE;OUTDIR" + "FORMATS") + if(ARG_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "wg21_add_paper(${name}): unexpected arguments: " + "${ARG_UNPARSED_ARGUMENTS}") + endif() + + if(NOT ARG_SOURCE) + set(ARG_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${name}.md") + endif() + if(NOT ARG_FORMATS) + set(ARG_FORMATS html) + endif() + if(NOT ARG_OUTDIR) + set(ARG_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}") + endif() + + get_filename_component(_venv_bin "${MparkWg21_PYTHON}" DIRECTORY) + set(_data "${MparkWg21_STAGED_DATA_DIR}") + # Render from the source's directory so relative resources (e.g. images) + # resolve, as they do when running make from a paper directory. + get_filename_component(_src_dir "${ARG_SOURCE}" DIRECTORY) + + set(_outputs "") + foreach(fmt IN LISTS ARG_FORMATS) + set(_out "${ARG_OUTDIR}/${name}.${fmt}") + add_custom_command( + OUTPUT "${_out}" + COMMAND "${CMAKE_COMMAND}" + "-DPANDOC=${Pandoc_EXECUTABLE}" + "-DPYTHON=${MparkWg21_PYTHON}" + "-DVENV_BIN=${_venv_bin}" + "-DSRC=${ARG_SOURCE}" + "-DOUT=${_out}" + "-DFORMAT=${fmt}" + "-DDATA_DIR=${_data}" + -P "${MparkWg21_RENDER_SCRIPT}" + DEPENDS "${ARG_SOURCE}" + "${Pandoc_EXECUTABLE}" + "${_data}/metadata.yaml" + "${_data}/csl.json" "${_data}/srefs.json" "${_data}/srefs.defs" + WORKING_DIRECTORY "${_src_dir}" + COMMENT "Rendering ${name}.${fmt}" + VERBATIM) + list(APPEND _outputs "${_out}") + endforeach() + + if(ARG_ALL) + add_custom_target(${name} ALL DEPENDS ${_outputs}) + else() + add_custom_target(${name} DEPENDS ${_outputs}) + endif() + add_dependencies(${name} wg21_data) +endfunction() diff --git a/cmake/render.cmake b/cmake/render.cmake new file mode 100644 index 0000000..598b98a --- /dev/null +++ b/cmake/render.cmake @@ -0,0 +1,61 @@ +# MPark.WG21 +# +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt) +# +# Build-time Pandoc driver (cmake -P). Inputs via -D: +# PANDOC, SRC, OUT, VENV_BIN, FORMAT, DATA_DIR, PYTHON + +foreach(_required PANDOC SRC OUT FORMAT DATA_DIR) + if(NOT DEFINED ${_required}) + message(FATAL_ERROR "render.cmake: ${_required} is required") + endif() +endforeach() + +# venv first on PATH so pandoc's filters resolve python3 (with panflute) via +# their shebang. Unix-only for now. +get_filename_component(_pandoc_dir "${PANDOC}" DIRECTORY) +set(_prefix "${_pandoc_dir}") +if(DEFINED VENV_BIN AND NOT VENV_BIN STREQUAL "") + set(_prefix "${VENV_BIN}:${_prefix}") +endif() +set(ENV{PATH} "${_prefix}:$ENV{PATH}") + +# srefs.defs (stable-reference link defs) is concatenated before the source +# so definitions in the user's file take precedence, matching base.mk. +set(_args "") +if(EXISTS "${DATA_DIR}/srefs.defs") + list(APPEND _args "${DATA_DIR}/srefs.defs") +endif() +list(APPEND _args "${SRC}" + -o "${OUT}" + "--data-dir=${DATA_DIR}" + -M "data-dir=${DATA_DIR}" + -d doc -d formatting) + +# Bridge the source's front-matter toc-depth into --toc-depth (html only; +# pandoc ignores the front-matter value otherwise). +if(FORMAT STREQUAL "html") + if(NOT DEFINED PYTHON) + message(FATAL_ERROR "render.cmake: PYTHON is required for html output") + endif() + execute_process( + COMMAND "${PYTHON}" "${DATA_DIR}/toc-depth.py" + INPUT_FILE "${SRC}" + OUTPUT_VARIABLE _toc_depth + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _rc) + if(NOT _rc EQUAL 0) + message(FATAL_ERROR "render.cmake: toc-depth.py failed for ${SRC} (exit ${_rc})") + endif() + if(_toc_depth) + list(APPEND _args --toc-depth ${_toc_depth}) + endif() +endif() + +execute_process( + COMMAND "${PANDOC}" ${_args} + RESULT_VARIABLE _rc) +if(NOT _rc EQUAL 0) + message(FATAL_ERROR "render.cmake: pandoc failed for ${SRC} -> ${OUT} (exit ${_rc})") +endif()