From 6ff3b19ee6120edf015fad8caab2991faa3070af Mon Sep 17 00:00:00 2001 From: Anthony Barbier Date: Mon, 4 Sep 2017 18:44:23 +0100 Subject: COMPMID-344 Updated doxygen Change-Id: I32f7b84daa560e460b77216add529c8fa8b327ae --- scripts/add_copyright.py | 83 +++++++++++++++++++ scripts/check_bad_style.sh | 71 ++++++++++++++++ scripts/check_clang-tidy.py | 59 ++++++++++++++ scripts/clang-tidy.h | 112 ++++++++++++++++++++++++++ scripts/clang-tidy.sh | 91 +++++++++++++++++++++ scripts/copyright_eula.txt | 19 +++++ scripts/copyright_mit.txt | 21 +++++ scripts/fix_code_formatting.sh | 33 ++++++++ scripts/format_doxygen.py | 151 +++++++++++++++++++++++++++++++++++ scripts/include_functions_kernels.py | 64 +++++++++++++++ 10 files changed, 704 insertions(+) create mode 100755 scripts/add_copyright.py create mode 100755 scripts/check_bad_style.sh create mode 100755 scripts/check_clang-tidy.py create mode 100644 scripts/clang-tidy.h create mode 100755 scripts/clang-tidy.sh create mode 100644 scripts/copyright_eula.txt create mode 100644 scripts/copyright_mit.txt create mode 100755 scripts/fix_code_formatting.sh create mode 100755 scripts/format_doxygen.py create mode 100755 scripts/include_functions_kernels.py (limited to 'scripts') diff --git a/scripts/add_copyright.py b/scripts/add_copyright.py new file mode 100755 index 0000000000..6142298828 --- /dev/null +++ b/scripts/add_copyright.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +import glob +import os.path + +eula_copyright = open("scripts/copyright_eula.txt",'r').read() + +def add_cpp_copyright( f, content): + global eula_copyright + out = open(f,'w') + out.write("/*\n") + for line in eula_copyright.split('\n')[:-1]: + out.write(" *"); + if line.strip() != "": + out.write(" %s" %line) + out.write("\n") + out.write(" */\n") + out.write(content.strip()) + out.write("\n") + out.close() + +def add_python_copyright( f, content): + global eula_copyright + out = open(f,'w') + for line in eula_copyright.split('\n')[:-1]: + out.write("#"); + if line.strip() != "": + out.write(" %s" %line) + out.write("\n") + out.write(content.strip()) + out.write("\n") + out.close() + +def remove_comment( content ): + comment=True + out="" + for line in content.split('\n'): + if comment: + if line.startswith(' */'): + comment=False + elif line.startswith('/*') or line.startswith(' *'): + #print(line) + continue + else: + raise Exception("ERROR: not a comment ? '%s'"% line) + else: + out += line + "\n" + return out +def remove_comment_python( content ): + comment=True + out="" + for line in content.split('\n'): + if comment and line.startswith('#'): + continue + else: + comment = False + out += line + "\n" + return out + +for top in ['./arm_compute', './tests','./src','./examples','./utils/']: + for root, _, files in os.walk(top): + for f in files: + path = os.path.join(root, f) + + if f in ['.clang-tidy', '.clang-format']: + print("Skipping file: {}".format(path)) + continue + + with open(path, 'r', encoding='utf-8') as fd: + content = fd.read() + _, extension = os.path.splitext(f) + + if extension in ['.cpp', '.h', '.inl', '.cl']: + if not content.startswith('/*'): + add_cpp_copyright(path, content) + elif extension == '.py' or f in ['SConstruct', 'SConscript']: + if not content.startswith('# Copyright'): + add_python_copyright(path, content) + elif f == 'CMakeLists.txt': + if not content.startswith('# Copyright'): + add_python_copyright(path, content) + else: + raise Exception("Unhandled file: {}".format(path)) diff --git a/scripts/check_bad_style.sh b/scripts/check_bad_style.sh new file mode 100755 index 0000000000..1cc514cdc3 --- /dev/null +++ b/scripts/check_bad_style.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +set -e + +DIRECTORIES="./arm_compute ./src ./examples ./tests ./utils" + +grep -HrnP "/\*\*$" $DIRECTORIES | tee bad_style.log +if (( `cat bad_style.log | wc -l` > 0 )) +then + echo "" + echo "ERROR: Doxygen comments should start on the first line: \"/** My comment\"" + exit -1 +fi + +grep -Hnr --exclude=Doxyfile "@brief" $DIRECTORIES | tee bad_style.log +if (( `cat bad_style.log | wc -l` > 0 )) +then + echo "" + echo "ERROR: Doxygen comments shouldn't use '@brief'" + exit -1 +fi + +grep -HnRE "\buint " --exclude-dir=cl_kernels $DIRECTORIES | tee bad_style.log +if [[ $(cat bad_style.log | wc -l) > 0 ]] +then + echo "" + echo "ERROR: C/C++ don't define 'uint'. Use 'unsigned int' instead." + exit -1 +fi + +grep -HnR "float32_t" $DIRECTORIES | tee bad_style.log +if [[ $(cat bad_style.log | wc -l) > 0 ]] +then + echo "" + echo "ERROR: C/C++ don't define 'float32_t'. Use 'float' instead." + exit -1 +fi + +grep -Hnir "arm[_ ]\?cv" $DIRECTORIES | tee bad_style.log +if [[ $(cat bad_style.log | wc -l) > 0 ]] +then + echo "" + echo "ERROR: Reference to arm_cv detected in the files above (Replace with arm_compute)" + exit -1 +fi + +spdx_missing=0 +for f in $(find $DIRECTORIES -type f) +do + if [[ $(grep SPDX $f | wc -l) == 0 ]] + then + # List of exceptions: + case `basename $f` in + "arm_compute_version.embed");; + ".clang-format");; + ".clang-tidy");; + #It's an error for other files to not contain the MIT header: + *) + spdx_missing=1 + echo $f; + ;; + esac + fi; +done + +if [[ $spdx_missing > 0 ]] +then + echo "" + echo "ERROR: MIT Copyright header missing from the file(s) above." + exit -1 +fi diff --git a/scripts/check_clang-tidy.py b/scripts/check_clang-tidy.py new file mode 100755 index 0000000000..ead2513a93 --- /dev/null +++ b/scripts/check_clang-tidy.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import sys + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("usage: {} CLANG-TIDY_OUTPUT_FILE".format(sys.argv[0])) + sys.exit(1) + + failed = False + + with open(sys.argv[1], mode="r") as clang_tidy_file: + lines = clang_tidy_file.readlines() + + for i in range(0, len(lines)): + line = lines[i] + + if "error:" in line: + if (("Utils.cpp" in line and "'arm_compute_version.embed' file not found" in line) or + ("cl2.hpp" in line and "cast from pointer to smaller type 'cl_context_properties' (aka 'int') loses information" in line) or + ("memory" in line and "cast from pointer to smaller type 'uintptr_t' (aka 'unsigned int') loses information" in line) or + "3rdparty" in line): + continue + + failed = True + print(line) + elif "warning:" in line: + if ("uninitialized record type: '__ret'" in line or + "local variable '__bound_functor' is still referred to by the global variable '__once_callable'" in line or + ("Error.cpp" in line and "thrown exception type is not nothrow copy constructible" in line) or + ("Error.cpp" in line and "uninitialized record type: 'args'" in line) or + ("Error.cpp" in line and "do not call c-style vararg functions" in line) or + ("Error.cpp" in line and "do not define a C-style variadic function" in line) or + ("NEMinMaxLocationKernel.cpp" in line and "move constructors should be marked noexcept" in line) or + ("NEMinMaxLocationKernel.cpp" in line and "move assignment operators should be marked noexcept" in line) or + ("PMUCounter.cpp" in line and "consider replacing 'long long' with 'int64'" in line) or + "3rdparty" in line): + continue + + if "do not use C-style cast to convert between unrelated types" in line: + if i + 1 < len(lines) and "vgetq_lane_f16" in lines[i + 1]: + continue + + if "use 'using' instead of 'typedef'" in line: + if i + 1 < len(lines) and "BOOST_FIXTURE_TEST_SUITE" in lines[i + 1]: + continue + + if "do not call c-style vararg functions" in line: + if (i + 1 < len(lines) and + ("BOOST_TEST" in lines[i + 1] or + "BOOST_FAIL" in lines[i + 1] or + "BOOST_CHECK_THROW" in lines[i + 1] or + "syscall" in lines[i + 1])): + continue + + failed = True + print(line) + + sys.exit(0 if not failed else 1) diff --git a/scripts/clang-tidy.h b/scripts/clang-tidy.h new file mode 100644 index 0000000000..32b0f6955e --- /dev/null +++ b/scripts/clang-tidy.h @@ -0,0 +1,112 @@ +#include + +inline float16x8_t vcvtq_f16_u16(uint16x8_t) +{ + return vdupq_n_f16(0); +} + +inline uint16x8_t vcvtq_u16_f16(float16x8_t) +{ + return vdupq_n_u16(0); +} + +inline int16x8_t vcvtq_s16_f16(float16x8_t) +{ + return vdupq_n_s16(0); +} + +inline float16x8_t vaddq_f16(float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vsubq_f16(float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vmulq_f16(float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vmulq_n_f16(float16x8_t, float16_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vfmaq_f16(float16x8_t, float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline uint16x8_t vcgeq_f16(float16x8_t, float16x8_t) +{ + return vdupq_n_u16(0); +} + +inline uint16x8_t vcgtq_f16(float16x8_t, float16x8_t) +{ + return vdupq_n_u16(0); +} + +inline float16x8_t vbslq_f16 (uint16x8_t, float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0);; +} + +inline float16x8_t vextq_f16(float16x8_t, float16x8_t, int) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vabsq_f16(float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline uint16x8_t vcvtq_f16_s16(float16x8_t) +{ + return vdupq_n_s16(0); +} + +inline float16x4_t vbsl_f16 (uint16x4_t,float16x4_t, float16x4_t) +{ + return vdup_n_f16(0); +} + +inline float16x8_t vrsqrteq_f16(float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vfmsq_f16 (float16x8_t, float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vrecpeq_f16 (float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vrecpsq_f16 (float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vmaxq_f16 (float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline float16x8_t vminq_f16 (float16x8_t, float16x8_t) +{ + return vdupq_n_f16(0); +} + +inline uint16x8_t vcltq_f16(float16x8_t, float16x8_t) +{ + return vdupq_n_u16(0); +} + diff --git a/scripts/clang-tidy.sh b/scripts/clang-tidy.sh new file mode 100755 index 0000000000..053e5783c2 --- /dev/null +++ b/scripts/clang-tidy.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +DIRECTORIES="./arm_compute ./src ./examples ./tests ./utils" + +if [ $# -eq 0 ] +then + files=$(find $DIRECTORIES -type f -name \*.cpp | sort) +else + files=$@ +fi + +SCRIPT_PATH=$(dirname $0) + +CLANG_TIDY=$(which clang-tidy) + +if [[ -z $CLANG_TIDY ]]; then + echo "clang-tidy not found!" + exit 1 +else + echo "Found clang-tidy:" $CLANG_TIDY +fi + +CLANG_TIDY_PATH=$(dirname $CLANG_TIDY)/.. + +ARMV7_GCC=$(which arm-linux-gnueabihf-g++) + +if [[ -z $ARMV7_GCC ]]; then + echo "arm-linux-gnueabihf-g++ not found!" + exit 1 +else + echo "Found arm-linux-gnueabihf-g++:" $ARMV7_GCC +fi + +ARMV7_GCC_PATH=$(dirname $ARMV7_GCC)/.. + +AARCH64_GCC=$(which aarch64-linux-gnu-g++) + +if [[ -z $AARCH64_GCC ]]; then + echo "aarch64-linux-gnu-g++ not found!" + exit 1 +else + echo "Found aarch64-linux-gnu-g++:" $AARCH64_GCC +fi + +ARMV7_GCC_PATH=$(dirname $ARMV7_GCC)/.. +AARCH64_GCC_PATH=$(dirname $AARCH64_GCC)/.. + +function armv7 +{ + USE_BOOST="" + + if [[ "$1" == *tests/validation* ]] + then + USE_BOOST="-DBOOST" + fi + + $CLANG_TIDY \ + "$1" \ + -- \ + -target armv7a-none-linux-gnueabihf \ + --gcc-toolchain=$ARMV7_GCC_PATH \ + -std=c++11 \ + -Iinclude -I. -I3rdparty/include -Ikernels -Itests -Icomputer_vision \ + -DARM_COMPUTE_CPP_SCHEDULER=1 $USE_BOOST + #read -rsp $'Press enter to continue...\n' +} + +function aarch64 +{ + USE_BOOST="" + + if [[ "$1" == *tests/validation* ]] + then + USE_BOOST="-DBOOST" + fi + + $CLANG_TIDY \ + "$1" \ + -- \ + -target aarch64-none-linux-gnueabi \ + --gcc-toolchain=$AARCH64_GCC_PATH \ + -std=c++11 \ + -include $SCRIPT_PATH/clang-tidy.h \ + -Iinclude -I. -I3rdparty/include -Ikernels -Itests -Icomputer_vision \ + -DARM_COMPUTE_ENABLE_FP16 -DARM_COMPUTE_CPP_SCHEDULER=1 $USE_BOOST +} + +for f in $files; do + #armv7 "$f" + aarch64 "$f" +done diff --git a/scripts/copyright_eula.txt b/scripts/copyright_eula.txt new file mode 100644 index 0000000000..50e8090e4f --- /dev/null +++ b/scripts/copyright_eula.txt @@ -0,0 +1,19 @@ +Copyright (c) 2016, 2017 ARM Limited. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/copyright_mit.txt b/scripts/copyright_mit.txt new file mode 100644 index 0000000000..0beacf8d93 --- /dev/null +++ b/scripts/copyright_mit.txt @@ -0,0 +1,21 @@ +Copyright (c) 2016, 2017 ARM Limited. + +SPDX-License-Identifier: MIT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/fix_code_formatting.sh b/scripts/fix_code_formatting.sh new file mode 100755 index 0000000000..2ab3c1d532 --- /dev/null +++ b/scripts/fix_code_formatting.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +ASTYLE_PARAMETERS=" --style=ansi \ + --indent=spaces \ + --indent-switches \ + --indent-col1-comments \ + --min-conditional-indent=0 \ + --max-instatement-indent=120 \ + --pad-oper \ + --align-pointer=name \ + --align-reference=name \ + --break-closing-brackets \ + --keep-one-line-statements \ + --max-code-length=200 \ + --mode=c \ + --lineend=linux \ + --indent-preprocessor \ + " + +DIRECTORIES="./arm_compute ./src ./examples ./tests ./utils" + +if [ $# -eq 0 ] +then + files=$(find $DIRECTORIES -type f \( -name \*.cpp -o -iname \*.h -o -name \*.inl -o -name \*.cl \)) +else + files=$@ +fi +for f in $files +do + sed -i 's/\t/ /g' $f + clang-format -i -style=file $f + astyle -n -q $ASTYLE_PARAMETERS $f +done diff --git a/scripts/format_doxygen.py b/scripts/format_doxygen.py new file mode 100755 index 0000000000..96adf52726 --- /dev/null +++ b/scripts/format_doxygen.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 + +import os.path +import re +import sys + +def process_comment(fd, comment, first_param, last_param): + if first_param < 0: + # Nothing to do: just copy the comment + fd.write("".join(comment)) + else: + params = list() + + # Copy the non param lines unmodified: + fd.write("".join(comment[:first_param])) + + # Measure the indentation of the first param and use that to create an empty comment line string: + m = re.match(r" *\*", comment[first_param]) + + if not m: + raise Exception("Not a comment ? '{}'".format(comment[first_param])) + + empty_line = "{}\n".format(m.group(0)) + + # For each param split the line into 3 columns: param, param_name, description + for param in range(first_param, last_param): + m = re.match(r"([^@]+@param\[[^\]]+\]) +(\S+) +(.+)", comment[param]) + + if m: + params.append( (m.group(1), m.group(2), m.group(3)) ) + else: + # If it's not a match then it must be a multi-line param description: + + m = re.match("( *\*) +(.*)", comment[param]) + + if not m: + raise Exception("Not a comment line ? ({})".format(n_line - len(comment) + param)) + + params.append( (m.group(1), "", m.group(2)) ) + + # Now that we've got a list of params, find what is the longest string for each column: + max_len = [0, 0] + + for p in params: + for l in range(len(max_len)): + max_len[l] = max(max_len[l], len(p[l])) + + # Insert an empty line if needed before the first param to make it easier to read: + m = re.match(r" *\* *$", comment[first_param - 1]) + + if not m: + # insert empty line + fd.write(empty_line) + + # Write out the formatted list of params: + for p in params: + fd.write("{}{} {}{} {}\n".format( + p[0], " " * (max_len[0] - len(p[0])), + p[1], " " * (max_len[1] - len(p[1])), + p[2])) + + # If the next line after the list of params is a command (@return, @note, @warning, etc), insert an empty line to separate it from the list of params + if last_param < len(comment) - 1: + if re.match(r" *\* *@\w+", comment[last_param]): + # insert empty line + fd.write(empty_line) + + # Copy the remaining of the comment unmodified: + fd.write("".join(comment[last_param:])) + +if __name__ == "__main__": + n_file=0 + + if len(sys.argv) == 1: + paths = [] + + for top_level in ["./arm_compute", "./src", "./examples", "./tests", "./utils"]: + for root, _, files in os.walk(top_level): + paths.extend([os.path.join(root, f) for f in files]) + else: + paths = sys.argv[1:] + + for path in paths: + if (path[-3:] not in ("cpp", "inl") and + path[-2:] not in ("cl") and + path[-1] not in ("h")): + continue + + print("[{}] {}".format(n_file, path)) + + n_file += 1 + + with open(path,'r+', encoding="utf-8") as fd: + comment = list() + first_param = -1 + last_param = -1 + n_line = 0 + + lines = fd.readlines() + fd.seek(0) + fd.truncate() + + for line in lines: + n_line += 1 + + # Start comment + # Match C-style comment /* anywhere in the line + if re.search(r"/\*", line): + #print("Start comment {}".format(n_line)) + + if len(comment) > 0: + raise Exception("Already in a comment! ({})".format(n_line)) + + comment.append(line) + + # Comment already started + elif len(comment) > 0: + #print("Add line to comment {}".format(n_line)) + + comment.append(line) + + # Non-comment line + else: + #print("Normal line {}".format(n_line)) + + fd.write(line) + + # Match param declaration in Doxygen comment + # @param[in] name description + if re.search(r"@param\[[^\]]+\] +\S+ +\S", line): + #print("Param {}".format(n_line)) + + if first_param < 0: + first_param = len(comment) - 1 + + last_param = len(comment) + + # Match end of C-style comment */ + if re.search(r"\*/", line): + #print("End comment {}".format(n_line)) + + if len(comment) < 1: + raise Exception("Was not in a comment! ({})".format(n_line)) + + #print("Process comment {} {}".format(first_param, last_param)) + + process_comment(fd, comment, first_param, last_param) + + comment = list() + first_param = -1 + last_param = -1 diff --git a/scripts/include_functions_kernels.py b/scripts/include_functions_kernels.py new file mode 100755 index 0000000000..9f566cbc1a --- /dev/null +++ b/scripts/include_functions_kernels.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3.5 + +import glob +import collections +import os + +Target = collections.namedtuple('Target', 'name prefix') + +targets = [Target("NEON", "NE"), Target("CL", "CL"), Target("CPP", "CPP")] + +armcv_path = "arm_compute" +core_path = armcv_path + "/core/" +runtime_path = armcv_path + "/runtime/" +include_str = "#include \"" + + +def read_file(file): + with open(file, "r") as f: + lines = f.readlines() + return lines + + +def write_file(file, lines): + with open(file, "w") as f: + for line in lines: + f.write(line) + + +def remove_existing_includes(lines): + first_pos = next(i for i, line in enumerate(lines) if include_str in line) + return [x for x in lines if not x.startswith(include_str)], first_pos + + +def add_updated_includes(lines, pos, includes): + lines[pos:pos] = includes + return lines + + +def create_include_list(folder): + files_path = folder + "/*.h" + files = glob.glob(files_path) + updated_files = [include_str + folder + "/" + x.rsplit('/',1)[1] + "\"\n" for x in files] + updated_files.sort() + return updated_files + + +def include_components(path, header_prefix, folder): + for t in targets: + target_path = path + t.name + "/" + components_file = target_path + t.prefix + header_prefix + if os.path.exists(components_file): + include_list = create_include_list(target_path + folder) + lines = read_file(components_file) + lines, first_pos = remove_existing_includes(lines) + lines = add_updated_includes(lines, first_pos, include_list) + write_file(components_file, lines) + + +if __name__ == "__main__": + # Include kernels + include_components(core_path, "Kernels.h", "kernels") + + # Include functions + include_components(runtime_path, "Functions.h", "functions") -- cgit v1.2.1