1# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2# file Copyright.txt or https://cmake.org/licensing for details.
3
4#[=======================================================================[.rst:
5ExternalData
6------------
7
8.. only:: html
9
10   .. contents::
11
12Manage data files stored outside source tree
13
14Introduction
15^^^^^^^^^^^^
16
17Use this module to unambiguously reference data files stored outside
18the source tree and fetch them at build time from arbitrary local and
19remote content-addressed locations.  Functions provided by this module
20recognize arguments with the syntax ``DATA{<name>}`` as references to
21external data, replace them with full paths to local copies of those
22data, and create build rules to fetch and update the local copies.
23
24For example:
25
26.. code-block:: cmake
27
28 include(ExternalData)
29 set(ExternalData_URL_TEMPLATES "file:///local/%(algo)/%(hash)"
30                                "file:////host/share/%(algo)/%(hash)"
31                                "http://data.org/%(algo)/%(hash)")
32 ExternalData_Add_Test(MyData
33   NAME MyTest
34   COMMAND MyExe DATA{MyInput.png}
35   )
36 ExternalData_Add_Target(MyData)
37
38When test ``MyTest`` runs the ``DATA{MyInput.png}`` argument will be
39replaced by the full path to a real instance of the data file
40``MyInput.png`` on disk.  If the source tree contains a content link
41such as ``MyInput.png.md5`` then the ``MyData`` target creates a real
42``MyInput.png`` in the build tree.
43
44Module Functions
45^^^^^^^^^^^^^^^^
46
47.. command:: ExternalData_Expand_Arguments
48
49  The ``ExternalData_Expand_Arguments`` function evaluates ``DATA{}``
50  references in its arguments and constructs a new list of arguments::
51
52    ExternalData_Expand_Arguments(
53      <target>   # Name of data management target
54      <outVar>   # Output variable
55      [args...]  # Input arguments, DATA{} allowed
56      )
57
58  It replaces each ``DATA{}`` reference in an argument with the full path of
59  a real data file on disk that will exist after the ``<target>`` builds.
60
61.. command:: ExternalData_Add_Test
62
63  The ``ExternalData_Add_Test`` function wraps around the CMake
64  :command:`add_test` command but supports ``DATA{}`` references in
65  its arguments::
66
67    ExternalData_Add_Test(
68      <target>   # Name of data management target
69      ...        # Arguments of add_test(), DATA{} allowed
70      )
71
72  It passes its arguments through ``ExternalData_Expand_Arguments`` and then
73  invokes the :command:`add_test` command using the results.
74
75.. command:: ExternalData_Add_Target
76
77  The ``ExternalData_Add_Target`` function creates a custom target to
78  manage local instances of data files stored externally::
79
80    ExternalData_Add_Target(
81      <target>                  # Name of data management target
82      [SHOW_PROGRESS <ON|OFF>]  # Show progress during the download
83      )
84
85  It creates custom commands in the target as necessary to make data
86  files available for each ``DATA{}`` reference previously evaluated by
87  other functions provided by this module.
88  Data files may be fetched from one of the URL templates specified in
89  the ``ExternalData_URL_TEMPLATES`` variable, or may be found locally
90  in one of the paths specified in the ``ExternalData_OBJECT_STORES``
91  variable.
92
93  .. versionadded:: 3.20
94    The ``SHOW_PROGRESS`` argument may be passed to suppress progress information
95    during the download of objects. If not provided, it defaults to ``OFF`` for
96    :generator:`Ninja` and :generator:`Ninja Multi-Config` generators and ``ON``
97    otherwise.
98
99  Typically only one target is needed to manage all external data within
100  a project.  Call this function once at the end of configuration after
101  all data references have been processed.
102
103Module Variables
104^^^^^^^^^^^^^^^^
105
106The following variables configure behavior.  They should be set before
107calling any of the functions provided by this module.
108
109.. variable:: ExternalData_BINARY_ROOT
110
111  The ``ExternalData_BINARY_ROOT`` variable may be set to the directory to
112  hold the real data files named by expanded ``DATA{}`` references.  The
113  default is ``CMAKE_BINARY_DIR``.  The directory layout will mirror that of
114  content links under ``ExternalData_SOURCE_ROOT``.
115
116.. variable:: ExternalData_CUSTOM_SCRIPT_<key>
117
118  .. versionadded:: 3.2
119
120  Specify a full path to a ``.cmake`` custom fetch script identified by
121  ``<key>`` in entries of the ``ExternalData_URL_TEMPLATES`` list.
122  See `Custom Fetch Scripts`_.
123
124.. variable:: ExternalData_LINK_CONTENT
125
126  The ``ExternalData_LINK_CONTENT`` variable may be set to the name of a
127  supported hash algorithm to enable automatic conversion of real data
128  files referenced by the ``DATA{}`` syntax into content links.  For each
129  such ``<file>`` a content link named ``<file><ext>`` is created.  The
130  original file is renamed to the form ``.ExternalData_<algo>_<hash>`` to
131  stage it for future transmission to one of the locations in the list
132  of URL templates (by means outside the scope of this module).  The
133  data fetch rule created for the content link will use the staged
134  object if it cannot be found using any URL template.
135
136.. variable:: ExternalData_NO_SYMLINKS
137
138  .. versionadded:: 3.3
139
140  The real data files named by expanded ``DATA{}`` references may be made
141  available under ``ExternalData_BINARY_ROOT`` using symbolic links on
142  some platforms.  The ``ExternalData_NO_SYMLINKS`` variable may be set
143  to disable use of symbolic links and enable use of copies instead.
144
145.. variable:: ExternalData_OBJECT_STORES
146
147  The ``ExternalData_OBJECT_STORES`` variable may be set to a list of local
148  directories that store objects using the layout ``<dir>/%(algo)/%(hash)``.
149  These directories will be searched first for a needed object.  If the
150  object is not available in any store then it will be fetched remotely
151  using the URL templates and added to the first local store listed.  If
152  no stores are specified the default is a location inside the build
153  tree.
154
155.. variable:: ExternalData_SERIES_PARSE
156              ExternalData_SERIES_PARSE_PREFIX
157              ExternalData_SERIES_PARSE_NUMBER
158              ExternalData_SERIES_PARSE_SUFFIX
159              ExternalData_SERIES_MATCH
160
161  See `Referencing File Series`_.
162
163.. variable:: ExternalData_SOURCE_ROOT
164
165  The ``ExternalData_SOURCE_ROOT`` variable may be set to the highest source
166  directory containing any path named by a ``DATA{}`` reference.  The
167  default is ``CMAKE_SOURCE_DIR``.  ``ExternalData_SOURCE_ROOT`` and
168  ``CMAKE_SOURCE_DIR`` must refer to directories within a single source
169  distribution (e.g.  they come together in one tarball).
170
171.. variable:: ExternalData_TIMEOUT_ABSOLUTE
172
173  The ``ExternalData_TIMEOUT_ABSOLUTE`` variable sets the download
174  absolute timeout, in seconds, with a default of ``300`` seconds.
175  Set to ``0`` to disable enforcement.
176
177.. variable:: ExternalData_TIMEOUT_INACTIVITY
178
179  The ``ExternalData_TIMEOUT_INACTIVITY`` variable sets the download
180  inactivity timeout, in seconds, with a default of ``60`` seconds.
181  Set to ``0`` to disable enforcement.
182
183.. variable:: ExternalData_URL_ALGO_<algo>_<key>
184
185  .. versionadded:: 3.3
186
187  Specify a custom URL component to be substituted for URL template
188  placeholders of the form ``%(algo:<key>)``, where ``<key>`` is a
189  valid C identifier, when fetching an object referenced via hash
190  algorithm ``<algo>``.  If not defined, the default URL component
191  is just ``<algo>`` for any ``<key>``.
192
193.. variable:: ExternalData_URL_TEMPLATES
194
195  The ``ExternalData_URL_TEMPLATES`` may be set to provide a list
196  of URL templates using the placeholders ``%(algo)`` and ``%(hash)``
197  in each template.  Data fetch rules try each URL template in order
198  by substituting the hash algorithm name for ``%(algo)`` and the hash
199  value for ``%(hash)``.  Alternatively one may use ``%(algo:<key>)``
200  with ``ExternalData_URL_ALGO_<algo>_<key>`` variables to gain more
201  flexibility in remote URLs.
202
203Referencing Files
204^^^^^^^^^^^^^^^^^
205
206Referencing Single Files
207""""""""""""""""""""""""
208
209The ``DATA{}`` syntax is literal and the ``<name>`` is a full or relative path
210within the source tree.  The source tree must contain either a real
211data file at ``<name>`` or a "content link" at ``<name><ext>`` containing a
212hash of the real file using a hash algorithm corresponding to ``<ext>``.
213For example, the argument ``DATA{img.png}`` may be satisfied by either a
214real ``img.png`` file in the current source directory or a ``img.png.md5``
215file containing its MD5 sum.
216
217.. versionadded:: 3.8
218  Multiple content links of the same name with different hash algorithms
219  are supported (e.g. ``img.png.sha256`` and ``img.png.sha1``) so long as
220  they all correspond to the same real file.  This allows objects to be
221  fetched from sources indexed by different hash algorithms.
222
223Referencing File Series
224"""""""""""""""""""""""
225
226The ``DATA{}`` syntax can be told to fetch a file series using the form
227``DATA{<name>,:}``, where the ``:`` is literal.  If the source tree
228contains a group of files or content links named like a series then a
229reference to one member adds rules to fetch all of them.  Although all
230members of a series are fetched, only the file originally named by the
231``DATA{}`` argument is substituted for it.  The default configuration
232recognizes file series names ending with ``#.ext``, ``_#.ext``, ``.#.ext``,
233or ``-#.ext`` where ``#`` is a sequence of decimal digits and ``.ext`` is
234any single extension.  Configure it with a regex that parses ``<number>``
235and ``<suffix>`` parts from the end of ``<name>``::
236
237 ExternalData_SERIES_PARSE = regex of the form (<number>)(<suffix>)$
238
239For more complicated cases set::
240
241 ExternalData_SERIES_PARSE = regex with at least two () groups
242 ExternalData_SERIES_PARSE_PREFIX = <prefix> regex group number, if any
243 ExternalData_SERIES_PARSE_NUMBER = <number> regex group number
244 ExternalData_SERIES_PARSE_SUFFIX = <suffix> regex group number
245
246Configure series number matching with a regex that matches the
247``<number>`` part of series members named ``<prefix><number><suffix>``::
248
249 ExternalData_SERIES_MATCH = regex matching <number> in all series members
250
251Note that the ``<suffix>`` of a series does not include a hash-algorithm
252extension.
253
254Referencing Associated Files
255""""""""""""""""""""""""""""
256
257The ``DATA{}`` syntax can alternatively match files associated with the
258named file and contained in the same directory.  Associated files may
259be specified by options using the syntax
260``DATA{<name>,<opt1>,<opt2>,...}``.  Each option may specify one file by
261name or specify a regular expression to match file names using the
262syntax ``REGEX:<regex>``.  For example, the arguments::
263
264 DATA{MyData/MyInput.mhd,MyInput.img}                   # File pair
265 DATA{MyData/MyFrames00.png,REGEX:MyFrames[0-9]+\\.png} # Series
266
267will pass ``MyInput.mha`` and ``MyFrames00.png`` on the command line but
268ensure that the associated files are present next to them.
269
270Referencing Directories
271"""""""""""""""""""""""
272
273The ``DATA{}`` syntax may reference a directory using a trailing slash and
274a list of associated files.  The form ``DATA{<name>/,<opt1>,<opt2>,...}``
275adds rules to fetch any files in the directory that match one of the
276associated file options.  For example, the argument
277``DATA{MyDataDir/,REGEX:.*}`` will pass the full path to a ``MyDataDir``
278directory on the command line and ensure that the directory contains
279files corresponding to every file or content link in the ``MyDataDir``
280source directory.
281
282.. versionadded:: 3.3
283  In order to match associated files in subdirectories,
284  specify a ``RECURSE:`` option, e.g. ``DATA{MyDataDir/,RECURSE:,REGEX:.*}``.
285
286Hash Algorithms
287^^^^^^^^^^^^^^^
288
289The following hash algorithms are supported::
290
291 %(algo)     <ext>     Description
292 -------     -----     -----------
293 MD5         .md5      Message-Digest Algorithm 5, RFC 1321
294 SHA1        .sha1     US Secure Hash Algorithm 1, RFC 3174
295 SHA224      .sha224   US Secure Hash Algorithms, RFC 4634
296 SHA256      .sha256   US Secure Hash Algorithms, RFC 4634
297 SHA384      .sha384   US Secure Hash Algorithms, RFC 4634
298 SHA512      .sha512   US Secure Hash Algorithms, RFC 4634
299 SHA3_224    .sha3-224 Keccak SHA-3
300 SHA3_256    .sha3-256 Keccak SHA-3
301 SHA3_384    .sha3-384 Keccak SHA-3
302 SHA3_512    .sha3-512 Keccak SHA-3
303
304.. versionadded:: 3.8
305  Added the ``SHA3_*`` hash algorithms.
306
307Note that the hashes are used only for unique data identification and
308download verification.
309
310.. _`ExternalData Custom Fetch Scripts`:
311
312Custom Fetch Scripts
313^^^^^^^^^^^^^^^^^^^^
314
315.. versionadded:: 3.2
316
317When a data file must be fetched from one of the URL templates
318specified in the ``ExternalData_URL_TEMPLATES`` variable, it is
319normally downloaded using the :command:`file(DOWNLOAD)` command.
320One may specify usage of a custom fetch script by using a URL
321template of the form ``ExternalDataCustomScript://<key>/<loc>``.
322The ``<key>`` must be a C identifier, and the ``<loc>`` must
323contain the ``%(algo)`` and ``%(hash)`` placeholders.
324A variable corresponding to the key, ``ExternalData_CUSTOM_SCRIPT_<key>``,
325must be set to the full path to a ``.cmake`` script file.  The script
326will be included to perform the actual fetch, and provided with
327the following variables:
328
329.. variable:: ExternalData_CUSTOM_LOCATION
330
331  When a custom fetch script is loaded, this variable is set to the
332  location part of the URL, which will contain the substituted hash
333  algorithm name and content hash value.
334
335.. variable:: ExternalData_CUSTOM_FILE
336
337  When a custom fetch script is loaded, this variable is set to the
338  full path to a file in which the script must store the fetched
339  content.  The name of the file is unspecified and should not be
340  interpreted in any way.
341
342The custom fetch script is expected to store fetched content in the
343file or set a variable:
344
345.. variable:: ExternalData_CUSTOM_ERROR
346
347  When a custom fetch script fails to fetch the requested content,
348  it must set this variable to a short one-line message describing
349  the reason for failure.
350
351#]=======================================================================]
352
353function(ExternalData_add_test target)
354  # Expand all arguments as a single string to preserve escaped semicolons.
355  ExternalData_expand_arguments("${target}" testArgs "${ARGN}")
356  add_test(${testArgs})
357endfunction()
358
359function(ExternalData_add_target target)
360  if(NOT ExternalData_URL_TEMPLATES AND NOT ExternalData_OBJECT_STORES)
361    message(FATAL_ERROR
362      "Neither ExternalData_URL_TEMPLATES nor ExternalData_OBJECT_STORES is set!")
363  endif()
364  if(NOT ExternalData_OBJECT_STORES)
365    set(ExternalData_OBJECT_STORES ${CMAKE_BINARY_DIR}/ExternalData/Objects)
366  endif()
367  set(_ExternalData_CONFIG_CODE "")
368
369  cmake_parse_arguments(PARSE_ARGV 1 _ExternalData_add_target
370    ""
371    "SHOW_PROGRESS"
372    "")
373  if (_ExternalData_add_target_UNPARSED_ARGUMENTS)
374    message(AUTHOR_WARNING
375      "Ignoring unrecognized arguments passed to ExternalData_add_target: "
376      "`${_ExternalData_add_target_UNPARSED_ARGUMENTS}`")
377  endif ()
378
379  # Turn `SHOW_PROGRESS` into a boolean
380  if (NOT DEFINED _ExternalData_add_target_SHOW_PROGRESS)
381    # The default setting
382    if (CMAKE_GENERATOR MATCHES "Ninja")
383      set(_ExternalData_add_target_SHOW_PROGRESS OFF)
384    else ()
385      set(_ExternalData_add_target_SHOW_PROGRESS ON)
386    endif ()
387  elseif (_ExternalData_add_target_SHOW_PROGRESS)
388    set(_ExternalData_add_target_SHOW_PROGRESS ON)
389  else ()
390    set(_ExternalData_add_target_SHOW_PROGRESS OFF)
391  endif ()
392
393  # Store custom script configuration.
394  foreach(url_template IN LISTS ExternalData_URL_TEMPLATES)
395    if("${url_template}" MATCHES "^ExternalDataCustomScript://([^/]*)/(.*)$")
396      set(key "${CMAKE_MATCH_1}")
397      if(key MATCHES "^[A-Za-z_][A-Za-z0-9_]*$")
398        if(ExternalData_CUSTOM_SCRIPT_${key})
399          if(IS_ABSOLUTE "${ExternalData_CUSTOM_SCRIPT_${key}}")
400            string(CONCAT _ExternalData_CONFIG_CODE "${_ExternalData_CONFIG_CODE}\n"
401              "set(ExternalData_CUSTOM_SCRIPT_${key} \"${ExternalData_CUSTOM_SCRIPT_${key}}\")")
402          else()
403            message(FATAL_ERROR
404              "No ExternalData_CUSTOM_SCRIPT_${key} is not set to a full path:\n"
405              " ${ExternalData_CUSTOM_SCRIPT_${key}}")
406          endif()
407        else()
408          message(FATAL_ERROR
409            "No ExternalData_CUSTOM_SCRIPT_${key} is set for URL template:\n"
410            " ${url_template}")
411        endif()
412      else()
413        message(FATAL_ERROR
414          "Bad ExternalDataCustomScript key '${key}' in URL template:\n"
415          " ${url_template}\n"
416          "The key must be a valid C identifier.")
417      endif()
418    endif()
419
420    # Store custom algorithm name to URL component maps.
421    if("${url_template}" MATCHES "%\\(algo:([^)]*)\\)")
422      set(key "${CMAKE_MATCH_1}")
423      if(key MATCHES "^[A-Za-z_][A-Za-z0-9_]*$")
424        string(REPLACE "|" ";" _algos "${_ExternalData_REGEX_ALGO}")
425        foreach(algo ${_algos})
426          if(DEFINED ExternalData_URL_ALGO_${algo}_${key})
427            string(CONCAT _ExternalData_CONFIG_CODE "${_ExternalData_CONFIG_CODE}\n"
428              "set(ExternalData_URL_ALGO_${algo}_${key} \"${ExternalData_URL_ALGO_${algo}_${key}}\")")
429          endif()
430        endforeach()
431      else()
432        message(FATAL_ERROR
433          "Bad %(algo:${key}) in URL template:\n"
434          " ${url_template}\n"
435          "The transform name must be a valid C identifier.")
436      endif()
437    endif()
438  endforeach()
439
440  # Store configuration for use by build-time script.
441  set(config ${CMAKE_CURRENT_BINARY_DIR}/${target}_config.cmake)
442  configure_file(${_ExternalData_SELF_DIR}/ExternalData_config.cmake.in ${config} @ONLY)
443
444  set(files "")
445
446  # Set a "_ExternalData_FILE_${file}" variable for each output file to avoid
447  # duplicate entries within this target.  Set a directory property of the same
448  # name to avoid repeating custom commands with the same output in this directory.
449  # Repeating custom commands with the same output across directories or across
450  # targets in the same directory may be a race, but this is likely okay because
451  # we use atomic replacement of output files.
452  #
453  # Use local data first to prefer real files over content links.
454
455  # Custom commands to copy or link local data.
456  get_property(data_local GLOBAL PROPERTY _ExternalData_${target}_LOCAL)
457  foreach(entry IN LISTS data_local)
458    string(REPLACE "|" ";" tuple "${entry}")
459    list(GET tuple 0 file)
460    list(GET tuple 1 name)
461    if(NOT DEFINED "_ExternalData_FILE_${file}")
462      set("_ExternalData_FILE_${file}" 1)
463      get_property(added DIRECTORY PROPERTY "_ExternalData_FILE_${file}")
464      if(NOT added)
465        set_property(DIRECTORY PROPERTY "_ExternalData_FILE_${file}" 1)
466        add_custom_command(
467          COMMENT "Generating ${file}"
468          OUTPUT "${file}"
469          COMMAND ${CMAKE_COMMAND} -Drelative_top=${CMAKE_BINARY_DIR}
470                                   -Dfile=${file} -Dname=${name}
471                                   -DExternalData_ACTION=local
472                                   -DExternalData_SHOW_PROGRESS=${_ExternalData_add_target_SHOW_PROGRESS}
473                                   -DExternalData_CONFIG=${config}
474                                   -P ${_ExternalData_SELF}
475          MAIN_DEPENDENCY "${name}"
476          )
477      endif()
478      list(APPEND files "${file}")
479    endif()
480  endforeach()
481
482  # Custom commands to fetch remote data.
483  get_property(data_fetch GLOBAL PROPERTY _ExternalData_${target}_FETCH)
484  foreach(entry IN LISTS data_fetch)
485    string(REPLACE "|" ";" tuple "${entry}")
486    list(GET tuple 0 file)
487    list(GET tuple 1 name)
488    list(GET tuple 2 exts)
489    string(REPLACE "+" ";" exts_list "${exts}")
490    list(GET exts_list 0 first_ext)
491    set(stamp "-hash-stamp")
492    if(NOT DEFINED "_ExternalData_FILE_${file}")
493      set("_ExternalData_FILE_${file}" 1)
494      get_property(added DIRECTORY PROPERTY "_ExternalData_FILE_${file}")
495      if(NOT added)
496        set_property(DIRECTORY PROPERTY "_ExternalData_FILE_${file}" 1)
497        add_custom_command(
498          # Users care about the data file, so hide the hash/timestamp file.
499          COMMENT "Generating ${file}"
500          # The hash/timestamp file is the output from the build perspective.
501          # List the real file as a second output in case it is a broken link.
502          # The files must be listed in this order so CMake can hide from the
503          # make tool that a symlink target may not be newer than the input.
504          OUTPUT "${file}${stamp}" "${file}"
505          # Run the data fetch/update script.
506          COMMAND ${CMAKE_COMMAND} -Drelative_top=${CMAKE_BINARY_DIR}
507                                   -Dfile=${file} -Dname=${name} -Dexts=${exts}
508                                   -DExternalData_ACTION=fetch
509                                   -DExternalData_SHOW_PROGRESS=${_ExternalData_add_target_SHOW_PROGRESS}
510                                   -DExternalData_CONFIG=${config}
511                                   -P ${_ExternalData_SELF}
512          # Update whenever the object hash changes.
513          MAIN_DEPENDENCY "${name}${first_ext}"
514          )
515      endif()
516      list(APPEND files "${file}${stamp}")
517    endif()
518  endforeach()
519
520  # Custom target to drive all update commands.
521  add_custom_target(${target} ALL DEPENDS ${files})
522endfunction()
523
524function(ExternalData_expand_arguments target outArgsVar)
525  # Replace DATA{} references with real arguments.
526  set(data_regex "DATA{([^;{}\r\n]*)}")
527  set(other_regex "([^D]|D[^A]|DA[^T]|DAT[^A]|DATA[^{])+|.")
528  set(outArgs "")
529  # This list expansion un-escapes semicolons in list element values so we
530  # must re-escape them below anywhere a new list expansion will occur.
531  foreach(arg IN LISTS ARGN)
532    if("x${arg}" MATCHES "${data_regex}")
533      # Re-escape in-value semicolons before expansion in foreach below.
534      string(REPLACE ";" "\\;" tmp "${arg}")
535      # Split argument into DATA{}-pieces and other pieces.
536      string(REGEX MATCHALL "${data_regex}|${other_regex}" pieces "${tmp}")
537      # Compose output argument with DATA{}-pieces replaced.
538      set(outArg "")
539      foreach(piece IN LISTS pieces)
540        if("x${piece}" MATCHES "^x${data_regex}$")
541          # Replace this DATA{}-piece with a file path.
542          _ExternalData_arg("${target}" "${piece}" "${CMAKE_MATCH_1}" file)
543          string(APPEND outArg "${file}")
544        else()
545          # No replacement needed for this piece.
546          string(APPEND outArg "${piece}")
547        endif()
548      endforeach()
549    else()
550      # No replacements needed in this argument.
551      set(outArg "${arg}")
552    endif()
553    # Re-escape in-value semicolons in resulting list.
554    string(REPLACE ";" "\\;" outArg "${outArg}")
555    list(APPEND outArgs "${outArg}")
556  endforeach()
557  set("${outArgsVar}" "${outArgs}" PARENT_SCOPE)
558endfunction()
559
560#-----------------------------------------------------------------------------
561# Private helper interface
562
563set(_ExternalData_REGEX_ALGO "MD5|SHA1|SHA224|SHA256|SHA384|SHA512|SHA3_224|SHA3_256|SHA3_384|SHA3_512")
564set(_ExternalData_REGEX_EXT "md5|sha1|sha224|sha256|sha384|sha512|sha3-224|sha3-256|sha3-384|sha3-512")
565set(_ExternalData_SELF "${CMAKE_CURRENT_LIST_FILE}")
566get_filename_component(_ExternalData_SELF_DIR "${_ExternalData_SELF}" PATH)
567
568function(_ExternalData_compute_hash var_hash algo file)
569  if("${algo}" MATCHES "^${_ExternalData_REGEX_ALGO}$")
570    file("${algo}" "${file}" hash)
571    set("${var_hash}" "${hash}" PARENT_SCOPE)
572  else()
573    message(FATAL_ERROR "Hash algorithm ${algo} unimplemented.")
574  endif()
575endfunction()
576
577function(_ExternalData_random var)
578  string(RANDOM LENGTH 6 random)
579  set("${var}" "${random}" PARENT_SCOPE)
580endfunction()
581
582function(_ExternalData_exact_regex regex_var string)
583  string(REGEX REPLACE "([][+.*()^])" "\\\\\\1" regex "${string}")
584  set("${regex_var}" "${regex}" PARENT_SCOPE)
585endfunction()
586
587function(_ExternalData_atomic_write file content)
588  _ExternalData_random(random)
589  set(tmp "${file}.tmp${random}")
590  file(WRITE "${tmp}" "${content}")
591  file(RENAME "${tmp}" "${file}")
592endfunction()
593
594function(_ExternalData_link_content name var_ext)
595  if("${ExternalData_LINK_CONTENT}" MATCHES "^(${_ExternalData_REGEX_ALGO})$")
596    set(algo "${ExternalData_LINK_CONTENT}")
597  else()
598    message(FATAL_ERROR
599      "Unknown hash algorithm specified by ExternalData_LINK_CONTENT:\n"
600      "  ${ExternalData_LINK_CONTENT}")
601  endif()
602  _ExternalData_compute_hash(hash "${algo}" "${name}")
603  get_filename_component(dir "${name}" PATH)
604  set(staged "${dir}/.ExternalData_${algo}_${hash}")
605  string(TOLOWER ".${algo}" ext)
606  _ExternalData_atomic_write("${name}${ext}" "${hash}\n")
607  file(RENAME "${name}" "${staged}")
608  set("${var_ext}" "${ext}" PARENT_SCOPE)
609
610  file(RELATIVE_PATH relname "${ExternalData_SOURCE_ROOT}" "${name}${ext}")
611  message(STATUS "Linked ${relname} to ExternalData ${algo}/${hash}")
612endfunction()
613
614function(_ExternalData_arg target arg options var_file)
615  # Separate data path from the options.
616  string(REPLACE "," ";" options "${options}")
617  list(GET options 0 data)
618  list(REMOVE_AT options 0)
619
620  # Interpret trailing slashes as directories.
621  set(data_is_directory 0)
622  if("x${data}" MATCHES "^x(.*)([/\\])$")
623    set(data_is_directory 1)
624    set(data "${CMAKE_MATCH_1}")
625  endif()
626
627  # Convert to full path.
628  if(IS_ABSOLUTE "${data}")
629    set(absdata "${data}")
630  else()
631    set(absdata "${CMAKE_CURRENT_SOURCE_DIR}/${data}")
632  endif()
633  get_filename_component(absdata "${absdata}" ABSOLUTE)
634
635  # Convert to relative path under the source tree.
636  if(NOT ExternalData_SOURCE_ROOT)
637    set(ExternalData_SOURCE_ROOT "${CMAKE_SOURCE_DIR}")
638  endif()
639  set(top_src "${ExternalData_SOURCE_ROOT}")
640  file(RELATIVE_PATH reldata "${top_src}" "${absdata}")
641  if(IS_ABSOLUTE "${reldata}" OR "${reldata}" MATCHES "^\\.\\./")
642    message(FATAL_ERROR "Data file referenced by argument\n"
643      "  ${arg}\n"
644      "does not lie under the top-level source directory\n"
645      "  ${top_src}\n")
646  endif()
647  if(data_is_directory AND NOT IS_DIRECTORY "${top_src}/${reldata}")
648    message(FATAL_ERROR "Data directory referenced by argument\n"
649      "  ${arg}\n"
650      "corresponds to source tree path\n"
651      "  ${reldata}\n"
652      "that does not exist as a directory!")
653  endif()
654  if(NOT ExternalData_BINARY_ROOT)
655    set(ExternalData_BINARY_ROOT "${CMAKE_BINARY_DIR}")
656  endif()
657  set(top_bin "${ExternalData_BINARY_ROOT}")
658
659  # Handle in-source builds gracefully.
660  if("${top_src}" STREQUAL "${top_bin}")
661    if(ExternalData_LINK_CONTENT)
662      message(WARNING "ExternalData_LINK_CONTENT cannot be used in-source")
663      set(ExternalData_LINK_CONTENT 0)
664    endif()
665    set(top_same 1)
666  endif()
667
668  set(external "") # Entries external to the source tree.
669  set(internal "") # Entries internal to the source tree.
670  set(have_original ${data_is_directory})
671  set(have_original_as_dir 0)
672
673  # Process options.
674  set(series_option "")
675  set(recurse_option "")
676  set(associated_files "")
677  set(associated_regex "")
678  foreach(opt ${options})
679    # Regular expression to match associated files.
680    if("x${opt}" MATCHES "^xREGEX:([^:/]+)$")
681      list(APPEND associated_regex "${CMAKE_MATCH_1}")
682    elseif(opt STREQUAL ":")
683      # Activate series matching.
684      set(series_option "${opt}")
685    elseif(opt STREQUAL "RECURSE:")
686      # Activate recursive matching in directories.
687      set(recurse_option "${opt}")
688    elseif("x${opt}" MATCHES "^[^][:/*?]+$")
689      # Specific associated file.
690      list(APPEND associated_files "${opt}")
691    else()
692      message(FATAL_ERROR "Unknown option \"${opt}\" in argument\n"
693        "  ${arg}\n")
694    endif()
695  endforeach()
696
697  if(series_option)
698    if(data_is_directory)
699      message(FATAL_ERROR "Series option \"${series_option}\" not allowed with directories.")
700    endif()
701    if(associated_files OR associated_regex)
702      message(FATAL_ERROR "Series option \"${series_option}\" not allowed with associated files.")
703    endif()
704    if(recurse_option)
705      message(FATAL_ERROR "Recurse option \"${recurse_option}\" allowed only with directories.")
706    endif()
707    # Load a whole file series.
708    _ExternalData_arg_series()
709  elseif(data_is_directory)
710    if(associated_files OR associated_regex)
711      # Load listed/matching associated files in the directory.
712      _ExternalData_arg_associated()
713    else()
714      message(FATAL_ERROR "Data directory referenced by argument\n"
715        "  ${arg}\n"
716        "must list associated files.")
717    endif()
718  else()
719    if(recurse_option)
720      message(FATAL_ERROR "Recurse option \"${recurse_option}\" allowed only with directories.")
721    endif()
722    # Load the named data file.
723    _ExternalData_arg_single()
724    if(associated_files OR associated_regex)
725      # Load listed/matching associated files.
726      _ExternalData_arg_associated()
727    endif()
728  endif()
729
730  if(NOT have_original)
731    if(have_original_as_dir)
732      set(msg_kind FATAL_ERROR)
733      set(msg "that is directory instead of a file!")
734    else()
735      set(msg_kind AUTHOR_WARNING)
736      set(msg "that does not exist as a file (with or without an extension)!")
737    endif()
738    message(${msg_kind} "Data file referenced by argument\n"
739      "  ${arg}\n"
740      "corresponds to source tree path\n"
741      "  ${reldata}\n"
742      "${msg}")
743  endif()
744
745  if(external)
746    # Make the series available in the build tree.
747    set_property(GLOBAL APPEND PROPERTY
748      _ExternalData_${target}_FETCH "${external}")
749    set_property(GLOBAL APPEND PROPERTY
750      _ExternalData_${target}_LOCAL "${internal}")
751    set("${var_file}" "${top_bin}/${reldata}" PARENT_SCOPE)
752  else()
753    # The whole series is in the source tree.
754    set("${var_file}" "${top_src}/${reldata}" PARENT_SCOPE)
755  endif()
756endfunction()
757
758macro(_ExternalData_arg_associated)
759  # Associated files lie in the same directory.
760  if(data_is_directory)
761    set(reldir "${reldata}")
762  else()
763    get_filename_component(reldir "${reldata}" PATH)
764  endif()
765  if(reldir)
766    string(APPEND reldir "/")
767  endif()
768  _ExternalData_exact_regex(reldir_regex "${reldir}")
769  if(recurse_option)
770    set(glob GLOB_RECURSE)
771    string(APPEND reldir_regex "(.+/)?")
772  else()
773    set(glob GLOB)
774  endif()
775
776  # Find files named explicitly.
777  foreach(file ${associated_files})
778    _ExternalData_exact_regex(file_regex "${file}")
779    _ExternalData_arg_find_files(${glob} "${reldir}${file}"
780      "${reldir_regex}${file_regex}")
781  endforeach()
782
783  # Find files matching the given regular expressions.
784  set(all "")
785  set(sep "")
786  foreach(regex ${associated_regex})
787    string(APPEND all "${sep}${reldir_regex}${regex}")
788    set(sep "|")
789  endforeach()
790  _ExternalData_arg_find_files(${glob} "${reldir}" "${all}")
791endmacro()
792
793macro(_ExternalData_arg_single)
794  # Match only the named data by itself.
795  _ExternalData_exact_regex(data_regex "${reldata}")
796  _ExternalData_arg_find_files(GLOB "${reldata}" "${data_regex}")
797endmacro()
798
799macro(_ExternalData_arg_series)
800  # Configure series parsing and matching.
801  set(series_parse_prefix "")
802  set(series_parse_number "\\1")
803  set(series_parse_suffix "\\2")
804  if(ExternalData_SERIES_PARSE)
805    if(ExternalData_SERIES_PARSE_NUMBER AND ExternalData_SERIES_PARSE_SUFFIX)
806      if(ExternalData_SERIES_PARSE_PREFIX)
807        set(series_parse_prefix "\\${ExternalData_SERIES_PARSE_PREFIX}")
808      endif()
809      set(series_parse_number "\\${ExternalData_SERIES_PARSE_NUMBER}")
810      set(series_parse_suffix "\\${ExternalData_SERIES_PARSE_SUFFIX}")
811    elseif(NOT "x${ExternalData_SERIES_PARSE}" MATCHES "^x\\([^()]*\\)\\([^()]*\\)\\$$")
812      message(FATAL_ERROR
813        "ExternalData_SERIES_PARSE is set to\n"
814        "  ${ExternalData_SERIES_PARSE}\n"
815        "which is not of the form\n"
816        "  (<number>)(<suffix>)$\n"
817        "Fix the regular expression or set variables\n"
818        "  ExternalData_SERIES_PARSE_PREFIX = <prefix> regex group number, if any\n"
819        "  ExternalData_SERIES_PARSE_NUMBER = <number> regex group number\n"
820        "  ExternalData_SERIES_PARSE_SUFFIX = <suffix> regex group number\n"
821        )
822    endif()
823    set(series_parse "${ExternalData_SERIES_PARSE}")
824  else()
825    set(series_parse "([0-9]*)(\\.[^./]*)$")
826  endif()
827  if(ExternalData_SERIES_MATCH)
828    set(series_match "${ExternalData_SERIES_MATCH}")
829  else()
830    set(series_match "[_.-]?[0-9]*")
831  endif()
832
833  # Parse the base, number, and extension components of the series.
834  string(REGEX REPLACE "${series_parse}" "${series_parse_prefix};${series_parse_number};${series_parse_suffix}" tuple "${reldata}")
835  list(LENGTH tuple len)
836  if(NOT "${len}" EQUAL 3)
837    message(FATAL_ERROR "Data file referenced by argument\n"
838      "  ${arg}\n"
839      "corresponds to path\n"
840      "  ${reldata}\n"
841      "that does not match regular expression\n"
842      "  ${series_parse}")
843  endif()
844  list(GET tuple 0 relbase)
845  list(GET tuple 2 ext)
846
847  # Glob files that might match the series.
848  # Then match base, number, and extension.
849  _ExternalData_exact_regex(series_base "${relbase}")
850  _ExternalData_exact_regex(series_ext "${ext}")
851  _ExternalData_arg_find_files(GLOB "${relbase}*${ext}"
852    "${series_base}${series_match}${series_ext}")
853endmacro()
854
855function(_ExternalData_arg_find_files glob pattern regex)
856  cmake_policy(PUSH)
857  cmake_policy(SET CMP0009 NEW)
858  file(${glob} globbed RELATIVE "${top_src}" "${top_src}/${pattern}*")
859  cmake_policy(POP)
860  set(externals_count -1)
861  foreach(entry IN LISTS globbed)
862    if("x${entry}" MATCHES "^x(.*)(\\.(${_ExternalData_REGEX_EXT}))$")
863      set(relname "${CMAKE_MATCH_1}")
864      set(alg "${CMAKE_MATCH_2}")
865    else()
866      set(relname "${entry}")
867      set(alg "")
868    endif()
869    if("x${relname}" MATCHES "^x${regex}$" # matches
870        AND NOT "x${relname}" MATCHES "(^x|/)\\.ExternalData_" # not staged obj
871        )
872      if(IS_DIRECTORY "${top_src}/${entry}")
873        if("${relname}" STREQUAL "${reldata}")
874          set(have_original_as_dir 1)
875        endif()
876      else()
877        set(name "${top_src}/${relname}")
878        set(file "${top_bin}/${relname}")
879        if(alg)
880          if(NOT "${external_${externals_count}_file_name}" STREQUAL "${file}|${name}")
881            math(EXPR externals_count "${externals_count} + 1")
882            set(external_${externals_count}_file_name "${file}|${name}")
883          endif()
884          list(APPEND external_${externals_count}_algs "${alg}")
885        elseif(ExternalData_LINK_CONTENT)
886          _ExternalData_link_content("${name}" alg)
887          list(APPEND external "${file}|${name}|${alg}")
888        elseif(NOT top_same)
889          list(APPEND internal "${file}|${name}")
890        endif()
891        if("${relname}" STREQUAL "${reldata}")
892          set(have_original 1)
893        endif()
894      endif()
895    endif()
896  endforeach()
897  if(${externals_count} GREATER -1)
898    foreach(ii RANGE ${externals_count})
899      string(REPLACE ";" "+" algs_delim "${external_${ii}_algs}")
900      list(APPEND external "${external_${ii}_file_name}|${algs_delim}")
901      unset(external_${ii}_algs)
902      unset(external_${ii}_file_name)
903    endforeach()
904  endif()
905  set(external "${external}" PARENT_SCOPE)
906  set(internal "${internal}" PARENT_SCOPE)
907  set(have_original "${have_original}" PARENT_SCOPE)
908  set(have_original_as_dir "${have_original_as_dir}" PARENT_SCOPE)
909endfunction()
910
911#-----------------------------------------------------------------------------
912# Private script mode interface
913
914if(CMAKE_GENERATOR OR NOT ExternalData_ACTION)
915  return()
916endif()
917
918if(ExternalData_CONFIG)
919  include(${ExternalData_CONFIG})
920endif()
921if(NOT ExternalData_URL_TEMPLATES AND NOT ExternalData_OBJECT_STORES)
922  message(FATAL_ERROR
923    "Neither ExternalData_URL_TEMPLATES nor ExternalData_OBJECT_STORES is set!")
924endif()
925
926function(_ExternalData_link_or_copy src dst)
927  # Create a temporary file first.
928  get_filename_component(dst_dir "${dst}" PATH)
929  file(MAKE_DIRECTORY "${dst_dir}")
930  _ExternalData_random(random)
931  set(tmp "${dst}.tmp${random}")
932  if(UNIX AND NOT ExternalData_NO_SYMLINKS)
933    # Create a symbolic link.
934    set(tgt "${src}")
935    if(relative_top)
936      # Use relative path if files are close enough.
937      file(RELATIVE_PATH relsrc "${relative_top}" "${src}")
938      file(RELATIVE_PATH relfile "${relative_top}" "${dst}")
939      if(NOT IS_ABSOLUTE "${relsrc}" AND NOT "${relsrc}" MATCHES "^\\.\\./" AND
940          NOT IS_ABSOLUTE "${reldst}" AND NOT "${reldst}" MATCHES "^\\.\\./")
941        file(RELATIVE_PATH tgt "${dst_dir}" "${src}")
942      endif()
943    endif()
944    # Create link (falling back to copying if there's a problem).
945    file(CREATE_LINK "${tgt}" "${tmp}" RESULT result COPY_ON_ERROR SYMBOLIC)
946  else()
947    # Create a copy.
948    file(COPY_FILE "${src}" "${tmp}" RESULT result)
949  endif()
950  if(result)
951    file(REMOVE "${tmp}")
952    message(FATAL_ERROR "Failed to create:\n  \"${tmp}\"\nfrom:\n  \"${obj}\"\nwith error:\n  ${result}")
953  endif()
954
955  # Atomically create/replace the real destination.
956  file(RENAME "${tmp}" "${dst}")
957endfunction()
958
959function(_ExternalData_download_file url file err_var msg_var)
960  set(retry 3)
961  while(retry)
962    math(EXPR retry "${retry} - 1")
963    if(ExternalData_TIMEOUT_INACTIVITY)
964      set(inactivity_timeout INACTIVITY_TIMEOUT ${ExternalData_TIMEOUT_INACTIVITY})
965    elseif(NOT "${ExternalData_TIMEOUT_INACTIVITY}" EQUAL 0)
966      set(inactivity_timeout INACTIVITY_TIMEOUT 60)
967    else()
968      set(inactivity_timeout "")
969    endif()
970    if(ExternalData_TIMEOUT_ABSOLUTE)
971      set(absolute_timeout TIMEOUT ${ExternalData_TIMEOUT_ABSOLUTE})
972    elseif(NOT "${ExternalData_TIMEOUT_ABSOLUTE}" EQUAL 0)
973      set(absolute_timeout TIMEOUT 300)
974    else()
975      set(absolute_timeout "")
976    endif()
977    set(show_progress_args)
978    if (ExternalData_SHOW_PROGRESS)
979      list(APPEND show_progress_args SHOW_PROGRESS)
980    endif ()
981    file(DOWNLOAD "${url}" "${file}" STATUS status LOG log ${inactivity_timeout} ${absolute_timeout} ${show_progress_args})
982    list(GET status 0 err)
983    list(GET status 1 msg)
984    if(err)
985      if("${msg}" MATCHES "HTTP response code said error" AND
986          "${log}" MATCHES "error: 503")
987        set(msg "temporarily unavailable")
988      endif()
989    elseif("${log}" MATCHES "\nHTTP[^\n]* 503")
990      set(err TRUE)
991      set(msg "temporarily unavailable")
992    endif()
993    if(NOT err OR NOT "${msg}" MATCHES "partial|timeout|temporarily")
994      break()
995    elseif(retry)
996      message(STATUS "[download terminated: ${msg}, retries left: ${retry}]")
997    endif()
998  endwhile()
999  set("${err_var}" "${err}" PARENT_SCOPE)
1000  set("${msg_var}" "${msg}" PARENT_SCOPE)
1001endfunction()
1002
1003function(_ExternalData_custom_fetch key loc file err_var msg_var)
1004  if(NOT ExternalData_CUSTOM_SCRIPT_${key})
1005    set(err 1)
1006    set(msg "No ExternalData_CUSTOM_SCRIPT_${key} set!")
1007  elseif(NOT EXISTS "${ExternalData_CUSTOM_SCRIPT_${key}}")
1008    set(err 1)
1009    set(msg "No '${ExternalData_CUSTOM_SCRIPT_${key}}' exists!")
1010  else()
1011    set(ExternalData_CUSTOM_LOCATION "${loc}")
1012    set(ExternalData_CUSTOM_FILE "${file}")
1013    unset(ExternalData_CUSTOM_ERROR)
1014    include("${ExternalData_CUSTOM_SCRIPT_${key}}")
1015    if(DEFINED ExternalData_CUSTOM_ERROR)
1016      set(err 1)
1017      set(msg "${ExternalData_CUSTOM_ERROR}")
1018    else()
1019      set(err 0)
1020      set(msg "no error")
1021    endif()
1022  endif()
1023  set("${err_var}" "${err}" PARENT_SCOPE)
1024  set("${msg_var}" "${msg}" PARENT_SCOPE)
1025endfunction()
1026
1027function(_ExternalData_get_from_object_store hash algo var_obj var_success)
1028  # Search all object stores for an existing object.
1029  foreach(dir ${ExternalData_OBJECT_STORES})
1030    set(obj "${dir}/${algo}/${hash}")
1031    if(EXISTS "${obj}")
1032      message(STATUS "Found object: \"${obj}\"")
1033      set("${var_obj}" "${obj}" PARENT_SCOPE)
1034      set("${var_success}" 1 PARENT_SCOPE)
1035      return()
1036    endif()
1037  endforeach()
1038endfunction()
1039
1040function(_ExternalData_download_object name hash algo var_obj var_success var_errorMsg)
1041  # Search all object stores for an existing object.
1042  set(success 1)
1043  foreach(dir ${ExternalData_OBJECT_STORES})
1044    set(obj "${dir}/${algo}/${hash}")
1045    if(EXISTS "${obj}")
1046      message(STATUS "Found object: \"${obj}\"")
1047      set("${var_obj}" "${obj}" PARENT_SCOPE)
1048      set("${var_success}" "${success}" PARENT_SCOPE)
1049      return()
1050    endif()
1051  endforeach()
1052
1053  # Download object to the first store.
1054  list(GET ExternalData_OBJECT_STORES 0 store)
1055  set(obj "${store}/${algo}/${hash}")
1056
1057  _ExternalData_random(random)
1058  set(tmp "${obj}.tmp${random}")
1059  set(found 0)
1060  set(tried "")
1061  foreach(url_template IN LISTS ExternalData_URL_TEMPLATES)
1062    string(REPLACE "%(hash)" "${hash}" url_tmp "${url_template}")
1063    string(REPLACE "%(algo)" "${algo}" url "${url_tmp}")
1064    if(url MATCHES "^(.*)%\\(algo:([A-Za-z_][A-Za-z0-9_]*)\\)(.*)$")
1065      set(lhs "${CMAKE_MATCH_1}")
1066      set(key "${CMAKE_MATCH_2}")
1067      set(rhs "${CMAKE_MATCH_3}")
1068      if(DEFINED ExternalData_URL_ALGO_${algo}_${key})
1069        set(url "${lhs}${ExternalData_URL_ALGO_${algo}_${key}}${rhs}")
1070      else()
1071        set(url "${lhs}${algo}${rhs}")
1072      endif()
1073    endif()
1074    string(REGEX REPLACE "((https?|ftp)://)([^@]+@)?(.*)" "\\1\\4" secured_url "${url}")
1075    message(STATUS "Fetching \"${secured_url}\"")
1076    if(url MATCHES "^ExternalDataCustomScript://([A-Za-z_][A-Za-z0-9_]*)/(.*)$")
1077      _ExternalData_custom_fetch("${CMAKE_MATCH_1}" "${CMAKE_MATCH_2}" "${tmp}" err errMsg)
1078    else()
1079      _ExternalData_download_file("${url}" "${tmp}" err errMsg)
1080    endif()
1081    string(APPEND tried "\n  ${url}")
1082    if(err)
1083      string(APPEND tried " (${errMsg})")
1084    else()
1085      # Verify downloaded object.
1086      _ExternalData_compute_hash(dl_hash "${algo}" "${tmp}")
1087      if("${dl_hash}" STREQUAL "${hash}")
1088        set(found 1)
1089        break()
1090      else()
1091        string(APPEND tried " (wrong hash ${algo}=${dl_hash})")
1092        if("$ENV{ExternalData_DEBUG_DOWNLOAD}" MATCHES ".")
1093          file(RENAME "${tmp}" "${store}/${algo}/${dl_hash}")
1094        endif()
1095      endif()
1096    endif()
1097    file(REMOVE "${tmp}")
1098  endforeach()
1099
1100  get_filename_component(dir "${name}" PATH)
1101  set(staged "${dir}/.ExternalData_${algo}_${hash}")
1102
1103  set(success 1)
1104  if(found)
1105    # Atomically create the object.  If we lose a race with another process,
1106    # do not replace it.  Content-addressing ensures it has what we expect.
1107    file(RENAME "${tmp}" "${obj}" NO_REPLACE RESULT result)
1108    if (result STREQUAL "NO_REPLACE")
1109      file(REMOVE "${tmp}")
1110    elseif (result)
1111      message(FATAL_ERROR "Failed to rename:\n  \"${tmp}\"\nto:\n  \"${obj}\"\nwith error:\n  ${result}")
1112    endif()
1113    message(STATUS "Downloaded object: \"${obj}\"")
1114  elseif(EXISTS "${staged}")
1115    set(obj "${staged}")
1116    message(STATUS "Staged object: \"${obj}\"")
1117  else()
1118    if(NOT tried)
1119      set(tried "\n  (No ExternalData_URL_TEMPLATES given)")
1120    endif()
1121    set(success 0)
1122    set("${var_errorMsg}" "Object ${algo}=${hash} not found at:${tried}" PARENT_SCOPE)
1123  endif()
1124
1125  set("${var_obj}" "${obj}" PARENT_SCOPE)
1126  set("${var_success}" "${success}" PARENT_SCOPE)
1127endfunction()
1128
1129if("${ExternalData_ACTION}" STREQUAL "fetch")
1130  foreach(v ExternalData_OBJECT_STORES file name exts)
1131    if(NOT DEFINED "${v}")
1132      message(FATAL_ERROR "No \"-D${v}=\" value provided!")
1133    endif()
1134  endforeach()
1135
1136  string(REPLACE "+" ";" exts_list "${exts}")
1137  set(succeeded 0)
1138  set(errorMsg "")
1139  set(hash_list )
1140  set(algo_list )
1141  set(hash )
1142  set(algo )
1143  foreach(ext ${exts_list})
1144    file(READ "${name}${ext}" hash)
1145    string(STRIP "${hash}" hash)
1146
1147    if("${ext}" MATCHES "^\\.(${_ExternalData_REGEX_EXT})$")
1148      string(TOUPPER "${CMAKE_MATCH_1}" algo)
1149      string(REPLACE "-" "_" algo "${algo}")
1150    else()
1151      message(FATAL_ERROR "Unknown hash algorithm extension \"${ext}\"")
1152    endif()
1153
1154    list(APPEND hash_list ${hash})
1155    list(APPEND algo_list ${algo})
1156  endforeach()
1157
1158  list(LENGTH exts_list num_extensions)
1159  math(EXPR exts_range "${num_extensions} - 1")
1160  foreach(ii RANGE 0 ${exts_range})
1161    list(GET hash_list ${ii} hash)
1162    list(GET algo_list ${ii} algo)
1163    _ExternalData_get_from_object_store("${hash}" "${algo}" obj succeeded)
1164    if(succeeded)
1165      break()
1166    endif()
1167  endforeach()
1168  if(NOT succeeded)
1169    foreach(ii RANGE 0 ${exts_range})
1170      list(GET hash_list ${ii} hash)
1171      list(GET algo_list ${ii} algo)
1172      _ExternalData_download_object("${name}" "${hash}" "${algo}"
1173        obj succeeded algoErrorMsg)
1174      string(APPEND errorMsg "\n${algoErrorMsg}")
1175      if(succeeded)
1176        break()
1177      endif()
1178    endforeach()
1179  endif()
1180  if(NOT succeeded)
1181    message(FATAL_ERROR "${errorMsg}")
1182  endif()
1183  # Check if file already corresponds to the object.
1184  set(stamp "-hash-stamp")
1185  set(file_up_to_date 0)
1186  if(EXISTS "${file}" AND EXISTS "${file}${stamp}")
1187    file(READ "${file}${stamp}" f_hash)
1188    string(STRIP "${f_hash}" f_hash)
1189    if("${f_hash}" STREQUAL "${hash}")
1190      set(file_up_to_date 1)
1191    endif()
1192  endif()
1193
1194  if(file_up_to_date)
1195    # Touch the file to convince the build system it is up to date.
1196    file(TOUCH "${file}")
1197  else()
1198    _ExternalData_link_or_copy("${obj}" "${file}")
1199  endif()
1200
1201  # Atomically update the hash/timestamp file to record the object referenced.
1202  _ExternalData_atomic_write("${file}${stamp}" "${hash}\n")
1203elseif("${ExternalData_ACTION}" STREQUAL "local")
1204  foreach(v file name)
1205    if(NOT DEFINED "${v}")
1206      message(FATAL_ERROR "No \"-D${v}=\" value provided!")
1207    endif()
1208  endforeach()
1209  _ExternalData_link_or_copy("${name}" "${file}")
1210else()
1211  message(FATAL_ERROR "Unknown ExternalData_ACTION=[${ExternalData_ACTION}]")
1212endif()
1213