Skip to content
Snippets Groups Projects
PANDOC.cmake 17.6 KiB
Newer Older
################################################################################
##
##  Provide Pandoc compilation support for the CMake build system
##
##  Version: 0.0.1
##  Author: Jeet Sukumatan (jeetsukumaran@gmail.com)
##
##  Copyright 2015 Jeet Sukumaran.
##
##  This software is released under the BSD 3-Clause License.
##
##  Redistribution and use in source and binary forms, with or without
##  modification, are permitted provided that the following conditions are
##  met:
##
##  1. Redistributions of source code must retain the above copyright notice,
##  this list of conditions and the following disclaimer.
##
##  2. Redistributions in binary form must reproduce the above copyright
##  notice, this list of conditions and the following disclaimer in the
##  documentation and/or other materials provided with the distribution.
##
##  3. Neither the name of the copyright holder nor the names of its
##  contributors may be used to endorse or promote products derived from this
##  software without specific prior written permission.
##
##  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
##  IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
##  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
##  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
##  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
##  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
##  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
##  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
##  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
##  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
##  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
##
################################################################################

include(CMakeParseArguments)

if(NOT EXISTS ${PANDOC_EXECUTABLE})
    # find_program(PANDOC_EXECUTABLE NAMES pandoc)
    find_program(PANDOC_EXECUTABLE pandoc)
    mark_as_advanced(PANDOC_EXECUTABLE)
    if(NOT EXISTS ${PANDOC_EXECUTABLE})
        unset(PANDOC_EXECUTABLE)
        return()
    endif()
endif()

###############################################################################
# Based on code from UseLATEX
# Author: Kenneth Moreland <kmorel@sandia.gov>
# Copyright 2004 Sandia Corporation.
# Under the terms of Contract DE-AC04-94AL85000, there is a non-exclusive
# license for use of this work by or on behalf of the
# U.S. Government. Redistribution and use in source and binary forms, with
# or without modification, are permitted provided that this Notice and any
# statement of authorship are reproduced on all copies.

# Adds command to copy file from the source directory to the destination
# directory: used to move source files from source directory into build
# directory before main build
function(pandocology_add_input_file source_path dest_dir dest_filelist_var)
    set(dest_filelist)
    file(GLOB globbed_source_paths "${source_path}")
    foreach(globbed_source_path ${globbed_source_paths})
        # MESSAGE(FATAL_ERROR "${globbed_source_path}")
        if(NOT IS_ABSOLUTE ${globbed_source_path})
            get_filename_component(filename ${globbed_source_path} NAME)
            get_filename_component(absolute_dest_path ${dest_dir}/${filename} ABSOLUTE)
            file(RELATIVE_PATH relative_dest_path ${CMAKE_CURRENT_BINARY_DIR} ${absolute_dest_path})
            list(APPEND dest_filelist ${absolute_dest_path})
        
            add_custom_command(
                OUTPUT ${relative_dest_path}
                COMMAND ${CMAKE_COMMAND} -E copy ${globbed_source_path} ${dest_dir}/${filename}
                DEPENDS ${globbed_source_path}
                )
        else()
            list(APPEND dest_filelist ${globbed_source_path})
        endif()
        set(${dest_filelist_var} ${${dest_filelist_var}} ${dest_filelist} PARENT_SCOPE)
    endforeach()
endfunction()

# A version of GET_FILENAME_COMPONENT that treats extensions after the last
# period rather than the first.
function(pandocology_get_file_stemname varname filename)
    SET(result)
    GET_FILENAME_COMPONENT(name ${filename} NAME)
    STRING(REGEX REPLACE "\\.[^.]*\$" "" result "${name}")
    SET(${varname} "${result}" PARENT_SCOPE)
endfunction()

function(pandocology_get_file_extension varname filename)
    SET(result)
    GET_FILENAME_COMPONENT(name ${filename} NAME)
    STRING(REGEX MATCH "\\.[^.]*\$" result "${name}")
    SET(${varname} "${result}" PARENT_SCOPE)
endfunction()
###############################################################################

function(pandocology_add_input_dir source_dir dest_parent_dir dir_dest_filelist_var)
    set(dir_dest_filelist)
    get_filename_component(dir_name ${source_dir} NAME)
    get_filename_component(absolute_dest_dir ${dest_parent_dir}/${dir_name} ABSOLUTE)
    file(RELATIVE_PATH relative_dest_dir ${CMAKE_CURRENT_BINARY_DIR} ${absolute_dest_dir})
    add_custom_command(
        OUTPUT ${relative_dest_dir}
        COMMAND ${CMAKE_COMMAND} -E make_directory ${absolute_dest_dir}
        DEPENDS ${source_dir}
        )
    file(GLOB source_files "${source_dir}/*")
    foreach(source_file ${source_files})
        # get_filename_component(absolute_source_path ${CMAKE_CURRENT_SOURCE_DIR}/${source_file} ABSOLUTE)
        pandocology_add_input_file(${source_file} ${absolute_dest_dir} dir_dest_filelist)
    endforeach()
    set(${dir_dest_filelist_var} ${${dir_dest_filelist_var}} ${dir_dest_filelist} PARENT_SCOPE)
endfunction()

function(add_to_make_clean filepath)
    get_directory_property(make_clean_files ADDITIONAL_MAKE_CLEAN_FILES)
    set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${make_clean_files};${filepath}")
endfunction()

function(disable_insource_build)
    IF ( CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR AND NOT MSVC_IDE )
        MESSAGE(FATAL_ERROR "The build directory must be different from the main project source "
"directory. Please create a directory such as '${CMAKE_SOURCE_DIR}/build', "
"and run CMake from there, passing the path to this source directory as "
"the path argument. E.g.:
    $ cd ${CMAKE_SOURCE_DIR}
    $ mkdir build
    $ cd build
    $ cmake .. && make && sudo make install
This process created the file `CMakeCache.txt' and the directory `CMakeFiles'.
Please delete them:
    $ rm -r CMakeFiles/ CmakeCache.txt
")
    ENDIF()
endfunction()

# This builds a document
#
# Usage:
#
#
#     INCLUDE(pandocology)
#
#     add_document(
#         figures.tex
#         SOURCES              figures.md
#         RESOURCE_DIRS        figs
#         PANDOC_DIRECTIVES    -t latex
#         NO_EXPORT_PRODUCT
#         )
#
#     add_document(
#         opus.pdf
#         SOURCES              opus.md
#         RESOURCE_FILES       opus.bib systbiol.template.latex systematic-biology.csl
#         RESOURCE_DIRS        figs
#         PANDOC_DIRECTIVES    -t             latex
#                             --smart
#                             --template     systbiol.template.latex
#                             --filter       pandoc-citeproc
#                             --csl          systematic-biology.csl
#                             --bibliography opus.bib
#                             --include-after-body=figures.tex
#         DEPENDS             figures.tex
#         EXPORT_ARCHIVE
#         )
#
function(add_document target_name)
    set(options          EXPORT_ARCHIVE NO_EXPORT_PRODUCT EXPORT_PDF DIRECT_TEX_TO_PDF VERBOSE)
    set(oneValueArgs     PRODUCT_DIRECTORY)
    set(multiValueArgs   SOURCES RESOURCE_FILES RESOURCE_DIRS PANDOC_DIRECTIVES DEPENDS)
    cmake_parse_arguments(ADD_DOCUMENT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )

    # this is because `make clean` will dangerously clean up source files
    disable_insource_build()

    # get the stem of the target name
    pandocology_get_file_stemname(target_stemname ${target_name})
    pandocology_get_file_extension(target_extension ${target_name})
    if (${ADD_DOCUMENT_EXPORT_PDF})
        if (NOT "${target_extension}" STREQUAL ".tex" AND NOT "${target_extension}" STREQUAL ".latex")
        # if (NOT "${target_extension}" STREQUAL ".tex")
            MESSAGE(FATAL_ERROR "Target '${target_name}': Cannot use 'EXPORT_PDF' for target of type '${target_extension}': target type must be '.tex' or '.latex'")
        endif()
    endif()
    if (${ADD_DOCUMENT_DIRECT_TEX_TO_PDF})
        list(LENGTH ${ADD_DOCUMENT_SOURCES} SOURCE_LEN)
        if (SOURCE_LEN GREATER 1)
            MESSAGE(FATAL_ERROR "Target '${target_name}': Only one source can be specified when using the 'DIRECT_TEX_TO_PDF' option")
        endif()
        # set(ADD_DOCUMENT_SOURCES, list(GET ${ADD_DOCUMENT_SOURCES} 1))
        pandocology_get_file_stemname(source_stemname ${ADD_DOCUMENT_SOURCES})
        pandocology_get_file_extension(source_extension ${ADD_DOCUMENT_SOURCES})
        if (NOT "${source_extension}" STREQUAL ".tex" AND NOT "${source_extension}" STREQUAL ".latex")
            MESSAGE(FATAL_ERROR "Target '${target_name}': Cannot use 'DIRECT_TEX_TO_PDF' for source of type '${source_extension}': source type must be '.tex' or '.latex'")
        endif()
        SET(check_target ${source_stemname}.pdf)
        IF (NOT ${check_target} STREQUAL ${target_name})
            MESSAGE(FATAL_ERROR "Target '${target_name}': Must use target name of '${check_target}' if using 'DIRECT_TEX_TO_PDF'")
        endif()
    endif()

    ## set up output directory
    if ("${ADD_DOCUMENT_PRODUCT_DIRECTORY}" STREQUAL "")
        set(ADD_DOCUMENT_PRODUCT_DIRECTORY "product")
    endif()
    get_filename_component(product_directory ${CMAKE_BINARY_DIR}/${ADD_DOCUMENT_PRODUCT_DIRECTORY} ABSOLUTE)
    # get_filename_component(absolute_product_path ${product_directory}/${target_name} ABSOLUTE)
    
    ## get primary source
    set(build_sources)
    foreach(input_file ${ADD_DOCUMENT_SOURCES} )
        if(NOT IS_ABSOLUTE ${input_file})
            set(input_file ${CMAKE_CURRENT_SOURCE_DIR}/${input_file})
        endif()
        pandocology_add_input_file(${input_file} ${CMAKE_CURRENT_BINARY_DIR} build_sources)
    endforeach()

    ## get resource files
    set(build_resources)
    foreach(resource_file ${ADD_DOCUMENT_RESOURCE_FILES})
        if(NOT IS_ABSOLUTE ${resource_file})
            set(resource_file ${CMAKE_CURRENT_SOURCE_DIR}/${resource_file})
        endif()
        pandocology_add_input_file(${resource_file} ${CMAKE_CURRENT_BINARY_DIR} build_resources)
    endforeach()

    ## get resource dirs
    set(exported_resources)
    foreach(resource_dir ${ADD_DOCUMENT_RESOURCE_DIRS})
        if(NOT IS_ABSOLUTE ${resource_file})
            set(resource_dir ${CMAKE_CURRENT_SOURCE_DIR}/${resource_dir})
        endif()
        pandocology_add_input_dir(${resource_dir} ${CMAKE_CURRENT_BINARY_DIR} build_resources)
        if (${ADD_DOCUMENT_EXPORT_ARCHIVE})
            pandocology_add_input_dir(${resource_dir} ${product_directory} exported_resources)
        endif()
    endforeach()
    
    ## primary command
    if (${ADD_DOCUMENT_DIRECT_TEX_TO_PDF})
        if (${ADD_DOCUMENT_VERBOSE})
            add_custom_command(
                OUTPUT  ${target_name} # note that this is in the build directory
                DEPENDS ${build_sources} ${build_resources} ${ADD_DOCUMENT_DEPENDS}
                # WORKING_DIRECTORY ${working_directory}
                COMMAND ${CMAKE_COMMAND} -E make_directory ${product_directory}
                # we produce the target in the source directory, in case other build targets require it as a source
                COMMAND latexmk -gg -halt-on-error -interaction=nonstopmode -file-line-error -pdf ${build_sources}
                )
        else()
            add_custom_command(
                OUTPUT  ${target_name} # note that this is in the build directory
                DEPENDS ${build_sources} ${build_resources} ${ADD_DOCUMENT_DEPENDS}
                # WORKING_DIRECTORY ${working_directory}
                COMMAND ${CMAKE_COMMAND} -E make_directory ${product_directory}
                # we produce the target in the source directory, in case other build targets require it as a source
                COMMAND latexmk -gg -halt-on-error -interaction=nonstopmode -file-line-error -pdf ${build_sources} 2>/dev/null >/dev/null || (grep --no-messages -A8 ".*:[0-9]*:.*" ${target_stemname}.log && false)
                )
        endif()
        add_to_make_clean(${target_name})
    else()
        add_custom_command(
            OUTPUT  ${target_name} # note that this is in the build directory
            DEPENDS ${build_sources} ${build_resources} ${ADD_DOCUMENT_DEPENDS}
            # WORKING_DIRECTORY ${working_directory}
            COMMAND ${CMAKE_COMMAND} -E make_directory ${product_directory}
            # we produce the target in the source directory, in case other build targets require it as a source
            COMMAND ${PANDOC_EXECUTABLE} ${build_sources} ${ADD_DOCUMENT_PANDOC_DIRECTIVES} -o ${target_name}
            )
        add_to_make_clean(${target_name})
    endif()
    
    ## figure out what all is going to be produced by this build set, and set
    ## those as dependencies of the primary target
    set(primary_target_dependencies)
    set(primary_target_dependencies ${primary_target_dependencies} ${target_name})
    if (NOT ${ADD_DOCUMENT_NO_EXPORT_PRODUCT})
        set(primary_target_dependencies ${primary_target_dependencies} ${product_directory}/${target_name})
    endif()
    if (${ADD_DOCUMENT_EXPORT_PDF})
        set(primary_target_dependencies ${primary_target_dependencies} ${CMAKE_CURRENT_BINARY_DIR}/${target_stemname}.pdf)
        set(primary_target_dependencies ${primary_target_dependencies} ${product_directory}/${target_stemname}.pdf)
    endif()
    if (${ADD_DOCUMENT_EXPORT_ARCHIVE})
        set(primary_target_dependencies ${primary_target_dependencies} ${product_directory}/${target_stemname}.tbz)
    endif()
    
    # run post-pdf
    if (${ADD_DOCUMENT_EXPORT_PDF})
        # get_filename_component(target_stemname ${target_name} NAME_WE)
        add_custom_command(
            OUTPUT ${product_directory}/${target_stemname}.pdf ${CMAKE_CURRENT_BINARY_DIR}/${target_stemname}.pdf
            DEPENDS ${target_name} ${build_sources} ${build_resources} ${ADD_DOCUMENT_DEPENDS}

            # Does not work: custom template used to generate tex is ignored
            # COMMAND ${PANDOC_EXECUTABLE} ${target_name} -f latex -o ${target_stemname}.pdf

            # (1)   Apparently, both nonstopmode and batchmode produce an output file
            #       even if there was an error. This tricks latexmk into believing
            #       the file is actually up-to-date.
            #       So we use `-halt-on-error` or `-interaction=errorstopmode`
            #       instead.
            # (2)   `grep` returns a non-zero error code if the pattern is not
            #       found. So, in our scheme below to filter the output of
            #       `pdflatex`, it is precisely when there is NO error that
            #       grep returns a non-zero code, which fools CMake into thinking
            #       tex'ing failed.
            #       Hence the need for `| grep ...| cat` or `| grep  || true`.
            #       But we can go better:
            #           latexmk .. || (grep .. && false)
            #       So we can have our cake and eat it too: here we want to
            #       re-raise the error after a successful grep if there was an
            #       error in `latexmk`.
            # COMMAND latexmk -gg -halt-on-error -interaction=nonstopmode -file-line-error -pdf ${target_name} 2>&1 | grep -A8 ".*:[0-9]*:.*" || true
            COMMAND latexmk -gg -halt-on-error -interaction=nonstopmode -file-line-error -pdf ${target_name} 2>/dev/null >/dev/null || (grep --no-messages -A8 ".*:[0-9]*:.*" ${target_stemname}.log && false)

            COMMAND ${CMAKE_COMMAND} -E copy ${target_stemname}.pdf ${product_directory}
            )
        add_to_make_clean(${CMAKE_CURRENT_BINARY_DIR}/${target_stemname}.pdf)
        add_to_make_clean(${product_directory}/${target_stemname}.pdf)
    endif()
    
    ## copy products
    if (NOT ${ADD_DOCUMENT_NO_EXPORT_PRODUCT})
        add_custom_command(
            OUTPUT ${product_directory}/${target_name}
            DEPENDS ${build_sources} ${build_resources} ${ADD_DOCUMENT_DEPENDS}
            COMMAND ${CMAKE_COMMAND} -E copy ${target_name} ${product_directory}
            )
        add_to_make_clean(${product_directory}/${target_name})
    endif()
    
    ## copy resources
    if (${ADD_DOCUMENT_EXPORT_ARCHIVE})
        # get_filename_component(target_stemname ${target_name} NAME_WE)
        # add_custom_command(
        #     TARGET ${target_name} POST_BUILD
        #     DEPENDS ${build_sources} ${build_resources} ${ADD_DOCUMENT_DEPENDS}
        #     # COMMAND cp ${build_resources} ${ADD_DOCUMENT_DEPENDS} ${product_directory}
        #     COMMAND ${CMAKE_COMMAND} -E tar cjf ${product_directory}/${target_stemname}.tbz ${target_name} ${build_resources} ${ADD_DOCUMENT_DEPENDS}
        #     )
        add_custom_command(
            OUTPUT ${product_directory}/${target_stemname}.tbz
            DEPENDS ${target_name}
            # COMMAND cp ${build_resources} ${ADD_DOCUMENT_DEPENDS} ${product_directory}
            COMMAND ${CMAKE_COMMAND} -E tar cjf ${product_directory}/${target_stemname}.tbz ${target_name} ${build_resources} ${ADD_DOCUMENT_DEPENDS}
            )
        add_to_make_clean(${product_directory}/${target_stemname}.tbz)
        # add_custom_target(
        #     ${target_stemname}.ARCHIVE
        #     ALL
        #     DEPENDS ${product_directory}/${target_stemname}.tbz
        #     )
    endif()

endfunction(add_document)

function(add_pandoc_document)
    add_document(${ARGV})
endfunction()