OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / cmake / OpenCogGuile.cmake

456 lines · cmake ref:
# Copyright (C) 2016 OpenCog Foundation

# References:
# https://www.gnu.org/software/guile/manual/guile.html#Modules-and-the-File-System
# https://www.gnu.org/software/guile/manual/guile.html#Creating-Guile-Modules
# https://www.gnu.org/software/guile/manual/guile.html#Installing-Site-Packages

# Definitions:
#
# * MODULE_FILE: The name of the file that defines the module. It has
#   the same name as the directory it is in, or the name of the parent
#   directory of current directory if it is in a folder named 'scm'.
#   In addition this file shoule have a define-module expression
#   for it be importable, as per guile's specification. See reference
#   links above.

# By default Guile return the path to its installation location.
# Such path will not work for users who wants to compile and install
# the project with a custom CMAKE_INSTALL_PREFIX. Compiling with
# custom PREFIX is a common practice. To support custom PREFIX
# this condition is added to override GUILE_SITE_DIR value using
# `cmake -DGUILE_SITE_DIR=...`.
IF (NOT DEFINED GUILE_SITE_DIR)
    IF(HAVE_GUILE)
        EXECUTE_PROCESS(COMMAND guile -c "(display (%site-dir))"
            OUTPUT_VARIABLE GUILE_SITE_DIR
            OUTPUT_STRIP_TRAILING_WHITESPACE)
    ENDIF()
ENDIF()
ADD_DEFINITIONS(-DGUILE_SITE_DIR="${GUILE_SITE_DIR}")

# Location of complied scheme files (*.go files) that the guile loader
# will search when looking for modules. Used in Makefiles to install
# the *.go files. Typical location is /usr/local/lib/guile/3.0/site-ccache
# or similar. Over-ride by using
# `cmake -DGUILE_CCACHE_DIR=...`.
IF (NOT DEFINED GUILE_CCACHE_DIR)
    IF(HAVE_GUILE)
        EXECUTE_PROCESS(COMMAND guile -c "(display (cadr %load-compiled-path))"
            OUTPUT_VARIABLE GUILE_CCACHE_DIR
            OUTPUT_STRIP_TRAILING_WHITESPACE)
    ENDIF()
ENDIF()
# ADD_DEFINITIONS(-DGUILE_CCACHE_DIR="${GUILE_CCACHE_DIR}")

SET(GUILE_BIN_DIR "${CMAKE_BINARY_DIR}/opencog/scm")

# -------------------------------------------------------------------
#
# This configures the install and binary paths for each file.
# Variables set in the PARENTS_SCOPE context are MODULE_NAME and
# FILE_INSTALL_PATH. All other variables are local to this function.
FUNCTION(PROCESS_MODULE_STRUCTURE FILE_PATH)
    GET_PROPERTY(FILE_GENERATED SOURCE ${FILE_PATH}
        PROPERTY GENERATED SET)
    GET_FILENAME_COMPONENT(DIR_PATH ${FILE_PATH} DIRECTORY)
    GET_FILENAME_COMPONENT(FILE_NAME ${FILE_PATH} NAME)

    # Check if the file exists or is generated, and set
    # FULL_DIR_PATH or target dependencies.
    IF(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${DIR_PATH}/${FILE_NAME})
        SET(FULL_DIR_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${DIR_PATH}/")
    ELSEIF(EXISTS /${DIR_PATH}/${FILE_NAME})
        SET(FULL_DIR_PATH "/${DIR_PATH}/")
    ELSEIF(FILE_GENERATED AND (NOT SCM_DEPENDS))
        MESSAGE(FATAL_ERROR "The target that generates ${FILE_PATH} "
            "has not been added as a dependency using the keyword "
            "argument 'DEPENDS'")
    ELSEIF(FILE_GENERATED AND SCM_DEPENDS)
        ADD_DEPENDENCIES(${COPY_TARGET_NAME} ${SCM_DEPENDS})
        SET(FULL_DIR_PATH "/${DIR_PATH}/")
    ELSE()
        MESSAGE(FATAL_ERROR "${FILE_PATH} file does not exist in "
            "${CMAKE_CURRENT_SOURCE_DIR} nor does it have "
            "'GENERATED' property")
    ENDIF()

    # Specify module paths.
    STRING(REGEX MATCH
        "^(${PREFIX_DIR_PATH})([_a-z0-9/-]+)*/([_a-z0-9-]+)" ""
        ${SCM_MODULE_DESTINATION})

    # MODULE_NAME: it is equal to the MODULE_DESTINATION
    #              directory name
    # MODULE_FILE_DIR_PATH: the directory path where the
    #              MODULE_FILE is installed.
    # MODULE_DIR_PATH: the directory path where the files
    #              associated with the module are installed
    #              at and copied to, with the exception
    #              of the MODULE_FILE.
    SET(MODULE_NAME ${CMAKE_MATCH_3})
    SET(MODULE_NAME ${CMAKE_MATCH_3} PARENT_SCOPE)
    SET(MODULE_FILE_DIR_PATH ${CMAKE_MATCH_2})
    SET(MODULE_DIR_PATH ${CMAKE_MATCH_2}/${CMAKE_MATCH_3})

    IF (SCM_MODULE)
        SET(MODULE_NAME ${SCM_MODULE})
        SET(MODULE_NAME ${SCM_MODULE} PARENT_SCOPE)
    ENDIF()
    IF (NOT MODULE_NAME)
        SET(MODULE_NAME "opencog")
        SET(MODULE_NAME "opencog" PARENT_SCOPE)
    ENDIF()
    IF (${MODULE_DIR_PATH} STREQUAL "/")
        SET(MODULE_DIR_PATH ${DIR_PATH})
    ENDIF()

    # Set the install path.
    IF ("${MODULE_NAME}.scm" STREQUAL "${FILE_NAME}")
        SET(FILE_BUILD_PATH "${GUILE_BIN_DIR}/${MODULE_FILE_DIR_PATH}")
        SET(FILE_INSTALL_PATH "${GUILE_SITE_DIR}/${MODULE_FILE_DIR_PATH}"
            PARENT_SCOPE)
    ELSE()
        SET(FILE_BUILD_PATH "${GUILE_BIN_DIR}/${MODULE_DIR_PATH}")
        SET(FILE_INSTALL_PATH "${GUILE_SITE_DIR}/${MODULE_DIR_PATH}"
            PARENT_SCOPE)
    ENDIF()

    # Copy files into the build directory, mirroring the install
    # path structure.
    EXECUTE_PROCESS(
        COMMAND ${CMAKE_COMMAND} -E make_directory ${FILE_BUILD_PATH})

    ADD_CUSTOM_COMMAND(TARGET ${COPY_TARGET_NAME} PRE_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy "${FULL_DIR_PATH}/${FILE_NAME}"
             "${FILE_BUILD_PATH}/${FILE_NAME}")

ENDFUNCTION(PROCESS_MODULE_STRUCTURE)

# -------------------------------------------------------------------
#
# This compiles a guile module. First argument is the name of the
# module. Additional arguments are taken to be dependencies of the
# module (so that a recompile is forced, whenever a depedency changes).
FUNCTION(COMPILE_MODULE MODULE_NAME)
    SET(FILE_BUILD_PATH ${GUILE_BIN_DIR})

    # Set the install path for the compiled file. Do some guess-work
    # to handle the most common case. The guesswork fails, if the
    # modules is more than two deep e.g `(opencog foo bar)` XXX FIXME
    GET_FILENAME_COMPONENT(DIR_PATH ${MODULE_NAME} DIRECTORY)
    SET(GO_INSTALL_PATH ${GUILE_CCACHE_DIR}/${DIR_PATH} PARENT_SCOPE)
    IF ((${DIR_PATH}x STREQUAL x) AND (NOT (${MODULE_NAME} STREQUAL opencog)))
        SET(GO_INSTALL_PATH ${GUILE_CCACHE_DIR}/opencog PARENT_SCOPE)
    ENDIF()

    # Strip out slashes in the module name.
    STRING(REPLACE "/" "_" PHONY_TARGET ${MODULE_NAME})

    # Create a phony target so that users can reference it directly.
    ADD_CUSTOM_TARGET(${PHONY_TARGET}_go ALL
        DEPENDS ${FILE_BUILD_PATH}/${MODULE_NAME}.go)

    # Add a dependency on the base atomspace module, by default.
    # That is, opencog.go *must* be built before anything else,
    # as otherwise, there will be build errors.
    IF (NOT (${MODULE_NAME} STREQUAL opencog))
        ADD_DEPENDENCIES(${PHONY_TARGET}_go opencog_go)
        # Also add file-level dependency so parallel builds work correctly
        SET(OPENCOG_GO_DEP ${FILE_BUILD_PATH}/opencog.go)
    ELSE()
        SET(OPENCOG_GO_DEP "")
    ENDIF()

    # Remainder of the arguments is a list of dependencies.
    SET(MODULE_FILES ${ARGN})

    # Perform the actual compilation
    # Disable auto-compilation to prevent guild from trying to recompile
    # dependencies that appear newer in the build directory.
    # Set GUILE_LOAD_COMPILED_PATH so that guild can find opencog.go.
    # Add PROJECT_SOURCE_DIR/opencog/scm to load path so that files like
    # opencog/base/core-docs.scm can be found when loading opencog module.
    ADD_CUSTOM_COMMAND(
        OUTPUT ${FILE_BUILD_PATH}/${MODULE_NAME}.go
        COMMAND ${CMAKE_COMMAND} -E env
                GUILE_AUTO_COMPILE=0
                GUILE_LOAD_COMPILED_PATH=${GUILE_BIN_DIR}
                guild compile
                ${CMAKE_CURRENT_SOURCE_DIR}/${MODULE_NAME}.scm
                -o ${FILE_BUILD_PATH}/${MODULE_NAME}.go
                -L ${CMAKE_CURRENT_SOURCE_DIR}
                -L ${GUILE_BIN_DIR}
                -L ${PROJECT_SOURCE_DIR}/opencog/scm
        DEPENDS ${MODULE_FILES} ${OPENCOG_GO_DEP}
        COMMENT "Compiling ${MODULE_NAME}.scm"
        VERBATIM)

ENDFUNCTION(COMPILE_MODULE)

# ---------------------------------------------------------------------
# When building, all files specifed are are copied to
# '${CMAKE_BINARY_DIR}/opencog/scm' following the same file tree
# as will be installed (to /usr/local/share/guile/site/3.0/opencog)
#
# It has four keyword arguments
#
# FILES: List of files to be installed/copied.
#
# MODULE_DESTINATION: The absolute path where the files associated
#   with the module are installed, with the exception of the
#   MODULE_FILE(see definition at top of this file). The path for
#   MODULE_FILE, is inferred from this argument, even if it is the
#   only file to be installed.
#
# DEPENDS: Optional argument. A list of any targets that must be built
#   before the module can be built. A typical use is to make sure that
#   the `atom_types.scm` file is created first, before building this
#   module.
#
# COMPILE: Optional keyword. If present, the module file (the first
#   file in the file-list) will be compiled into guile RTL bytecode,
#   and installed into the guile bytecode cache location.
#
FUNCTION(ADD_GUILE_MODULE)
  # Define the target that will be used to copy scheme files in the
  # current source directory to the build directory. This is done so
  # as to be able to run scheme unit-tests without having to run
  # 'make install'. The tilde ~ occurs in package management (Debian).
  STRING(REGEX REPLACE "[/~]" "_" _TARGET_NAME_SUFFIX ${CMAKE_CURRENT_SOURCE_DIR})
  SET(COPY_TARGET_NAME "COPY_TO_LOAD_PATH_IN_BUILD_DIR_FROM_${_TARGET_NAME_SUFFIX}")
  IF(HAVE_GUILE)
    SET(PREFIX_DIR_PATH "${GUILE_SITE_DIR}")
    SET(options COMPILE)
    SET(oneValueArgs MODULE_DESTINATION MODULE)
    SET(multiValueArgs FILES DEPENDS)
    CMAKE_PARSE_ARGUMENTS(SCM "${options}" "${oneValueArgs}"
        "${multiValueArgs}" ${ARGN})

    # Cannot copy any files until after they are generated!
    IF (NOT (TARGET ${COPY_TARGET_NAME}))
      ADD_CUSTOM_TARGET(${COPY_TARGET_NAME} ALL DEPENDS ${SCM_DEPENDS})
    ENDIF()

    # The SCM module is given by the name of the first file
    # in the list.
    LIST(GET SCM_FILES 0 SCM_MODULE_FILE)
    STRING(REPLACE ".scm" "" SCM_MODULE ${SCM_MODULE_FILE})

    IF(NOT DEFINED SCM_MODULE_DESTINATION)
        GET_FILENAME_COMPONENT(DIR_PATH ${SCM_MODULE} DIRECTORY)
        SET(SCM_MODULE_DESTINATION ${GUILE_SITE_DIR}/${DIR_PATH})
    ENDIF()

    # The keyword argument 'FILES' is required.
    IF(DEFINED SCM_FILES)

        # If the COMPILE keyword is set, then compile the module into
        # module.go RTL bytecode.
        IF (${SCM_COMPILE})
            COMPILE_MODULE(${SCM_MODULE} ${SCM_FILES})

            # Override GO_INSTALL_PATH to match the .scm install location.
            # Replace GUILE_SITE_DIR with GUILE_CCACHE_DIR in the destination.
            STRING(REPLACE "${GUILE_SITE_DIR}" "${GUILE_CCACHE_DIR}"
                   GO_INSTALL_PATH "${SCM_MODULE_DESTINATION}")

            IF(DEFINED SCM_DEPENDS)
                # Strip out slashes in the module name.
                STRING(REPLACE "/" "_" PHONY_TARGET ${SCM_MODULE})
                ADD_DEPENDENCIES(${PHONY_TARGET}_go ${SCM_DEPENDS})
            ENDIF()

            # Touch the .go file after .scm files are copied, so that
            # the .go file has a newer timestamp than the .scm files.
            # This prevents Guile from auto-recompiling the module.
            SET(GO_BUILD_OUTPUT "${GUILE_BIN_DIR}/${SCM_MODULE}.go")
            STRING(REPLACE "/" "_" GO_TARGET_NAME ${SCM_MODULE})
            # Make COPY depend on _go so .go file exists before we touch it
            ADD_DEPENDENCIES(${COPY_TARGET_NAME} ${GO_TARGET_NAME}_go)
            ADD_CUSTOM_COMMAND(TARGET ${COPY_TARGET_NAME} POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E touch ${GO_BUILD_OUTPUT})
        ENDIF()

# Arghhh FILE TOUCH first appears in version 3.12.0
# Everyone else is screwed.  Well, that explains a lot.
if(${CMAKE_VERSION} VERSION_GREATER "3.11.0")

        # FILE_PATH is used for variable name because files in
        # sub-directories may be passed.
        FOREACH(FILE_PATH ${SCM_FILES})

            PROCESS_MODULE_STRUCTURE(${FILE_PATH})

            # If any file in the module is newer than the module
            # itself, then touch the module; this is needed to force
            # (reinstall and) recompilation of the module!
            INSTALL(CODE "
              IF(EXISTS
                     ${CMAKE_CURRENT_SOURCE_DIR}/${MODULE_NAME}.scm
                 AND NOT
                     ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_PATH}
                   STREQUAL
                     ${CMAKE_CURRENT_SOURCE_DIR}/${MODULE_NAME}.scm
                 AND
                     ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_PATH}
                   IS_NEWER_THAN
                     ${CMAKE_CURRENT_SOURCE_DIR}/${MODULE_NAME}.scm
                 )
                 MESSAGE(\"-- Touch: ${CMAKE_CURRENT_SOURCE_DIR}/${MODULE_NAME}.scm\")
                 MESSAGE(\"-- Newer: ${CMAKE_CURRENT_SOURCE_DIR}/${FILE_PATH}\")
                 FILE(TOUCH ${CMAKE_CURRENT_SOURCE_DIR}/${MODULE_NAME}.scm)
              ENDIF()
            ")

        ENDFOREACH()
endif()

        # Run the loop again, this time installing. We need to run
        # the loop a second time, because the file-touch above sets
        # time-stamps of files earlier in the loop.
        #
        # We install here, instead of in the PROCESS_MODULE_STRUCTURE
        # function, so as to avoid "Command INSTALL() is not
        # scriptable" errors. This error is hit when copying scheme
        # files auto-generated by the  OPENCOG_ADD_ATOM_TYPES macro.
        #
        # Note: We only compile the module file (the first file in the
        # list). Auxiliary files are NOT compiled separately, because
        # they are typically include-from-path'd by the module file,
        # meaning their content is already compiled into the module's
        # .go bytecode file.
        FOREACH(FILE_PATH ${SCM_FILES})

            PROCESS_MODULE_STRUCTURE(${FILE_PATH})

            INSTALL (FILES ${FILE_PATH}
                     DESTINATION ${FILE_INSTALL_PATH})

        ENDFOREACH()

        # Install the compiled module .go file, and touch it so that
        # the timestamp on the module.go file is newer than the
        # timestamp on the module.scm file.
        IF (${SCM_COMPILE})
            GET_FILENAME_COMPONENT(GO_FILE_NAME ${SCM_MODULE} NAME)
            SET(GO_FILE_NAME "${GO_FILE_NAME}.go")
            INSTALL(
                FILES ${GUILE_BIN_DIR}/${SCM_MODULE}.go
                DESTINATION ${GO_INSTALL_PATH})
            INSTALL(CODE "
                FILE(TOUCH \"\$ENV{DESTDIR}${GO_INSTALL_PATH}/${GO_FILE_NAME}\")
            ")
        ENDIF()

    ELSE()
        IF(NOT DEFINED SCM_FILES)
            MESSAGE(FATAL_ERROR "The keyword argument 'SCM_FILES' is not set")
        ENDIF()

        IF(NOT DEFINED SCM_MODULE_DESTINATION)
            MESSAGE(FATAL_ERROR "The keyword argument "
                   "'SCM_MODULE_DESTINATION' is not set")
        ENDIF()
    ENDIF()
  ENDIF()
ENDFUNCTION(ADD_GUILE_MODULE)

FUNCTION(ADD_GUILE_TEST TEST_NAME FILE_NAME)
    IF(HAVE_GUILE)
        SET(FILE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${FILE_NAME}")

        # Check if the file exists in the current source directory.
        IF(NOT EXISTS ${FILE_PATH})
            MESSAGE(FATAL_ERROR "${FILE_NAME} file does not exist in "
                ${CMAKE_CURRENT_SOURCE_DIR})
        ENDIF()

        ADD_TEST(NAME ${TEST_NAME}
            COMMAND guile -L ${PROJECT_BINARY_DIR}/opencog/scm
                      --use-srfi=64 ${FILE_PATH}
            WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})

        IF (GUILE_LOAD_PATH)
            SET_PROPERTY(TEST ${TEST_NAME} PROPERTY
                ENVIRONMENT "GUILE_LOAD_PATH=${GUILE_LOAD_PATH}:$ENV{GUILE_LOAD_PATH}")
        ENDIF(GUILE_LOAD_PATH)
    ENDIF()
ENDFUNCTION(ADD_GUILE_TEST)

# ----------------------------------------------------------------------------
# Declare a new dummy target representing path configuration for Guile
# extensions.  This must be called, before calling ADD_GUILE_EXTENSION
# and WRITE_GUILE_CONFIG.
#
# CONFIG_TARGET: The new target name
#
# MODULE_NAME: A space separated string representing the full guile
# module, e.g. "opencog as-config". This is passed to (define-module ...)
# and is used for importing extension paths.
#
FUNCTION(DECLARE_GUILE_CONFIG_TARGET CONFIG_TARGET MODULE_NAME TEST_ENV_VAR)
    ADD_CUSTOM_TARGET(${CONFIG_TARGET} ALL)
    SET_TARGET_PROPERTIES(${CONFIG_TARGET}
        PROPERTIES MODULE_NAME "${MODULE_NAME}")
ENDFUNCTION(DECLARE_GUILE_CONFIG_TARGET)

# ----------------------------------------------------------------------------
# Link a compiled guile extension to the config target
#
# CONFIG_TARGET: The target previously declared with DECLARE_GUILE_CONFIG
#
# EXTENSION_TARGET: A shared library target that can be loaded as a
# guile extension.
#
# SYMBOL_NAME: The guile symbol to use to define the path to the
# extension library.  In the build directory, this will point to the
# exact path. When installed this will point to OpenCog's shared library
# install dir.
#
FUNCTION(ADD_GUILE_EXTENSION CONFIG_TARGET EXTENSION_TARGET SYMBOL_NAME)
    GET_TARGET_PROPERTY(EXT_LIB_PATH ${EXTENSION_TARGET} BINARY_DIR)
    GET_TARGET_PROPERTY(EXT_LIBS ${CONFIG_TARGET} EXT_LIBS)
    IF (EXT_LIBS)
        LIST(APPEND EXT_LIBS "${SYMBOL_NAME}|${EXT_LIB_PATH}")
    ELSE(EXT_LIBS)
        SET(EXT_LIBS "${SYMBOL_NAME}|${EXT_LIB_PATH}")
    ENDIF (EXT_LIBS)
    SET_TARGET_PROPERTIES(${CONFIG_TARGET} PROPERTIES EXT_LIBS "${EXT_LIBS}")
ENDFUNCTION(ADD_GUILE_EXTENSION)

# ----------------------------------------------------------------------------
# Write out a config module, based on all extensions linked to the config
# target.
#
# OUTPUT_FILE: Where to write the file
#
# CONFIG_TARGET: The target previously declared with DECLARE_GUILE_CONFIG
#
# SCM_IN_BUILD_DIR: Whether to generate a config file for the build dir
# or for installing to a system dir.
#
FUNCTION(WRITE_GUILE_CONFIG OUTPUT_FILE CONFIG_TARGET SCM_IN_BUILD_DIR)
    set(SCM_BUILD_PATHS "")
    set(SCM_INSTALL_PATHS "")
    get_target_property(SYMBOL_PATH_LIST ${CONFIG_TARGET} EXT_LIBS)
    get_target_property(MODULE_NAME ${CONFIG_TARGET} MODULE_NAME)

    FILE(WRITE "${OUTPUT_FILE}" "(define-module (${MODULE_NAME}))\n")
    foreach(PATH_PAIR ${SYMBOL_PATH_LIST})
        string(REPLACE "|" ";" SYMBOL_AND_PATH ${PATH_PAIR})
        list(GET SYMBOL_AND_PATH 0 SYMBOL)
        list(GET SYMBOL_AND_PATH 1 LIBPATH)

        IF (SCM_IN_BUILD_DIR)
            FILE(APPEND "${OUTPUT_FILE}"
                "(define-public ${SYMBOL} \"${LIBPATH}/\")\n")
        ELSE (SCM_IN_BUILD_DIR)
            FILE(APPEND "${OUTPUT_FILE}"
                "(define-public ${SYMBOL} \"${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/opencog/\")\n")

        ENDIF (SCM_IN_BUILD_DIR)
    endforeach()

ENDFUNCTION(WRITE_GUILE_CONFIG)