1#
2# Copyright 2020, Data61, CSIRO (ABN 41 687 119 230)
3#
4# SPDX-License-Identifier: BSD-2-Clause
5#
6
7# Try to set the cache dir based on environment variable, but override
8# with value passed in as CMake -D argument.
9set(cache_dir "$ENV{SEL4_CACHE_DIR}")
10if(NOT ${SEL4_CACHE_DIR} STREQUAL "")
11    set(cache_dir "${SEL4_CACHE_DIR}")
12endif()
13# Convert to an absolute path.
14if((NOT ("${cache_dir}" STREQUAL "")) AND NOT IS_ABSOLUTE cache_dir)
15    get_filename_component(cache_dir "${cache_dir}" ABSOLUTE BASE_DIR "${CMAKE_BINARY_DIR}")
16endif()
17set(MEMOIZE_CACHE_DIR "${cache_dir}" CACHE INTERNAL "" FORCE)
18
19# This function wraps a call to add_custom_command and may instead use an alternative cached
20# copy if it can already find a version that has been built before.
21# If git_directory is provided and the git directory has any uncommitted changes, then the
22# cache is bypassed and will always build from source.
23# key: A key to distinguish different memoized commands by, and also used in diagnostic output
24# replace_dir: Directory to tar and cache. This tar'd directory is what gets expanded in cache hits.
25# git_directory: A directory in a Git repository for performing clean/dirty check.
26# extra_arguments: A string of extra arguments that if change invalidate previous cache entries.
27# replace_files: Subset list of files to tar from replace_dir. If empty string then the whole dir
28#   will be cached.
29function(memoize_add_custom_command key replace_dir git_directory extra_arguments replace_files)
30
31    message(STATUS "Detecting cached version of: ${key}")
32
33    # If a cache directory isn't set then call the underlying function and return.
34    if("${MEMOIZE_CACHE_DIR}" STREQUAL "")
35        message(
36            STATUS
37                "  No cache path given. Set SEL4_CACHE_DIR to a path to enable caching binary artifacts."
38        )
39        add_custom_command(${ARGN})
40        return()
41    endif()
42
43    set(dirty OFF)
44
45    # Check if the git directory has any changes.
46    # Sets dirty to ON if there are changes
47    # sets git_source_hash to the git hash
48    if(NOT ${git_directory} STREQUAL "")
49        find_package(Git REQUIRED)
50
51        execute_process(
52            COMMAND
53                "${GIT_EXECUTABLE}" diff --exit-code HEAD --
54            WORKING_DIRECTORY "${git_directory}"
55            RESULT_VARIABLE res
56            OUTPUT_VARIABLE out
57            ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE
58        )
59        if(res EQUAL 0)
60
61        else()
62            set(dirty ON)
63        endif()
64        execute_process(
65            COMMAND "${GIT_EXECUTABLE}" rev-parse HEAD
66            WORKING_DIRECTORY "${git_directory}"
67            RESULT_VARIABLE res
68            OUTPUT_VARIABLE out
69            ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE
70        )
71        set(git_source_hash ${out})
72    endif()
73
74    # Set the directory if no replace_files are set
75    if(replace_files STREQUAL "")
76        set(replace_files .)
77    endif()
78
79    # Set the cache dir based on a hash of the git hash and extra arguments
80    # Note: We don't use the entire args to add_custom_command as inputs to the
81    # hash as they will change based on the path of the build directory.
82    # also it's too hard to guarantee that we can perfectly know whether a cache
83    # entry is valid or not, so delegate figuring out to the caller.
84    set(hash_string ${extra_arguments} ${git_source_hash})
85    string(MD5 hash "${hash_string}")
86    set(cache_dir ${MEMOIZE_CACHE_DIR}/${key}/${hash})
87    if(dirty)
88        # The git directory has changed, so don't use the cache
89        message(STATUS "  ${key} is dirty - will build from source")
90        add_custom_command(${ARGN})
91    else()
92        if(EXISTS ${cache_dir}/code.tar.gz)
93            # Cache hit. Create a different call to add_custom_command that
94            # merely unpacks the result from the last build instance into the
95            # target directory.
96            message(STATUS "  Found valid cache entry for ${key}")
97
98            # Set a cmake rebuild dependency on the cache file so if it changes
99            # cmake will get automatically rerun
100            set_property(
101                DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
102                APPEND
103                PROPERTY CMAKE_CONFIGURE_DEPENDS "${cache_dir}/code.tar.gz"
104            )
105
106            # As we use the same output file we extract it from the args passed in.
107            if(NOT "${ARGV5}" STREQUAL "OUTPUT")
108                message(FATAL_ERROR "OUTPUT must be first argument to this function")
109            endif()
110            add_custom_command(
111                OUTPUT
112                    # This has to match up with the OUTPUT variable of the call to add_custom_command
113                    ${ARGV6}
114                    # If we have to rebuild, first clear the temporary build directory as
115                    # we have no correctly captured the output files or dependencies
116                COMMAND rm -r ${replace_dir}
117                COMMAND mkdir -p ${replace_dir}
118                COMMAND
119                    tar -C ${replace_dir} -xf ${cache_dir}/code.tar.gz
120                DEPENDS ${deps} COMMAND_EXPAND_LISTS
121                COMMENT "Using cache ${key} build"
122            )
123        else()
124            # Don't have a previous build in the cache.  Create the rule but add
125            # some commands on the end to cache the result in our cache.
126            message(STATUS "  Not found cache entry for ${key} - will build from source")
127            add_custom_command(
128                ${ARGN}
129                COMMAND mkdir -p ${cache_dir}
130                COMMAND
131                    tar -zcf code.tar.gz -C ${replace_dir} ${replace_files}
132                COMMAND mv code.tar.gz ${cache_dir}/
133            )
134        endif()
135    endif()
136endfunction()
137