1# GncAddSchemeTargets.cmake Define a command to compile Scheme programs with Guile
2# Copyright (c) 2015, Rob Gowin
3# Copyright 2017 John Ralls <jralls@ceridwen.us>
4#
5# This program is distributed in the hope that it will be useful,
6# but WITHOUT ANY WARRANTY; without even the implied warranty of
7# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
8# GNU General Public License for more details.
9#
10# You should have received a copy of the GNU General Public License
11# along with this program; if not, contact:
12# Free Software Foundation           Voice:  +1-617-542-5942
13# 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
14# Boston, MA  02110-1301,  USA       gnu@gnu.org
15
16# Guile and ltdl require MSYS paths on MinGW-w64; this function transforms them.
17function(make_unix_path PATH)
18    string(REGEX REPLACE "^([A-Za-z]):" "/\\1" newpath ${${PATH}})
19    string(REGEX REPLACE "\\\\" "/" newpath ${newpath})
20    set(${PATH} ${newpath} PARENT_SCOPE)
21endfunction()
22
23#PATH variables in the environment are separated by colons, but CMake lists are separated by semicolons. This function transforms the separators.
24function(make_unix_path_list PATH)
25    string(REPLACE ";" ":" newpath "${${PATH}}")
26    set(${PATH} ${newpath} PARENT_SCOPE)
27endfunction()
28
29# This function will set two or four environment variables to a directory in the parent PARENT_SCOPE
30# * _DIRCLASS (eg "prefix", "sitedir" is used to construct the variable name(s) and in error messages
31# * _DIRCMD is the guile command to run to get the path for the given _DIRCLASS (eg "(display (%site-dir))")
32# * _PREFIX: if set will be used to calculate paths relative to this prefix and
33#   set two more environment variable with this relative path.
34# When run successfully this function will set following variables in the parent scope:
35# * GUILE_${CMDCLASS} and GUILE_UNIX_${CMDCLASS} - the latter is the former transformed
36#   into an msys compatible format (c:\some\directory => /c/some/directory)
37# * If _PREFIX was set: GUILE_${CMDCLASS} and GUILE_REL_UNIX_${CMDCLASS}
38#   which are the former two variables with _PREFIX removed
39#   (_PREFIX=/usr, GUILE_${CMDCLASS} = /usr/share/something
40#    => GUILE_REL_${CMDCLASS} = share/something
41function(find_one_guile_dir _DIRCLASS _DIRCMD _PREFIX)
42
43    string(TOUPPER ${_DIRCLASS} CLASS_UPPER)
44    string(TOLOWER ${_DIRCLASS} CLASS_LOWER)
45    execute_process(
46        COMMAND ${GUILE_EXECUTABLE} -c ${_DIRCMD}
47        RESULT_VARIABLE CMD_RESULT
48        OUTPUT_VARIABLE CMD_OUTPUT
49        ERROR_VARIABLE CMD_ERROR
50        OUTPUT_STRIP_TRAILING_WHITESPACE
51        ERROR_STRIP_TRAILING_WHITESPACE
52    )
53    if (CMD_RESULT)
54        message(SEND_ERROR "Could not determine Guile ${CLASS_LOWER}:\n${CMD_ERROR}")
55    endif()
56
57    set(GUILE_${CLASS_UPPER} ${CMD_OUTPUT} PARENT_SCOPE)
58    set(CMD_UNIX_OUTPUT  ${CMD_OUTPUT})
59    make_unix_path(CMD_UNIX_OUTPUT)
60    set(GUILE_UNIX_${CLASS_UPPER} ${CMD_UNIX_OUTPUT} PARENT_SCOPE)
61
62    if (_PREFIX)
63        string(REGEX REPLACE "^${_PREFIX}[\\/]*" "" CMD_REL_OUTPUT ${CMD_OUTPUT})
64        set(GUILE_REL_${CLASS_UPPER} ${CMD_REL_OUTPUT} PARENT_SCOPE)
65        set(CMD_REL_UNIX_OUTPUT  ${CMD_REL_OUTPUT})
66        make_unix_path(CMD_REL_UNIX_OUTPUT)
67        set(GUILE_REL_UNIX_${CLASS_UPPER} ${CMD_REL_UNIX_OUTPUT} PARENT_SCOPE)
68    endif()
69
70endfunction()
71
72# Query the guile executable for path information. We're interested in guile's
73# datadir, libdir, sitedir, ccachedir and siteccachedir
74macro(find_guile_dirs)
75    # Get GUILE_PREFIX and GUILE_UNIX_PREFIX
76    find_one_guile_dir("prefix" "(display (assoc-ref %guile-build-info 'prefix))" "")
77    # Get GUILE_DATADIR, GUILE_UNIX_DATADIR, GUILE_REL_DATADIR, GUILE_REL_UNIX_DATADIR
78    find_one_guile_dir("datadir" "(display (%package-data-dir))" ${GUILE_PREFIX})
79    # Get GUILE_LIBDIR, GUILE_UNIX_LIBDIR, GUILE_REL_LIBDIR, GUILE_REL_UNIX_LIBDIR
80    find_one_guile_dir("libdir" "(display (%library-dir))" ${GUILE_PREFIX})
81    # Get GUILE_CCACHEDIR, GUILE_UNIX_CCACHEDIR, GUILE_REL_CCACHEDIR, GUILE_REL_UNIX_CCACHEDIR
82    find_one_guile_dir("ccachedir" "(display (assoc-ref %guile-build-info 'ccachedir))" ${GUILE_PREFIX})
83    # Get GUILE_SITEDIR, GUILE_UNIX_SITEDIR, GUILE_REL_SITEDIR, GUILE_REL_UNIX_SITEDIR
84    find_one_guile_dir("sitedir" "(display (%site-dir))" ${GUILE_PREFIX})
85    # Get GUILE_SITECCACHEDIR, GUILE_UNIX_SITECCACHEDIR, GUILE_REL_SITECCACHEDIR, GUILE_REL_UNIX_SITECCACHEDIR
86    find_one_guile_dir("siteccachedir" "(display (%site-ccache-dir))" ${GUILE_PREFIX})
87    string(REGEX REPLACE "[/\\]*${GUILE_EFFECTIVE_VERSION}$" "" GUILE_REL_TOP_SITEDIR ${GUILE_REL_SITEDIR})
88    string(REGEX REPLACE "[/]*${GUILE_EFFECTIVE_VERSION}$" "" GUILE_REL_UNIX_TOP_SITEDIR ${GUILE_REL_UNIX_SITEDIR})
89
90    # Generate replacement strings for use in environment file. The paths used are
91    # the paths found in %load-path and %load-compiled-path by default but
92    # rebased on {GNC_HOME} (which is the runtime resolved variable to where gnucash
93    # gets installed).
94    set (GNC_GUILE_LOAD_PATH
95            "{GNC_HOME}/${GUILE_REL_UNIX_LIBDIR}"
96            "{GNC_HOME}/${GUILE_REL_UNIX_SITEDIR}"
97            "{GNC_HOME}/${GUILE_REL_UNIX_TOP_SITEDIR}"
98            "{GNC_HOME}/${GUILE_REL_UNIX_SITEDIR}/gnucash/deprecated" # Path to gnucash' deprecated modules
99            "{GNC_HOME}/${GUILE_REL_UNIX_DATADIR}")
100
101    set (GNC_GUILE_LOAD_COMPILED_PATH
102            "{GNC_HOME}/${GUILE_REL_UNIX_CCACHEDIR}"
103            "{GNC_HOME}/${GUILE_REL_UNIX_CCACHEDIR}/gnucash/deprecated"
104            "{GNC_HOME}/${GUILE_REL_UNIX_SITECCACHEDIR}")
105endmacro(find_guile_dirs)
106
107# gnc_add_scheme_targets (target
108#                         SOURCES source1 source2 ...
109#                         OUTPUT_DIR directory
110#                         [DEPENDS depedency1 dependency2 ...]
111#                         [MAKE_LINKS] [TEST])
112#
113#̉ Use this function to add scheme targets, that is *.scm files to
114# compile into their corresponding *.go files.
115#
116# SOURCES is a list of scm source files to compile.
117#
118# The resulting *.go binaries will be generated in OUTPUT_DIR directory.
119# This directory is interpreted as a path relative to the build's guile compiled directory
120# directory. For example if guile binaries in the build directory are located in
121# $HOME/gnucash/build/lib/x86_64-linux-gnu/guile/2.0/site-cache and OUTPUT_DIR is "gnucash"
122# the binary .go files will go into
123# $HOME/gnucash/build/lib/x86_64-linux-gnu/guile/2.0/site-cache/gnucash
124#
125# If cmake targets are provided via the DEPENDS keyword those will be added to
126# the guile targets as dependencies.
127#
128# If MAKE_LINKS is set links (or copies on Windows) will be set up
129# from the source directory to the build's guile sources directory.
130# For example if guile source path in the build directory is
131# $HOME/gnucash/build/share/guile/site/2.0 and OUTPUT_DIR is "gnucash"
132# the links or copies will go into
133# $HOME/gnucash/build/share/guile/site/2.0/gnucash
134#
135# If keyword TEST is specified this target will be treated as a test target.
136# That is its compiled files won't be installed and will be added to the set
137# of tests to run via the "check" target. If TEST is not set the targets are
138# considerd normal targets and will be added to the list of files to install.
139# They will be installed in the guile compied directory relative to the prefix
140# set up for this build, with the OUTPUT_DIR appended to it. For example:
141# /usr/local/lib/x86_64-linux-gnu/guile/2.0/site-cache/gnucash
142function(gnc_add_scheme_targets _TARGET)
143  set(noValues MAKE_LINKS TEST)
144  set(singleValues OUTPUT_DIR)
145  set(multiValues SOURCES DEPENDS)
146  cmake_parse_arguments(SCHEME_TGT "${noValues}" "${singleValues}" "${multiValues}" ${ARGN})
147
148  set(__DEBUG FALSE)
149  if (__DEBUG)
150    message("Parameters to COMPILE_SCHEME for target ${_TARGET}")
151    message("   SOURCE_FILES: ${SCHEME_TGT_SOURCES}")
152    message("   GUILE_DEPENDS: ${SCHEME_TGT_DEPENDS}")
153    message("   MAKE_LINKS: ${SCHEME_TGT_MAKE_LINKS}")
154    message("   TEST: ${SCHEME_TGT_TEST}")
155    message("   DIRECTORIES: ${BINDIR_BUILD}, ${LIBDIR_BUILD}, ${DATADIR_BUILD}, ${SCHEME_TGT_OUTPUT_DIR}")
156  endif()
157  set(_CMD "create_symlink")
158  if(WIN32)
159    set(_CMD "copy")
160  endif()
161  set(current_srcdir ${CMAKE_CURRENT_SOURCE_DIR})
162  set(current_bindir ${CMAKE_CURRENT_BINARY_DIR})
163  set(build_bindir ${BINDIR_BUILD})
164  set(build_libdir ${LIBDIR_BUILD})
165  set(build_datadir ${DATADIR_BUILD})
166  if(MINGW64 AND ${GUILE_EFFECTIVE_VERSION} VERSION_LESS 2.2)
167    make_unix_path(build_bindir)
168    make_unix_path(build_libdir)
169    make_unix_path(build_datadir)
170    make_unix_path(current_bindir)
171    make_unix_path(current_srcdir)
172    make_unix_path(CMAKE_BINARY_DIR)
173    make_unix_path(CMAKE_SOURCE_DIR)
174  endif()
175
176  # If links are requested, we simply link (or copy, for Windows) each source file to the dest directory
177  set(TARGET_LINKS "")
178  if(SCHEME_TGT_MAKE_LINKS)
179    set(_LINK_DIR ${CMAKE_BINARY_DIR}/${GUILE_REL_UNIX_SITEDIR}/${SCHEME_TGT_OUTPUT_DIR})
180    file(MAKE_DIRECTORY ${_LINK_DIR})
181    set(_SCHEME_LINKS "")
182    foreach(scheme_file ${SCHEME_TGT_SOURCES})
183      set(_SOURCE_FILE ${current_srcdir}/${scheme_file})
184      if(IS_ABSOLUTE ${scheme_file})
185        set(_SOURCE_FILE ${scheme_file})
186      endif()
187      get_filename_component(name ${scheme_file} NAME)
188      set(_OUTPUT_FILE ${_LINK_DIR}/${name})
189      if(NOT EXISTS ${_OUTPUT_FILE})
190        list(APPEND _SCHEME_LINKS ${_OUTPUT_FILE})
191        add_custom_command(
192            OUTPUT ${_OUTPUT_FILE}
193            COMMAND ${CMAKE_COMMAND} -E ${_CMD} ${_SOURCE_FILE} ${_OUTPUT_FILE}
194        )
195      endif()
196    endforeach(scheme_file)
197    set(TARGET_LINKS ${_TARGET}-links)
198    add_custom_target(${TARGET_LINKS} ALL DEPENDS ${_SCHEME_LINKS})
199  endif()
200
201  # Construct the guile source and compiled load paths
202  set(_GUILE_LOAD_PATH "${current_srcdir}" "${current_bindir}" "${current_bindir}/deprecated")
203  set(_GUILE_LOAD_COMPILED_PATH "${current_bindir}")
204  # VERSION_GREATER_EQUAL introduced in CMake 3.7.
205  if(MINGW64 AND (${GUILE_EFFECTIVE_VERSION} VERSION_GREATER_EQUAL 2.2))
206    if (NOT (DEFINED ENV{GUILE_LOAD_PATH} AND DEFINED ENV{GUILE_LOAD_COMPILED_PATH}))
207      message(FATAL_ERROR "$GUILE_LOAD_PATH and $GUILE_LOAD_COMPILED_PATH must be defined in the environment to configure GnuCash on Microsoft Windows.")
208    endif()
209    file(TO_CMAKE_PATH $ENV{GUILE_LOAD_PATH} guile_load_path)
210    file(TO_CMAKE_PATH $ENV{GUILE_LOAD_COMPILED_PATH} guile_load_compiled_path)
211    list(APPEND _GUILE_LOAD_PATH ${guile_load_path})
212    list(APPEND _GUILE_LOAD_COMPILED_PATH ${guile_load_compiled_path})
213  endif()
214  set(_GUILE_CACHE_DIR "${CMAKE_BINARY_DIR}/${GUILE_REL_UNIX_SITECCACHEDIR}")
215  list(APPEND _GUILE_LOAD_PATH "${CMAKE_BINARY_DIR}/${GUILE_REL_UNIX_SITEDIR}")
216  list(APPEND _GUILE_LOAD_COMPILED_PATH ${_GUILE_CACHE_DIR}
217              "${CMAKE_BINARY_DIR}/${GUILE_REL_UNIX_SITECCACHEDIR}/gnucash/deprecated")
218
219  set(_TARGET_FILES "")
220
221  foreach(source_file ${SCHEME_TGT_SOURCES})
222      set(guile_depends ${SCHEME_TGT_DEPENDS})
223      get_filename_component(basename ${source_file} NAME_WE)
224
225      set(output_file ${basename}.go)
226      set(_TMP_OUTPUT_DIR ${SCHEME_TGT_OUTPUT_DIR})
227      if (_TMP_OUTPUT_DIR)
228        set(output_file ${SCHEME_TGT_OUTPUT_DIR}/${basename}.go)
229      endif()
230      set(output_file ${_GUILE_CACHE_DIR}/${output_file})
231      list(APPEND _TARGET_FILES ${output_file})
232
233      set(source_file_abs_path ${CMAKE_CURRENT_SOURCE_DIR}/${source_file})
234      if (IS_ABSOLUTE ${source_file})
235        set(source_file_abs_path ${source_file})
236      endif()
237      if (__DEBUG)
238        message("add_custom_command: output = ${output_file}")
239      endif()
240      if (MINGW64)
241        set(fpath "")
242        file(TO_CMAKE_PATH "$ENV{PATH}" fpath)
243        set(LIBRARY_PATH "PATH=${BINDIR_BUILD};${fpath}")
244      else()
245        set (LIBRARY_PATH "LD_LIBRARY_PATH=${LIBDIR_BUILD}:${LIBDIR_BUILD}/gnucash:$ENV{LD_LIBRARY_PATH}")
246      endif()
247      if (APPLE)
248        set (LIBRARY_PATH "DYLD_LIBRARY_PATH=${LIBDIR_BUILD}:${LIBDIR_BUILD}/gnucash:$ENV{DYLD_LIBRARY_PATH}")
249      endif()
250      set(_GNC_MODULE_PATH "")
251      if(MINGW64)
252        set(_GNC_MODULE_PATH "${build_bindir}")
253      else()
254        set(_GNC_MODULE_PATH "${LIBDIR_BUILD}" "${LIBDIR_BUILD}/gnucash" "${GNC_MODULE_PATH}")
255      endif()
256      if(NOT MINGW64 OR ${GUILE_EFFECTIVE_VERSION} VERSION_LESS 2.2)
257        make_unix_path_list(_GUILE_LOAD_PATH)
258        make_unix_path_list(_GUILE_LOAD_COMPILED_PATH)
259      endif()
260      make_unix_path_list(_GNC_MODULE_PATH)
261      if (__DEBUG)
262        message("  ")
263        message("   LIBRARY_PATH: ${LIBRARY_PATH}")
264        message("   GUILE_LOAD_PATH: ${_GUILE_LOAD_PATH}")
265        message("   GUILE_LOAD_COMPILED_PATH: ${_GUILE_LOAD_COMPILED_PATH}")
266        message("   GNC_MODULE_PATH: ${_GNC_MODULE_PATH}")
267      endif()
268      #We quote the arguments to stop CMake stripping the path separators.
269      add_custom_command(
270        OUTPUT ${output_file}
271        COMMAND ${CMAKE_COMMAND} -E env
272            "${LIBRARY_PATH}"
273            "GNC_UNINSTALLED=YES"
274            "GNC_BUILDDIR=${CMAKE_BINARY_DIR}"
275            "GUILE_LOAD_PATH=${_GUILE_LOAD_PATH}"
276            "GUILE_LOAD_COMPILED_PATH=${_GUILE_LOAD_COMPILED_PATH}"
277            "GNC_MODULE_PATH=${_GNC_MODULE_PATH}"
278            ${GUILE_EXECUTABLE} -e "\(@@ \(guild\) main\)" -s ${GUILD_EXECUTABLE} compile -o ${output_file} ${source_file_abs_path}
279        DEPENDS ${guile_depends}
280        MAIN_DEPENDENCY ${source_file_abs_path}
281        VERBATIM
282        )
283  endforeach(source_file)
284  if (__DEBUG)
285    message("TARGET_FILES are ${_TARGET_FILES}")
286  endif()
287  add_custom_target(${_TARGET} ALL DEPENDS ${_TARGET_FILES} ${TARGET_LINKS})
288
289  set(_TARGET_FILES "${_TARGET_FILES}" PARENT_SCOPE)
290
291  if(SCHEME_TGT_TEST)
292    add_dependencies(check ${_TARGET})
293  else()
294    install(FILES ${_TARGET_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/${GUILE_REL_SITECCACHEDIR}/${SCHEME_TGT_OUTPUT_DIR})
295    install(FILES ${SCHEME_TGT_SOURCES} DESTINATION ${CMAKE_INSTALL_PREFIX}/${GUILE_REL_SITEDIR}/${SCHEME_TGT_OUTPUT_DIR})
296  endif()
297endfunction()
298
299
300# gnc_add_scheme_test_targets (target
301#                              SOURCES source1 source2 ...
302#                              OUTPUT_DIR directory
303#                              [DEPENDS depedency1 dependency2 ...]
304#                              [MAKE_LINKS])
305#
306# Calls gnc_add_scheme_targets with the TEST keyword set
307# See that function's description for more details.
308function(gnc_add_scheme_test_targets _TARGET)
309  gnc_add_scheme_targets(${_TARGET} ${ARGN} TEST)
310endfunction()
311
312# gnc_add_scheme_deprecated_module (OLD_MODULE old_module_name
313#                                   [NEW_MODULE new_module_name
314#                                    DEPENDS new_module_target]
315#                                   [MESSAGE msg_string])
316#
317# Function to write boilerplate code for deprecated guile modules
318# such that invocation of the old module will emit a deprecation warning
319# message.
320#
321# All but the OLD_MODULE keyword are optional
322#
323# OLD_MODULE and NEW_MODULE should be passed in the form
324# "gnucash mod parts"
325#
326# If NEW_MODULE is set that module will be loaded instead of the
327# deprecated module.
328# If NEW_MODULE is set, DEPENDS should be set to the target for which
329# that module is a source file.
330# For example module (gnucash reports standard transaction)
331# is defined in transaction.scm, which is a source file for
332# cmake target scm-reports-standard so that should be set as DEPENDS.
333#
334# If MESSAGE is left blank, the module will emit a generic message.
335# Otherwise MESSAGE will be emitted.
336function(gnc_add_scheme_deprecated_module)
337
338    set(singleValues OLD_MODULE NEW_MODULE MESSAGE)
339    set(multiValues DEPENDS)
340    cmake_parse_arguments(DM "" "${singleValues}" "${multiValues}" ${ARGN})
341
342    string(STRIP DM_OLD_MODULE "${DM_OLD_MODULE}")
343    string(REPLACE " " "-" _TARGET ${DM_OLD_MODULE})
344    set(_TARGET "scm-deprecated-${_TARGET}")
345
346    string(REPLACE " " ";" MODPARTS "${DM_OLD_MODULE}")
347    list(GET MODPARTS -1 DEPFILENAME)
348    set(SOURCEFILE "${CMAKE_CURRENT_BINARY_DIR}/deprecated/${DEPFILENAME}.scm")
349
350    string(FIND "${DM_OLD_MODULE}" ${DEPFILENAME} POS REVERSE)
351    if (${POS} LESS 2)
352        set(MODPATH "gnucash/deprecated")
353    else()
354        list(REMOVE_AT MODPARTS -1)
355        string(REPLACE ";" "/" MODPATH "${MODPARTS}")
356        set(MODPATH "gnucash/deprecated/${MODPATH}")
357    endif()
358
359    set(DEPPREFIX "* WARN <gnc-guile-deprecation> *: ")
360    if (DM_MESSAGE)
361        set(DEPWARNING "(issue-deprecation-warning \"${DEPPREFIX}${DM_MESSAGE}\")")
362    else()
363        set(DEPWARNING
364            "(issue-deprecation-warning \"${DEPPREFIX}Module '(${DM_OLD_MODULE})' has been deprecated and will be removed in the future.\")")
365        if (DM_NEW_MODULE)
366            set(DEPWARNING "${DEPWARNING}
367                (issue-deprecation-warning \"${DEPPREFIX}Use module '(${DM_NEW_MODULE})' instead.\")")
368        endif()
369    endif()
370
371    # Write the stub file
372    file(WRITE ${SOURCEFILE} "
373;; ${DEPFILENAME}.scm
374;; Compatibility module for deprecated (${DM_OLD_MODULE}).
375;; This file is autogenerated, do not modify by hand.
376
377(define-module (${DM_OLD_MODULE}))
378
379${DEPWARNING}
380")
381
382    if (DM_NEW_MODULE)
383        file(APPEND ${SOURCEFILE} "
384(use-modules (${DM_NEW_MODULE}))
385
386(let ((i (module-public-interface (current-module))))
387     (module-use! i (resolve-interface '(${DM_NEW_MODULE}))))")
388    endif()
389
390    gnc_add_scheme_targets("${_TARGET}"
391                           SOURCES "${SOURCEFILE}"
392                           OUTPUT_DIR "${MODPATH}"
393                           DEPENDS "${DM_DEPENDS}")
394endfunction()
395