diff --git a/tools/README b/tools/README index 3a0818cd..fffe7191 100644 --- a/tools/README +++ b/tools/README @@ -1,6 +1,11 @@ README -The SDK uses 'kconfig', 'idf_monitor.py' of esp-idf and information of esp-idf is following: +Information of 'idf_monitor.py' is following: URL: https://github.com/espressif/esp-idf tag: v3.1-dev + +Information of 'cmake', 'kconfig', 'kconfig_new' and 'idf.py' is following: + +commit: 48c3ad37 + diff --git a/tools/cmake/components.cmake b/tools/cmake/components.cmake new file mode 100644 index 00000000..506d8a06 --- /dev/null +++ b/tools/cmake/components.cmake @@ -0,0 +1,165 @@ +# Given a list of components in 'component_paths', filter only paths to the components +# mentioned in 'components' and return as a list in 'result_paths' +function(components_get_paths component_paths components result_paths) + set(result "") + foreach(path ${component_paths}) + get_filename_component(name "${path}" NAME) + if("${name}" IN_LIST components) + list(APPEND result "${name}") + endif() + endforeach() + set("${result_path}" "${result}" PARENT_SCOPE) +endfunction() + +# Add a component to the build, using the COMPONENT variables defined +# in the parent +# +function(register_component) + get_filename_component(component_dir ${CMAKE_CURRENT_LIST_FILE} DIRECTORY) + get_filename_component(component ${component_dir} NAME) + + spaces2list(COMPONENT_SRCDIRS) + spaces2list(COMPONENT_ADD_INCLUDEDIRS) + + # Add to COMPONENT_SRCS by globbing in COMPONENT_SRCDIRS + if(NOT COMPONENT_SRCS) + foreach(dir ${COMPONENT_SRCDIRS}) + get_filename_component(abs_dir ${dir} ABSOLUTE BASE_DIR ${component_dir}) + if(NOT IS_DIRECTORY ${abs_dir}) + message(FATAL_ERROR "${CMAKE_CURRENT_LIST_FILE}: COMPONENT_SRCDIRS entry '${dir}' does not exist") + endif() + + file(GLOB matches "${abs_dir}/*.c" "${abs_dir}/*.cpp" "${abs_dir}/*.S") + if(matches) + list(SORT matches) + set(COMPONENT_SRCS "${COMPONENT_SRCS};${matches}") + else() + message(FATAL_ERROR "${CMAKE_CURRENT_LIST_FILE}: COMPONENT_SRCDIRS entry '${dir}' has no source files") + endif() + endforeach() + endif() + + # add as a PUBLIC library (if there are source files) or INTERFACE (if header only) + if(COMPONENT_SRCS OR embed_binaries) + add_library(${component} STATIC ${COMPONENT_SRCS}) + set(include_type PUBLIC) + else() + add_library(${component} INTERFACE) # header-only component + set(include_type INTERFACE) + endif() + + # binaries to embed directly in library + spaces2list(COMPONENT_EMBED_FILES) + spaces2list(COMPONENT_EMBED_TXTFILES) + foreach(embed_data ${COMPONENT_EMBED_FILES} ${COMPONENT_EMBED_TXTFILES}) + if(embed_data IN_LIST COMPONENT_EMBED_TXTFILES) + set(embed_type "TEXT") + else() + set(embed_type "BINARY") + endif() + target_add_binary_data("${component}" "${embed_data}" "${embed_type}") + endforeach() + + # add component public includes + foreach(include_dir ${COMPONENT_ADD_INCLUDEDIRS}) + get_filename_component(abs_dir ${include_dir} ABSOLUTE BASE_DIR ${component_dir}) + if(NOT IS_DIRECTORY ${abs_dir}) + message(FATAL_ERROR "${CMAKE_CURRENT_LIST_FILE}: " + "COMPONENT_ADD_INCLUDEDIRS entry '${include_dir}' not found") + endif() + target_include_directories(${component} ${include_type} ${abs_dir}) + endforeach() + + # add component private includes + foreach(include_dir ${COMPONENT_PRIV_INCLUDEDIRS}) + if(${include_type} STREQUAL INTERFACE) + message(FATAL_ERROR "${CMAKE_CURRENT_LIST_FILE} " + "sets no component source files but sets COMPONENT_PRIV_INCLUDEDIRS") + endif() + + get_filename_component(abs_dir ${include_dir} ABSOLUTE BASE_DIR ${component_dir}) + if(NOT IS_DIRECTORY ${abs_dir}) + message(FATAL_ERROR "${CMAKE_CURRENT_LIST_FILE}: " + "COMPONENT_PRIV_INCLUDEDIRS entry '${include_dir}' does not exist") + endif() + target_include_directories(${component} PRIVATE ${abs_dir}) + endforeach() +endfunction() + +function(register_config_only_component) + get_filename_component(component_dir ${CMAKE_CURRENT_LIST_FILE} DIRECTORY) + get_filename_component(component ${component_dir} NAME) + + # No-op for now... +endfunction() + +function(add_component_dependencies target dep dep_type) + get_target_property(target_type ${target} TYPE) + get_target_property(target_imported ${target} IMPORTED) + + if(${target_type} STREQUAL STATIC_LIBRARY OR ${target_type} STREQUAL EXECUTABLE) + if(TARGET ${dep}) + # Add all compile options exported by dep into target + target_include_directories(${target} ${dep_type} + $) + target_compile_definitions(${target} ${dep_type} + $) + target_compile_options(${target} ${dep_type} + $) + endif() + endif() +endfunction() + +function(components_finish_registration) + + # have the executable target depend on all components in the build + set_target_properties(${CMAKE_PROJECT_NAME}.elf PROPERTIES INTERFACE_COMPONENT_REQUIRES "${BUILD_COMPONENTS}") + + spaces2list(COMPONENT_REQUIRES_COMMON) + + # each component should see the include directories of its requirements + # + # (we can't do this until all components are registered and targets exist in cmake, as we have + # a circular requirements graph...) + foreach(a ${BUILD_COMPONENTS}) + if(TARGET ${a}) + get_component_requirements("${a}" a_deps a_priv_deps) + list(APPEND a_priv_deps ${COMPONENT_REQUIRES_COMMON}) + foreach(b ${a_deps}) + add_component_dependencies(${a} ${b} PUBLIC) + endforeach() + + foreach(b ${a_priv_deps}) + add_component_dependencies(${a} ${b} PRIVATE) + endforeach() + + get_target_property(a_type ${a} TYPE) + if(${a_type} MATCHES .+_LIBRARY) + set(COMPONENT_LIBRARIES "${COMPONENT_LIBRARIES};${a}") + endif() + endif() + endforeach() + + # Add each component library's link-time dependencies (which are otherwise ignored) to the executable + # LINK_DEPENDS in order to trigger a re-link when needed (on Ninja/Makefile generators at least). + # (maybe this should probably be something CMake does, but it doesn't do it...) + foreach(component ${BUILD_COMPONENTS}) + if(TARGET ${component}) + get_target_property(imported ${component} IMPORTED) + get_target_property(type ${component} TYPE) + if(NOT imported) + if(${type} STREQUAL STATIC_LIBRARY OR ${type} STREQUAL EXECUTABLE) + get_target_property(link_depends "${component}" LINK_DEPENDS) + if(link_depends) + set_property(TARGET ${CMAKE_PROJECT_NAME}.elf APPEND PROPERTY LINK_DEPENDS "${link_depends}") + endif() + endif() + endif() + endif() + endforeach() + + target_link_libraries(${CMAKE_PROJECT_NAME}.elf ${COMPONENT_LIBRARIES}) + + message(STATUS "Component libraries: ${COMPONENT_LIBRARIES}") + +endfunction() diff --git a/tools/cmake/convert_to_cmake.py b/tools/cmake/convert_to_cmake.py new file mode 100755 index 00000000..82e69717 --- /dev/null +++ b/tools/cmake/convert_to_cmake.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python +# +# Command line tool to convert simple ESP-IDF Makefile & component.mk files to +# CMakeLists.txt files +# +import argparse +import subprocess +import re +import os.path +import glob +import sys + +debug = False + +def get_make_variables(path, makefile="Makefile", expected_failure=False, variables={}): + """ + Given the path to a Makefile of some kind, return a dictionary of all variables defined in this Makefile + + Uses 'make' to parse the Makefile syntax, so we don't have to! + + Overrides IDF_PATH= to avoid recursively evaluating the entire project Makefile structure. + """ + variable_setters = [ ("%s=%s" % (k,v)) for (k,v) in variables.items() ] + + cmdline = ["make", "-rpn", "-C", path, "-f", makefile ] + variable_setters + if debug: + print("Running %s..." % (" ".join(cmdline))) + + p = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (output, stderr) = p.communicate("\n") + + if (not expected_failure) and p.returncode != 0: + raise RuntimeError("Unexpected make failure, result %d" % p.returncode) + + if debug: + print("Make stdout:") + print(output) + print("Make stderr:") + print(stderr) + + next_is_makefile = False # is the next line a makefile variable? + result = {} + BUILT_IN_VARS = set(["MAKEFILE_LIST", "SHELL", "CURDIR", "MAKEFLAGS"]) + + for line in output.decode().split("\n"): + if line.startswith("# makefile"): # this line appears before any variable defined in the makefile itself + next_is_makefile = True + elif next_is_makefile: + next_is_makefile = False + m = re.match(r"(?P[^ ]+) :?= (?P.+)", line) + if m is not None: + if not m.group("var") in BUILT_IN_VARS: + result[m.group("var")] = m.group("val").strip() + + return result + +def get_component_variables(project_path, component_path): + make_vars = get_make_variables(component_path, + os.path.join(os.environ["IDF_PATH"], + "make", + "component_wrapper.mk"), + expected_failure=True, + variables = { + "COMPONENT_MAKEFILE" : os.path.join(component_path, "component.mk"), + "COMPONENT_NAME" : os.path.basename(component_path), + "PROJECT_PATH": project_path, + }) + + if "COMPONENT_OBJS" in make_vars: # component.mk specifies list of object files + # Convert to sources + def find_src(obj): + obj = os.path.splitext(obj)[0] + for ext in [ "c", "cpp", "S" ]: + if os.path.exists(os.path.join(component_path, obj) + "." + ext): + return obj + "." + ext + print("WARNING: Can't find source file for component %s COMPONENT_OBJS %s" % (component_path, obj)) + return None + + srcs = [] + for obj in make_vars["COMPONENT_OBJS"].split(" "): + src = find_src(obj) + if src is not None: + srcs.append(src) + make_vars["COMPONENT_SRCS"] = " ".join(srcs) + else: # Use COMPONENT_SRCDIRS + make_vars["COMPONENT_SRCDIRS"] = make_vars.get("COMPONENT_SRCDIRS", ".") + + make_vars["COMPONENT_ADD_INCLUDEDIRS"] = make_vars.get("COMPONENT_ADD_INCLUDEDIRS", "include") + + return make_vars + + +def convert_project(project_path): + if not os.path.exists(project_path): + raise RuntimeError("Project directory '%s' not found" % project_path) + if not os.path.exists(os.path.join(project_path, "Makefile")): + raise RuntimeError("Directory '%s' doesn't contain a project Makefile" % project_path) + + project_cmakelists = os.path.join(project_path, "CMakeLists.txt") + if os.path.exists(project_cmakelists): + raise RuntimeError("This project already has a CMakeLists.txt file") + + project_vars = get_make_variables(project_path, expected_failure=True) + if not "PROJECT_NAME" in project_vars: + raise RuntimeError("PROJECT_NAME does not appear to be defined in IDF project Makefile at %s" % project_path) + + component_paths = project_vars["COMPONENT_PATHS"].split(" ") + + # "main" component is made special in cmake, so extract it from the component_paths list + try: + main_component_path = [ p for p in component_paths if os.path.basename(p) == "main" ][0] + if debug: + print("Found main component %s" % main_component_path) + main_vars = get_component_variables(project_path, main_component_path) + except IndexError: + print("WARNING: Project has no 'main' component, but CMake-based system requires at least one file in MAIN_SRCS...") + main_vars = { "COMPONENT_SRCS" : ""} # dummy for MAIN_SRCS + + # Remove main component from list of components we're converting to cmake + component_paths = [ p for p in component_paths if os.path.basename(p) != "main" ] + + # Convert components as needed + for p in component_paths: + convert_component(project_path, p) + + # Look up project variables before we start writing the file, so nothing + # is created if there is an error + + main_srcs = main_vars["COMPONENT_SRCS"].split(" ") + # convert from component-relative to absolute paths + main_srcs = [ os.path.normpath(os.path.join(main_component_path, m)) for m in main_srcs ] + # convert to make relative to the project directory + main_srcs = [ os.path.relpath(m, project_path) for m in main_srcs ] + + project_name = project_vars["PROJECT_NAME"] + + # Generate the project CMakeLists.txt file + with open(project_cmakelists, "w") as f: + f.write(""" +# (Automatically converted from project Makefile by convert_to_cmake.py.) + +# The following four lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +""") + f.write("set(MAIN_SRCS %s)\n" % " ".join(main_srcs)) + f.write(""" +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +""") + f.write("project(%s)\n" % project_name) + + print("Converted project %s" % project_cmakelists) + +def convert_component(project_path, component_path): + if debug: + print("Converting %s..." % (component_path)) + cmakelists_path = os.path.join(component_path, "CMakeLists.txt") + if os.path.exists(cmakelists_path): + print("Skipping already-converted component %s..." % cmakelists_path) + return + v = get_component_variables(project_path, component_path) + + # Look up all the variables before we start writing the file, so it's not + # created if there's an erro + component_srcs = v.get("COMPONENT_SRCS", None) + component_srcdirs = None + if component_srcs is not None: + # see if we should be using COMPONENT_SRCS or COMPONENT_SRCDIRS, if COMPONENT_SRCS is everything in SRCDIRS + component_allsrcs = [] + for d in v.get("COMPONENT_SRCDIRS", "").split(" "): + component_allsrcs += glob.glob(os.path.normpath(os.path.join(component_path, d, "*.[cS]"))) + component_allsrcs += glob.glob(os.path.normpath(os.path.join(component_path, d, "*.cpp"))) + abs_component_srcs = [os.path.normpath(os.path.join(component_path, p)) for p in component_srcs.split(" ")] + if set(component_allsrcs) == set(abs_component_srcs): + component_srcdirs = v.get("COMPONENT_SRCDIRS") + + component_add_includedirs = v["COMPONENT_ADD_INCLUDEDIRS"] + cflags = v.get("CFLAGS", None) + + with open(cmakelists_path, "w") as f: + f.write("set(COMPONENT_ADD_INCLUDEDIRS %s)\n\n" % component_add_includedirs) + + f.write("# Edit following two lines to set component requirements (see docs)\n") + f.write("set(COMPONENT_REQUIRES "")\n") + f.write("set(COMPONENT_PRIV_REQUIRES "")\n\n") + + if component_srcdirs is not None: + f.write("set(COMPONENT_SRCDIRS %s)\n\n" % component_srcdirs) + f.write("register_component()\n") + elif component_srcs is not None: + f.write("set(COMPONENT_SRCS %s)\n\n" % component_srcs) + f.write("register_component()\n") + else: + f.write("register_config_only_component()\n") + if cflags is not None: + f.write("component_compile_options(%s)\n" % cflags) + + print("Converted %s" % cmakelists_path) + + +def main(): + global debug + + parser = argparse.ArgumentParser(description='convert_to_cmake.py - ESP-IDF Project Makefile to CMakeLists.txt converter', prog='convert_to_cmake') + + parser.add_argument('--debug', help='Display debugging output', + action='store_true') + + parser.add_argument('project', help='Path to project to convert (defaults to CWD)', default=os.getcwd(), metavar='project path', nargs='?') + + args = parser.parse_args() + debug = args.debug + print("Converting %s..." % args.project) + convert_project(args.project) + + +if __name__ == "__main__": + main() diff --git a/tools/cmake/crosstool_version_check.cmake b/tools/cmake/crosstool_version_check.cmake new file mode 100644 index 00000000..d572a82a --- /dev/null +++ b/tools/cmake/crosstool_version_check.cmake @@ -0,0 +1,32 @@ +# Function to check the toolchain used the expected version +# of crosstool, and warn otherwise + +set(ctng_version_warning "Check Getting Started documentation or proceed at own risk.") + +function(gcc_version_check expected_gcc_version) + if(NOT "${CMAKE_C_COMPILER_VERSION}" STREQUAL "${expected_gcc_version}") + message(WARNING "Xtensa toolchain ${CMAKE_C_COMPILER} version ${CMAKE_C_COMPILER_VERSION} " + "is not the supported version ${expected_gcc_version}. ${ctng_version_warning}") + endif() +endfunction() + +function(crosstool_version_check expected_ctng_version) + execute_process( + COMMAND ${CMAKE_C_COMPILER} -v + ERROR_VARIABLE toolchain_stderr + OUTPUT_QUIET) + + string(REGEX MATCH "crosstool-ng-[0-9a-g\\.-]+" ctng_version "${toolchain_stderr}") + string(REPLACE "crosstool-ng-" "" ctng_version "${ctng_version}") + # We use FIND to match version instead of STREQUAL because some toolchains are built + # with longer git hash strings than others. This will match any version which starts with + # the expected version string. + string(FIND "${ctng_version}" "${expected_ctng_version}" found_expected_version) + if(NOT ctng_version) + message(WARNING "Xtensa toolchain ${CMAKE_C_COMPILER} does not appear to be built with crosstool-ng. " + "${ctng_version_warning}") + elseif(found_expected_version EQUAL -1) + message(WARNING "Xtensa toolchain ${CMAKE_C_COMPILER} crosstool-ng version ${ctng_version} " + "doesn't match supported version ${expected_ctng_version}. ${ctng_version_warning}") + endif() +endfunction() diff --git a/tools/cmake/git_submodules.cmake b/tools/cmake/git_submodules.cmake new file mode 100644 index 00000000..09098b24 --- /dev/null +++ b/tools/cmake/git_submodules.cmake @@ -0,0 +1,61 @@ +find_package(Git) + +if(NOT GIT_FOUND) + message(WARNING "Git executable was not found. Git submodule checks will not be executed. " + "If you have any build issues at all, start by adding git executable to the PATH and " + "rerun cmake to not see this warning again.") + + function(git_submodule_check root_path) + # no-op + endfunction() +else() + + function(git_submodule_check root_path) + + execute_process( + COMMAND ${GIT_EXECUTABLE} submodule status + WORKING_DIRECTORY ${root_path} + OUTPUT_VARIABLE submodule_status + ) + + # git submodule status output not guaranteed to be stable, + # may need to check GIT_VERSION_STRING and do some fiddling in the + # future... + + lines2list(submodule_status) + + foreach(line ${submodule_status}) + string(REGEX MATCH "(.)[0-9a-f]+ ([^\( ]+) ?" _ignored "${line}") + set(status "${CMAKE_MATCH_1}") + set(submodule_path "${CMAKE_MATCH_2}") + + if("${status}" STREQUAL "-") # missing submodule + message(STATUS "Initialising new submodule ${submodule_path}...") + execute_process( + COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive ${submodule_path} + WORKING_DIRECTORY ${root_path} + RESULT_VARIABLE git_result + ) + if(git_result) + message(FATAL_ERROR "Git submodule init failed for ${submodule_path}") + endif() + + elseif(NOT "${status}" STREQUAL " ") + message(WARNING "Git submodule ${submodule_path} is out of date. " + "Run 'git submodule update --init --recursive' to fix.") + endif() + + # Force a re-run of cmake if the submodule's .git file changes or is changed (ie accidental deinit) + get_filename_component(submodule_abs_path ${submodule_path} ABSOLUTE BASE_DIR ${root_path}) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${submodule_abs_path}/.git) + # same if the HEAD file in the submodule's directory changes (ie commit changes). + # This will at least print the 'out of date' warning + set(submodule_head "${root_path}/.git/modules/${submodule_path}/HEAD") + if(EXISTS "${submodule_head}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${submodule_head}) + endif() + + endforeach() + endfunction() + +endif() diff --git a/tools/cmake/idf_functions.cmake b/tools/cmake/idf_functions.cmake new file mode 100644 index 00000000..3415f9da --- /dev/null +++ b/tools/cmake/idf_functions.cmake @@ -0,0 +1,203 @@ +# Some IDF-specific functions and functions + +include(crosstool_version_check) + +# +# Set some variables used by rest of the build +# +# Note at the time this macro is expanded, the config is not yet +# loaded and the toolchain and project are not yet set +# +macro(idf_set_global_variables) + # Note that CONFIG_xxx is not available when this function is called + + set_default(EXTRA_COMPONENT_DIRS "") + + # Commmon components, required by every component in the build + # + set_default(COMPONENT_REQUIRES_COMMON "cxx esp32 newlib freertos heap log soc") + + # PROJECT_PATH has the path to the IDF project (top-level cmake directory) + # + # (cmake calls this CMAKE_SOURCE_DIR, keeping old name for compatibility.) + set(PROJECT_PATH "${CMAKE_SOURCE_DIR}") + + # Note: Unlike older build system, "main" is no longer a component. See build docs for details. + set_default(COMPONENT_DIRS "${PROJECT_PATH}/components ${EXTRA_COMPONENT_DIRS} ${IDF_PATH}/components") + spaces2list(COMPONENT_DIRS) + + spaces2list(COMPONENTS) + + # Tell cmake to drop executables in the top-level build dir + set(EXECUTABLE_OUTPUT_PATH "${CMAKE_BINARY_DIR}") + + # path to idf.py tool + set(IDFTOOL ${PYTHON} "${IDF_PATH}/tools/idf.py") +endmacro() + +# Add all the IDF global compiler & preprocessor options +# (applied to all components). Some are config-dependent +# +# If you only want to set options for a particular component, +# don't call or edit this function. TODO DESCRIBE WHAT TO DO INSTEAD +# +function(idf_set_global_compiler_options) + add_definitions(-DESP_PLATFORM) + add_definitions(-DHAVE_CONFIG_H) + + if(CONFIG_OPTIMIZATION_LEVEL_RELEASE) + add_compile_options(-Os) + else() + add_compile_options(-Og) + endif() + + add_c_compile_options(-std=gnu99) + + add_cxx_compile_options(-std=gnu++11 -fno-rtti) + + if(CONFIG_CXX_EXCEPTIONS) + add_cxx_compile_options(-fexceptions) + else() + add_cxx_compile_options(-fno-exceptions) + endif() + + # Default compiler configuration + add_compile_options(-ffunction-sections -fdata-sections -fstrict-volatile-bitfields -mlongcalls -nostdlib) + + # Default warnings configuration + add_compile_options( + -Wall + -Werror=all + -Wno-error=unused-function + -Wno-error=unused-but-set-variable + -Wno-error=unused-variable + -Wno-error=deprecated-declarations + -Wextra + -Wno-unused-parameter + -Wno-sign-compare) + add_c_compile_options( + -Wno-old-style-declaration + ) + + # Stack protection + if(NOT BOOTLOADER_BUILD) + if(CONFIG_STACK_CHECK_NORM) + add_compile_options(-fstack-protector) + elseif(CONFIG_STACK_CHECK_STRONG) + add_compile_options(-fstack-protector-strong) + elseif(CONFIG_STACK_CHECK_ALL) + add_compile_options(-fstack-protector-all) + endif() + endif() + + if(CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED) + add_definitions(-DNDEBUG) + endif() + + # Always generate debug symbols (even in Release mode, these don't + # go into the final binary so have no impact on size) + add_compile_options(-ggdb) + + add_compile_options("-I${CMAKE_BINARY_DIR}") # for sdkconfig.h + + # Enable ccache if it's on the path + if(NOT CCACHE_DISABLE) + find_program(CCACHE_FOUND ccache) + if(CCACHE_FOUND) + message(STATUS "ccache will be used for faster builds") + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) + endif() + endif() + +endfunction() + + +# Verify the IDF environment is configured correctly (environment, toolchain, etc) +function(idf_verify_environment) + + if(NOT CMAKE_PROJECT_NAME) + message(FATAL_ERROR "Internal error, IDF project.cmake should have set this variable already") + endif() + + # Check toolchain is configured properly in cmake + if(NOT ( ${CMAKE_SYSTEM_NAME} STREQUAL "Generic" AND ${CMAKE_C_COMPILER} MATCHES xtensa)) + message(FATAL_ERROR "Internal error, toolchain has not been set correctly by project") + endif() + + # + # Warn if the toolchain version doesn't match + # + # TODO: make these platform-specific for diff toolchains + gcc_version_check("5.2.0") + crosstool_version_check("1.22.0-80-g6c4433a") + +endfunction() + +# idf_add_executable +# +# Calls add_executable to add the final project executable +# Adds .map & .bin file targets +# Sets up flash-related targets +function(idf_add_executable) + set(exe_target ${PROJECT_NAME}.elf) + + spaces2list(MAIN_SRCS) + add_executable(${exe_target} "${MAIN_SRCS}") + + add_map_file(${exe_target}) +endfunction() + + +# add_map_file +# +# Set linker args for 'exe_target' to generate a linker Map file +function(add_map_file exe_target) + get_filename_component(basename ${exe_target} NAME_WE) + set(mapfile "${basename}.map") + target_link_libraries(${exe_target} "-Wl,--gc-sections -Wl,--cref -Wl,--Map=${mapfile} -Wl,--start-group") + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY + ADDITIONAL_MAKE_CLEAN_FILES + "${CMAKE_CURRENT_BINARY_DIR}/${mapfile}") + + # add size targets, depend on map file, run idf_size.py + add_custom_target(size + DEPENDS ${exe_target} + COMMAND ${PYTHON} ${IDF_PATH}/tools/idf_size.py ${mapfile} + ) + add_custom_target(size-files + DEPENDS ${exe_target} + COMMAND ${PYTHON} ${IDF_PATH}/tools/idf_size.py --files ${mapfile} + ) + add_custom_target(size-components + DEPENDS ${exe_target} + COMMAND ${PYTHON} ${IDF_PATH}/tools/idf_size.py --archives ${mapfile} + ) + +endfunction() + +# component_compile_options +# +# Wrapper around target_compile_options that passes the component name +function(component_compile_options) + target_compile_options(${COMPONENT_NAME} PRIVATE ${ARGV}) +endfunction() + +# component_compile_definitions +# +# Wrapper around target_compile_definitions that passes the component name +function(component_compile_definitions) + target_compile_definitions(${COMPONENT_NAME} PRIVATE ${ARGV}) +endfunction() + +# idf_get_git_revision +# +# Set global IDF_VER to the git revision of ESP-IDF. +# +# Running git_describe() here automatically triggers rebuilds +# if the ESP-IDF git version changes +function(idf_get_git_revision) + git_describe(IDF_VER "${IDF_PATH}") + add_definitions(-DIDF_VER=\"${IDF_VER}\") + git_submodule_check("${IDF_PATH}") + set(IDF_VER ${IDF_VER} PARENT_SCOPE) +endfunction() diff --git a/tools/cmake/kconfig.cmake b/tools/cmake/kconfig.cmake new file mode 100644 index 00000000..eeb8ce11 --- /dev/null +++ b/tools/cmake/kconfig.cmake @@ -0,0 +1,130 @@ +include(ExternalProject) + +macro(kconfig_set_variables) + set_default(SDKCONFIG ${PROJECT_PATH}/sdkconfig) + set(SDKCONFIG_HEADER ${CMAKE_BINARY_DIR}/sdkconfig.h) + set(SDKCONFIG_CMAKE ${CMAKE_BINARY_DIR}/sdkconfig.cmake) + set(SDKCONFIG_JSON ${CMAKE_BINARY_DIR}/sdkconfig.json) + + set(ROOT_KCONFIG ${IDF_PATH}/Kconfig) + + set_default(SDKCONFIG_DEFAULTS "${SDKCONFIG}.defaults") +endmacro() + +if(CMAKE_HOST_WIN32) + # Prefer a prebuilt mconf on Windows + find_program(WINPTY winpty) + find_program(MCONF mconf) + + if(NOT MCONF) + find_program(NATIVE_GCC gcc) + if(NOT NATIVE_GCC) + message(FATAL_ERROR + "Windows requires a prebuilt ESP-IDF-specific mconf for your platform " + "on the PATH, or an MSYS2 version of gcc on the PATH to build mconf. " + "Consult the setup docs for ESP-IDF on Windows.") + endif() + elseif(WINPTY) + set(MCONF "${WINPTY}" "${MCONF}") + endif() +endif() + +if(NOT MCONF) + # Use the existing Makefile to build mconf (out of tree) when needed + # + set(MCONF kconfig_bin/mconf) + + externalproject_add(mconf + SOURCE_DIR ${IDF_PATH}/tools/kconfig + CONFIGURE_COMMAND "" + BINARY_DIR "kconfig_bin" + BUILD_COMMAND make -f ${IDF_PATH}/tools/kconfig/Makefile mconf + BUILD_BYPRODUCTS ${MCONF} + INSTALL_COMMAND "" + EXCLUDE_FROM_ALL 1 + ) + set(menuconfig_depends DEPENDS mconf) +endif() + +# Find all Kconfig files for all components +function(kconfig_process_config) + file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/include/config") + set(kconfigs) + set(kconfigs_projbuild) + + # Find Kconfig and Kconfig.projbuild for each component as applicable + # if any of these change, cmake should rerun + foreach(dir ${BUILD_COMPONENT_PATHS} "${CMAKE_SOURCE_DIR}/main") + file(GLOB kconfig "${dir}/Kconfig") + if(kconfig) + set(kconfigs "${kconfigs} ${kconfig}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${kconfig}) + endif() + file(GLOB kconfig ${dir}/Kconfig.projbuild) + if(kconfig) + set(kconfigs_projbuild "${kconfigs_projbuild} ${kconfig}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${kconfig}) + endif() + endforeach() + + if(EXISTS ${SDKCONFIG_DEFAULTS}) + set(defaults_arg --defaults "${SDKCONFIG_DEFAULTS}") + endif() + + # Set these in the parent scope, so that they can be written to project_description.json + set(kconfigs "${kconfigs}") + set(COMPONENT_KCONFIGS "${kconfigs}" PARENT_SCOPE) + set(COMPONENT_KCONFIGS_PROJBUILD "${kconfigs_projbuild}" PARENT_SCOPE) + + set(confgen_basecommand + ${PYTHON} ${IDF_PATH}/tools/kconfig_new/confgen.py + --kconfig ${ROOT_KCONFIG} + --config ${SDKCONFIG} + ${defaults_arg} + --create-config-if-missing + --env "COMPONENT_KCONFIGS=${kconfigs}" + --env "COMPONENT_KCONFIGS_PROJBUILD=${kconfigs_projbuild}") + + # Generate the menuconfig target (uses C-based mconf tool, either prebuilt or via mconf target above) + add_custom_target(menuconfig + ${menuconfig_depends} + # create any missing config file, with defaults if necessary + COMMAND ${confgen_basecommand} --output config ${SDKCONFIG} + COMMAND ${CMAKE_COMMAND} -E env + "COMPONENT_KCONFIGS=${kconfigs}" + "COMPONENT_KCONFIGS_PROJBUILD=${kconfigs_projbuild}" + "KCONFIG_CONFIG=${SDKCONFIG}" + ${MCONF} ${ROOT_KCONFIG} + VERBATIM + USES_TERMINAL) + + # Generate configuration output via confgen.py + # makes sdkconfig.h and skdconfig.cmake + # + # This happens during the cmake run not during the build + execute_process(COMMAND ${confgen_basecommand} + --output header ${SDKCONFIG_HEADER} + --output cmake ${SDKCONFIG_CMAKE} + --output json ${SDKCONFIG_JSON} + RESULT_VARIABLE config_result) + if(config_result) + message(FATAL_ERROR "Failed to run confgen.py (${confgen_basecommand}). Error ${config_result}") + endif() + + # When sdkconfig file changes in the future, trigger a cmake run + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SDKCONFIG}") + + # Ditto if either of the generated files are missing/modified (this is a bit irritating as it means + # you can't edit these manually without them being regenerated, but I don't know of a better way...) + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SDKCONFIG_HEADER}") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${SDKCONFIG_CMAKE}") + + # Or if the config generation tool changes + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${IDF_PATH}/tools/kconfig_new/confgen.py") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${IDF_PATH}/tools/kconfig_new/kconfiglib.py") + + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY + ADDITIONAL_MAKE_CLEAN_FILES + "${SDKCONFIG_HEADER}" "${SDKCONFIG_CMAKE}") + +endfunction() diff --git a/tools/cmake/project.cmake b/tools/cmake/project.cmake new file mode 100644 index 00000000..ff344a96 --- /dev/null +++ b/tools/cmake/project.cmake @@ -0,0 +1,140 @@ +# Designed to be included from an IDF app's CMakeLists.txt file +# +cmake_minimum_required(VERSION 3.5) + +# Set IDF_PATH, as nothing else will work without this +set(IDF_PATH "$ENV{IDF_PATH}") +if(NOT IDF_PATH) + # Documentation says you should set IDF_PATH in your environment, but we + # can infer it here if it's not set. + set(IDF_PATH ${CMAKE_CURRENT_LIST_DIR}) +endif() +file(TO_CMAKE_PATH "${IDF_PATH}" IDF_PATH) +set($ENV{IDF_PATH} "${IDF_PATH}") + +# +# Load cmake modules +# +set(CMAKE_MODULE_PATH + "${IDF_PATH}/tools/cmake" + "${IDF_PATH}/tools/cmake/third_party" + ${CMAKE_MODULE_PATH}) +include(GetGitRevisionDescription) +include(utilities) +include(components) +include(kconfig) +include(git_submodules) +include(idf_functions) + +set_default(PYTHON "python") + +# project +# +# This macro wraps the cmake 'project' command to add +# all of the IDF-specific functionality required +# +# Implementation Note: This macro wraps 'project' on purpose, because cmake has +# some backwards-compatible magic where if you don't call "project" in the +# top-level CMakeLists file, it will call it implicitly. However, the implicit +# project will not have CMAKE_TOOLCHAIN_FILE set and therefore tries to +# create a native build project. +# +# Therefore, to keep all the IDF "build magic", the cleanest way is to keep the +# top-level "project" call but customize it to do what we want in the IDF build. +# +macro(project name) + # Set global variables used by rest of the build + idf_set_global_variables() + + # Establish dependencies for components in the build + # (this happens before we even generate config...) + if(COMPONENTS) + # Make sure if an explicit list of COMPONENTS is given, it contains the "common" component requirements + # (otherwise, if COMPONENTS is empty then all components will be included in the build.) + set(COMPONENTS "${COMPONENTS} ${COMPONENT_REQUIRES_COMMON}") + endif() + execute_process(COMMAND "${CMAKE_COMMAND}" + -D "COMPONENTS=${COMPONENTS}" + -D "DEPENDENCIES_FILE=${CMAKE_BINARY_DIR}/component_depends.cmake" + -D "COMPONENT_DIRS=${COMPONENT_DIRS}" + -D "BOOTLOADER_BUILD=${BOOTLOADER_BUILD}" + -P "${IDF_PATH}/tools/cmake/scripts/expand_requirements.cmake" + WORKING_DIRECTORY "${IDF_PATH}/tools/cmake") + include("${CMAKE_BINARY_DIR}/component_depends.cmake") + + # We now have the following component-related variables: + # COMPONENTS is the list of initial components set by the user (or empty to include all components in the build). + # BUILD_COMPONENTS is the list of components to include in the build. + # BUILD_COMPONENT_PATHS is the paths to all of these components. + + # Print list of components + string(REPLACE ";" " " BUILD_COMPONENTS_SPACES "${BUILD_COMPONENTS}") + message(STATUS "Component names: ${BUILD_COMPONENTS_SPACES}") + unset(BUILD_COMPONENTS_SPACES) + message(STATUS "Component paths: ${BUILD_COMPONENT_PATHS}") + + kconfig_set_variables() + + kconfig_process_config() + + # Include sdkconfig.cmake so rest of the build knows the configuration + include(${SDKCONFIG_CMAKE}) + + # Now the configuration is loaded, set the toolchain appropriately + # + # TODO: support more toolchains than just ESP32 + set(CMAKE_TOOLCHAIN_FILE $ENV{IDF_PATH}/tools/cmake/toolchain-esp32.cmake) + + # Declare the actual cmake-level project + _project(${name} ASM C CXX) + + # generate compile_commands.json (needs to come after project) + set(CMAKE_EXPORT_COMPILE_COMMANDS 1) + + # Verify the environment is configured correctly + idf_verify_environment() + + # Add some idf-wide definitions + idf_set_global_compiler_options() + + # Check git revision (may trigger reruns of cmake) + ## sets IDF_VER to IDF git revision + idf_get_git_revision() + ## if project uses git, retrieve revision + git_describe(PROJECT_VER "${CMAKE_CURRENT_SOURCE_DIR}") + + # Include any top-level project_include.cmake files from components + foreach(component ${BUILD_COMPONENT_PATHS}) + include_if_exists("${component}/project_include.cmake") + endforeach() + + # + # Add each component to the build as a library + # + foreach(COMPONENT_PATH ${BUILD_COMPONENT_PATHS}) + get_filename_component(COMPONENT_NAME ${COMPONENT_PATH} NAME) + add_subdirectory(${COMPONENT_PATH} ${COMPONENT_NAME}) + endforeach() + unset(COMPONENT_NAME) + unset(COMPONENT_PATH) + + # + # Add the app executable to the build (has name of PROJECT.elf) + # + idf_add_executable() + + # Write project description JSON file + make_json_list("${BUILD_COMPONENTS}" build_components_json) + make_json_list("${BUILD_COMPONENT_PATHS}" build_component_paths_json) + configure_file("${IDF_PATH}/tools/cmake/project_description.json.in" + "${CMAKE_BINARY_DIR}/project_description.json") + unset(build_components_json) + unset(build_component_paths_json) + + # + # Finish component registration (add cross-dependencies, make + # executable dependent on all components) + # + components_finish_registration() + +endmacro() diff --git a/tools/cmake/project_description.json.in b/tools/cmake/project_description.json.in new file mode 100644 index 00000000..878dce3b --- /dev/null +++ b/tools/cmake/project_description.json.in @@ -0,0 +1,18 @@ +{ + "project_name": "${PROJECT_NAME}", + "project_path": "${PROJECT_PATH}", + "build_dir": "${CMAKE_BINARY_DIR}", + "config_file": "${SDKCONFIG}", + "config_defaults": "${SDKCONFIG_DEFAULTS}", + "app_elf": "${PROJECT_NAME}.elf", + "app_bin": "${PROJECT_NAME}.bin", + "git_revision": "${IDF_VER}", + "phy_data_partition": "${CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION}", + "monitor_baud" : "${CONFIG_MONITOR_BAUD}", + "config_environment" : { + "COMPONENT_KCONFIGS" : "${COMPONENT_KCONFIGS}", + "COMPONENT_KCONFIGS_PROJBUILD" : "${COMPONENT_KCONFIGS_PROJBUILD}" + }, + "build_components" : ${build_components_json}, + "build_component_paths" : ${build_component_paths_json} +} diff --git a/tools/cmake/run_cmake_lint.sh b/tools/cmake/run_cmake_lint.sh new file mode 100755 index 00000000..65f49eed --- /dev/null +++ b/tools/cmake/run_cmake_lint.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# Run cmakelint on all cmake files in IDF_PATH (except third party) +# +# cmakelint: https://github.com/richq/cmake-lint +# +# NOTE: This script makes use of features in (currently unreleased) +# cmakelint >1.4. Install directly from github as follows: +# +# pip install https://github.com/richq/cmake-lint/archive/058c6c0ed2536.zip +# + +if [ -z "${IDF_PATH}" ]; then + echo "IDF_PATH variable needs to be set" + exit 3 +fi + +# exclusions include some third-party directories which contain upstream +# CMakeLists files +find ${IDF_PATH} \ + -name build -prune \ + -o -name third_party -prune \ + \ + -o -name 'nghttp2' -prune \ + -o -name 'cJSON' -prune \ + -o -name 'Findsodium.cmake' -prune \ + \ + -o -name CMakeLists.txt -print0 \ + -o -name '*.cmake' -print0 \ + | xargs -0 cmakelint --linelength=120 --spaces=4 + + diff --git a/tools/cmake/scripts/data_file_embed_asm.cmake b/tools/cmake/scripts/data_file_embed_asm.cmake new file mode 100644 index 00000000..71329ade --- /dev/null +++ b/tools/cmake/scripts/data_file_embed_asm.cmake @@ -0,0 +1,82 @@ +# +# Convert a file (text or binary) into an assembler source file suitable +# for gcc. Designed to replicate 'objcopy' with more predictable +# naming, and supports appending a null byte for embedding text as +# a string. +# +# Designed to be run as a script with "cmake -P" +# +# Set variables DATA_FILE, SOURCE_FILE, FILE_TYPE when running this. +# +# If FILE_TYPE is set to TEXT, a null byte is appended to DATA_FILE's contents +# before SOURCE_FILE is created. +# +# If FILE_TYPE is unset (or any other value), DATA_FILE is copied +# verbatim into SOURCE_FILE. +# +# +if(NOT DATA_FILE) + message(FATAL_ERROR "DATA_FILE for converting must be specified") +endif() + +if(NOT SOURCE_FILE) + message(FATAL_ERROR "SOURCE_FILE destination must be specified") +endif() + +file(READ "${DATA_FILE}" data HEX) + +string(LENGTH "${data}" data_len) +math(EXPR data_len "${data_len} / 2") # 2 hex bytes per byte + +if(FILE_TYPE STREQUAL "TEXT") + set(data "${data}00") # null-byte termination +endif() + +## Convert string of raw hex bytes to lines of hex bytes as gcc .byte expressions +string(REGEX REPLACE "................................" ".byte \\0\n" data "${data}") # 16 bytes per line +string(REGEX REPLACE "[^\n]+$" ".byte \\0\n" data "${data}") # last line +string(REGEX REPLACE "[0-9a-f][0-9a-f]" "0x\\0, " data "${data}") # hex formatted C bytes +string(REGEX REPLACE ", \n" "\n" data "${data}") # trim the last comma + +## Come up with C-friendly symbol name based on source file +get_filename_component(source_filename "${DATA_FILE}" NAME) +string(MAKE_C_IDENTIFIER "${source_filename}" varname) + +function(append str) + file(APPEND "${SOURCE_FILE}" "${str}") +endfunction() + +function(append_line str) + append("${str}\n") +endfunction() + +function(append_identifier symbol) +append_line("\n.global ${symbol}") +append("${symbol}:") +if(${ARGC} GREATER 1) # optional comment + append(" /* ${ARGV1} */") +endif() +append("\n") +endfunction() + +file(WRITE "${SOURCE_FILE}" "/*") +append_line(" * Data converted from ${DATA_FILE}") +if(FILE_TYPE STREQUAL "TEXT") + append_line(" * (null byte appended)") +endif() +append_line(" */") + +append_line(".data") +append_identifier("${varname}") +append_identifier("_binary_${varname}_start" "for objcopy compatibility") +append("${data}") + +append_identifier("_binary_${varname}_end" "for objcopy compatibility") + +append_line("") +if(FILE_TYPE STREQUAL "TEXT") + append_identifier("${varname}_length" "not including null byte") +else() + append_identifier("${varname}_length") +endif() +append_line(".word ${data_len}") diff --git a/tools/cmake/scripts/expand_requirements.cmake b/tools/cmake/scripts/expand_requirements.cmake new file mode 100644 index 00000000..9b56cf09 --- /dev/null +++ b/tools/cmake/scripts/expand_requirements.cmake @@ -0,0 +1,216 @@ +# expand_requires.cmake is a utility cmake script to expand component requirements early in the build, +# before the components are ready to be included. +# +# Parameters: +# - COMPONENTS = Space-separated list of initial components to include in the build. +# Can be empty, in which case all components are in the build. +# - DEPENDENCIES_FILE = Path of generated cmake file which will contain the expanded dependencies for these +# components. +# - COMPONENT_DIRS = List of paths to search for all components. +# - DEBUG = Set -DDEBUG=1 to debug component lists in the build. +# +# If successful, DEPENDENCIES_FILE can be expanded to set BUILD_COMPONENTS & BUILD_COMPONENT_PATHS with all +# components required for the build, and the get_component_requirements() function to return each component's +# recursively expanded requirements. +# +# TODO: Error out if a component requirement is missing +cmake_minimum_required(VERSION 3.5) +include("utilities.cmake") + +if(NOT DEPENDENCIES_FILE) + message(FATAL_ERROR "DEPENDENCIES_FILE must be set.") +endif() + +if(NOT COMPONENT_DIRS) + message(FATAL_ERROR "COMPONENT_DIRS variable must be set") +endif() +spaces2list(COMPONENT_DIRS) + +function(debug message) + if(DEBUG) + message(STATUS "${message}") + endif() +endfunction() + +# Dummy register_component used to save requirements variables as global properties, for later expansion +# +# (expand_component_requirements() includes the component CMakeLists.txt, which then sets its component variables, +# calls this dummy macro, and immediately exits again.) +macro(register_component) + spaces2list(COMPONENT_REQUIRES) + set_property(GLOBAL PROPERTY "${COMPONENT}_REQUIRES" "${COMPONENT_REQUIRES}") + spaces2list(COMPONENT_PRIV_REQUIRES) + set_property(GLOBAL PROPERTY "${COMPONENT}_PRIV_REQUIRES" "${COMPONENT_PRIV_REQUIRES}") + + # This is tricky: we override register_component() so it returns out of the component CMakeLists.txt + # (as we're declaring it as a macro not a function, so it doesn't have its own scope.) + # + # This means no targets are defined, and the component expansion ends early. + return() +endmacro() + +macro(register_config_only_component) + register_component() +endmacro() + +# Given a component name (find_name) and a list of component paths (component_paths), +# return the path to the component in 'variable' +# +# Fatal error is printed if the component is not found. +function(find_component_path find_name component_paths variable) + foreach(path ${component_paths}) + get_filename_component(name "${path}" NAME) + if("${name}" STREQUAL "${find_name}") + set("${variable}" "${path}" PARENT_SCOPE) + return() + endif() + endforeach() + # TODO: find a way to print the dependency chain that lead to this not-found component + message(WARNING "Required component ${find_name} is not found in any of the provided COMPONENT_DIRS") +endfunction() + +# components_find_all: Search 'component_dirs' for components and return them +# as a list of names in 'component_names' and a list of full paths in +# 'component_paths' +# +# component_paths contains only unique component names. Directories +# earlier in the component_dirs list take precedence. +function(components_find_all component_dirs component_paths component_names) + # component_dirs entries can be files or lists of files + set(paths "") + set(names "") + + # start by expanding the component_dirs list with all subdirectories + foreach(dir ${component_dirs}) + # Iterate any subdirectories for values + file(GLOB subdirs LIST_DIRECTORIES true "${dir}/*") + foreach(subdir ${subdirs}) + set(component_dirs "${component_dirs};${subdir}") + endforeach() + endforeach() + + # Look for a component in each component_dirs entry + foreach(dir ${component_dirs}) + file(GLOB component "${dir}/CMakeLists.txt") + if(component) + get_filename_component(component "${component}" DIRECTORY) + get_filename_component(name "${component}" NAME) + if(NOT name IN_LIST names) + set(names "${names};${name}") + set(paths "${paths};${component}") + endif() + + else() # no CMakeLists.txt file + # test for legacy component.mk and warn + file(GLOB legacy_component "${dir}/component.mk") + if(legacy_component) + get_filename_component(legacy_component "${legacy_component}" DIRECTORY) + message(WARNING "Component ${legacy_component} contains old-style component.mk but no CMakeLists.txt. " + "Component will be skipped.") + endif() + endif() + + endforeach() + + set(${component_paths} ${paths} PARENT_SCOPE) + set(${component_names} ${names} PARENT_SCOPE) +endfunction() + +# expand_component_requirements: Recursively expand a component's requirements, +# setting global properties BUILD_COMPONENTS & BUILD_COMPONENT_PATHS and +# also invoking the components to call register_component() above, +# which will add per-component global properties with dependencies, etc. +function(expand_component_requirements component) + get_property(build_components GLOBAL PROPERTY BUILD_COMPONENTS) + if(${component} IN_LIST build_components) + return() # already added this component + endif() + + find_component_path("${component}" "${ALL_COMPONENT_PATHS}" component_path) + debug("Expanding dependencies of ${component} @ ${component_path}") + if(NOT component_path) + set_property(GLOBAL APPEND PROPERTY COMPONENTS_NOT_FOUND ${component}) + return() + endif() + + # include the component CMakeLists.txt to expand its properties + # into the global cache (via register_component(), above) + unset(COMPONENT_REQUIRES) + unset(COMPONENT_PRIV_REQUIRES) + set(COMPONENT ${component}) + include(${component_path}/CMakeLists.txt) + + set_property(GLOBAL APPEND PROPERTY BUILD_COMPONENT_PATHS ${component_path}) + set_property(GLOBAL APPEND PROPERTY BUILD_COMPONENTS ${component}) + + get_property(requires GLOBAL PROPERTY "${component}_REQUIRES") + get_property(requires_priv GLOBAL PROPERTY "${component}_PRIV_REQUIRES") + foreach(req ${requires} ${requires_priv}) + expand_component_requirements(${req}) + endforeach() +endfunction() + + +# Main functionality goes here + +# Find every available component in COMPONENT_DIRS, save as ALL_COMPONENT_PATHS and ALL_COMPONENTS +components_find_all("${COMPONENT_DIRS}" ALL_COMPONENT_PATHS ALL_COMPONENTS) + +if(NOT COMPONENTS) + set(COMPONENTS "${ALL_COMPONENTS}") +endif() +spaces2list(COMPONENTS) + +debug("ALL_COMPONENT_PATHS ${ALL_COMPONENT_PATHS}") +debug("ALL_COMPONENTS ${ALL_COMPONENTS}") + +set_property(GLOBAL PROPERTY BUILD_COMPONENTS "") +set_property(GLOBAL PROPERTY BUILD_COMPONENT_PATHS "") +set_property(GLOBAL PROPERTY COMPONENTS_NOT_FOUND "") + +foreach(component ${COMPONENTS}) + debug("Expanding initial component ${component}") + expand_component_requirements(${component}) +endforeach() + +get_property(build_components GLOBAL PROPERTY BUILD_COMPONENTS) +get_property(build_component_paths GLOBAL PROPERTY BUILD_COMPONENT_PATHS) +get_property(not_found GLOBAL PROPERTY COMPONENTS_NOT_FOUND) + +debug("components in build: ${build_components}") +debug("components in build: ${build_component_paths}") +debug("components not found: ${not_found}") + +function(line contents) + file(APPEND "${DEPENDENCIES_FILE}" "${contents}\n") +endfunction() + +file(WRITE "${DEPENDENCIES_FILE}" "# Component requirements generated by expand_requirements.cmake\n\n") +line("set(BUILD_COMPONENTS ${build_components})") +line("set(BUILD_COMPONENT_PATHS ${build_component_paths})") +line("") + +line("# get_component_requirements: Generated function to read the dependencies of a given component.") +line("#") +line("# Parameters:") +line("# - component: Name of component") +line("# - var_requires: output variable name. Set to recursively expanded COMPONENT_REQUIRES ") +line("# for this component.") +line("# - var_private_requires: output variable name. Set to recursively expanded COMPONENT_PRIV_REQUIRES ") +line("# for this component.") +line("#") +line("# Throws a fatal error if 'componeont' is not found (indicates a build system problem).") +line("#") +line("function(get_component_requirements component var_requires var_private_requires)") +foreach(build_component ${build_components}) + get_property(reqs GLOBAL PROPERTY "${build_component}_REQUIRES") + get_property(private_reqs GLOBAL PROPERTY "${build_component}_PRIV_REQUIRES") + line(" if(\"\$\{component}\" STREQUAL \"${build_component}\")") + line(" set(\${var_requires} \"${reqs}\" PARENT_SCOPE)") + line(" set(\${var_private_requires} \"${private_reqs}\" PARENT_SCOPE)") + line(" return()") + line(" endif()") +endforeach() + +line(" message(FATAL_ERROR \"Component not found: \${component}\")") +line("endfunction()") diff --git a/tools/cmake/third_party/GetGitRevisionDescription.cmake b/tools/cmake/third_party/GetGitRevisionDescription.cmake new file mode 100644 index 00000000..6c711bbd --- /dev/null +++ b/tools/cmake/third_party/GetGitRevisionDescription.cmake @@ -0,0 +1,133 @@ +# - Returns a version string from Git +# +# These functions force a re-configure on each git commit so that you can +# trust the values of the variables in your build system. +# +# get_git_head_revision( [ ...]) +# +# Returns the refspec and sha hash of the current head revision +# +# git_describe( [ ...]) +# +# Returns the results of git describe on the source tree, and adjusting +# the output so that it tests false if an error occurs. +# +# git_get_exact_tag( [ ...]) +# +# Returns the results of git describe --exact-match on the source tree, +# and adjusting the output so that it tests false if there was no exact +# matching tag. +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2010 Ryan Pavlik +# http://academic.cleardefinition.com +# Iowa State University HCI Graduate Program/VRAC +# +# Copyright Iowa State University 2009-2010. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +# Updated 2018 Espressif Systems to add _repo_dir argument +# to get revision of other repositories + +if(__get_git_revision_description) + return() +endif() +set(__get_git_revision_description YES) + +# We must run the following at "include" time, not at function call time, +# to find the path to this module rather than the path to a calling list file +get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) + +function(get_git_head_revision _refspecvar _hashvar _repo_dir) + set(GIT_PARENT_DIR "${_repo_dir}") + set(GIT_DIR "${GIT_PARENT_DIR}/.git") + while(NOT EXISTS "${GIT_DIR}") # .git dir not found, search parent directories + set(GIT_PREVIOUS_PARENT "${GIT_PARENT_DIR}") + get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH) + if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT) + # We have reached the root directory, we are not in git + set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE) + set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE) + return() + endif() + set(GIT_DIR "${GIT_PARENT_DIR}/.git") + endwhile() + # check if this is a submodule + if(NOT IS_DIRECTORY ${GIT_DIR}) + file(READ ${GIT_DIR} submodule) + string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule}) + get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) + get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE) + endif() + set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") + if(NOT EXISTS "${GIT_DATA}") + file(MAKE_DIRECTORY "${GIT_DATA}") + endif() + + if(NOT EXISTS "${GIT_DIR}/HEAD") + return() + endif() + set(HEAD_FILE "${GIT_DATA}/HEAD") + configure_file("${GIT_DIR}/HEAD" "${HEAD_FILE}" COPYONLY) + + configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" + "${GIT_DATA}/grabRef.cmake" + @ONLY) + include("${GIT_DATA}/grabRef.cmake") + + set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE) + set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE) +endfunction() + +function(git_describe _var _repo_dir) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + get_git_head_revision(refspec hash "${_repo_dir}") + if(NOT GIT_FOUND) + set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) + return() + endif() + if(NOT hash) + set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) + return() + endif() + + # TODO sanitize + #if((${ARGN}" MATCHES "&&") OR + # (ARGN MATCHES "||") OR + # (ARGN MATCHES "\\;")) + # message("Please report the following error to the project!") + # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") + #endif() + + #message(STATUS "Arguments to execute_process: ${ARGN}") + + execute_process(COMMAND + "${GIT_EXECUTABLE}" + describe + ${hash} + ${ARGN} + WORKING_DIRECTORY + "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE + res + OUTPUT_VARIABLE + out + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT res EQUAL 0) + set(out "${out}-${res}-NOTFOUND") + endif() + + set(${_var} "${out}" PARENT_SCOPE) +endfunction() + +function(git_get_exact_tag _var _repo_dir) + git_describe(out "${_repo_dir}" --exact-match ${ARGN}) + set(${_var} "${out}" PARENT_SCOPE) +endfunction() diff --git a/tools/cmake/third_party/GetGitRevisionDescription.cmake.in b/tools/cmake/third_party/GetGitRevisionDescription.cmake.in new file mode 100644 index 00000000..6d8b708e --- /dev/null +++ b/tools/cmake/third_party/GetGitRevisionDescription.cmake.in @@ -0,0 +1,41 @@ +# +# Internal file for GetGitRevisionDescription.cmake +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2010 Ryan Pavlik +# http://academic.cleardefinition.com +# Iowa State University HCI Graduate Program/VRAC +# +# Copyright Iowa State University 2009-2010. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +set(HEAD_HASH) + +file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) + +string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) +if(HEAD_CONTENTS MATCHES "ref") + # named branch + string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") + if(EXISTS "@GIT_DIR@/${HEAD_REF}") + configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) + else() + configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY) + file(READ "@GIT_DATA@/packed-refs" PACKED_REFS) + if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") + set(HEAD_HASH "${CMAKE_MATCH_1}") + endif() + endif() +else() + # detached HEAD + configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) +endif() + +if(NOT HEAD_HASH) + file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) + string(STRIP "${HEAD_HASH}" HEAD_HASH) +endif() diff --git a/tools/cmake/toolchain-esp32.cmake b/tools/cmake/toolchain-esp32.cmake new file mode 100644 index 00000000..c23fa4cb --- /dev/null +++ b/tools/cmake/toolchain-esp32.cmake @@ -0,0 +1,7 @@ +set(CMAKE_SYSTEM_NAME Generic) + +set(CMAKE_C_COMPILER xtensa-esp32-elf-gcc) +set(CMAKE_CXX_COMPILER xtensa-esp32-elf-g++) +set(CMAKE_ASM_COMPILER xtensa-esp32-elf-gcc) + +set(CMAKE_EXE_LINKER_FLAGS "-nostdlib" CACHE STRING "Linker Base Flags") diff --git a/tools/cmake/utilities.cmake b/tools/cmake/utilities.cmake new file mode 100644 index 00000000..791e3fbf --- /dev/null +++ b/tools/cmake/utilities.cmake @@ -0,0 +1,181 @@ +# set_default +# +# Define a variable to a default value if otherwise unset. +# +# Priority for new value is: +# - Existing cmake value (ie set with cmake -D, or already set in CMakeLists) +# - Value of any non-empty environment variable of the same name +# - Default value as provided to function +# +function(set_default variable default_value) + if(NOT ${variable}) + if($ENV{${variable}}) + set(${variable} $ENV{${variable}} PARENT_SCOPE) + else() + set(${variable} ${default_value} PARENT_SCOPE) + endif() + endif() +endfunction() + +# spaces2list +# +# Take a variable whose value was space-delimited values, convert to a cmake +# list (semicolon-delimited) +# +# Note: if using this for directories, keeps the issue in place that +# directories can't contain spaces... +# +# TODO: look at cmake separate_arguments, which is quote-aware +function(spaces2list variable_name) + string(REPLACE " " ";" tmp "${${variable_name}}") + set("${variable_name}" "${tmp}" PARENT_SCOPE) +endfunction() + + +# lines2list +# +# Take a variable with multiple lines of output in it, convert it +# to a cmake list (semicolon-delimited), one line per item +# +function(lines2list variable_name) + string(REGEX REPLACE "\r?\n" ";" tmp "${${variable_name}}") + string(REGEX REPLACE ";;" ";" tmp "${tmp}") + set("${variable_name}" "${tmp}" PARENT_SCOPE) +endfunction() + + +# move_if_different +# +# If 'source' has different md5sum to 'destination' (or destination +# does not exist, move it across. +# +# If 'source' has the same md5sum as 'destination', delete 'source'. +# +# Avoids timestamp updates for re-generated files where content hasn't +# changed. +function(move_if_different source destination) + set(do_copy 1) + file(GLOB dest_exists ${destination}) + if(dest_exists) + file(MD5 ${source} source_md5) + file(MD5 ${destination} dest_md5) + if(source_md5 STREQUAL dest_md5) + set(do_copy "") + endif() + endif() + + if(do_copy) + message("Moving ${source} -> ${destination}") + file(RENAME ${source} ${destination}) + else() + message("Not moving ${source} -> ${destination}") + file(REMOVE ${source}) + endif() + +endfunction() + + +# add_compile_options variant for C++ code only +# +# This adds global options, set target properties for +# component-specific flags +function(add_cxx_compile_options) + foreach(option ${ARGV}) + # note: the Visual Studio Generator doesn't support this... + add_compile_options($<$:${option}>) + endforeach() +endfunction() + +# add_compile_options variant for C code only +# +# This adds global options, set target properties for +# component-specific flags +function(add_c_compile_options) + foreach(option ${ARGV}) + # note: the Visual Studio Generator doesn't support this... + add_compile_options($<$:${option}>) + endforeach() +endfunction() + + +# target_add_binary_data adds binary data into the built target, +# by converting it to a generated source file which is then compiled +# to a binary object as part of the build +function(target_add_binary_data target embed_file embed_type) + + get_filename_component(embed_file "${embed_file}" ABSOLUTE) + + get_filename_component(name "${embed_file}" NAME) + set(embed_srcfile "${CMAKE_BINARY_DIR}/${name}.S") + + add_custom_command(OUTPUT "${embed_srcfile}" + COMMAND "${CMAKE_COMMAND}" + -D "DATA_FILE=${embed_file}" + -D "SOURCE_FILE=${embed_srcfile}" + -D "FILE_TYPE=${embed_type}" + -P "${IDF_PATH}/tools/cmake/scripts/data_file_embed_asm.cmake" + MAIN_DEPENDENCY "${embed_file}" + DEPENDENCIES "${IDF_PATH}/tools/cmake/scripts/data_file_embed_asm.cmake" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") + + set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" APPEND PROPERTY ADDITIONAL_MAKE_CLEAN_FILES "${embed_srcfile}") + + target_sources("${target}" PRIVATE "${embed_srcfile}") +endfunction() + +macro(include_if_exists path) + if(EXISTS "${path}") + include("${path}") + endif() +endmacro() + +# Append a single line to the file specified +# The line ending is determined by the host OS +function(file_append_line file line) + if(ENV{MSYSTEM} OR CMAKE_HOST_WIN32) + set(line_ending "\r\n") + else() # unix + set(line_ending "\n") + endif() + file(READ ${file} existing) + string(FIND ${existing} ${line_ending} last_newline REVERSE) + string(LENGTH ${existing} length) + math(EXPR length "${length}-1") + if(NOT length EQUAL last_newline) # file doesn't end with a newline + file(APPEND "${file}" "${line_ending}") + endif() + file(APPEND "${file}" "${line}${line_ending}") +endfunction() + +# Add one or more linker scripts to the target, including a link-time dependency +# +# Automatically adds a -L search path for the containing directory (if found), +# and then adds -T with the filename only. This allows INCLUDE directives to be +# used to include other linker scripts in the same directory. +function(target_linker_script target) + foreach(scriptfile ${ARGN}) + get_filename_component(abs_script "${scriptfile}" ABSOLUTE) + message(STATUS "Adding linker script ${abs_script}") + + get_filename_component(search_dir "${abs_script}" DIRECTORY) + get_filename_component(scriptname "${abs_script}" NAME) + + get_target_property(link_libraries "${target}" LINK_LIBRARIES) + list(FIND "${link_libraries}" "-L ${search_dir}" found_search_dir) + if(found_search_dir EQUAL "-1") # not already added as a search path + target_link_libraries("${target}" "-L ${search_dir}") + endif() + + target_link_libraries("${target}" "-T ${scriptname}") + + # Note: In ESP-IDF, most targets are libraries and libary LINK_DEPENDS don't propagate to + # executable(s) the library is linked to. This is done manually in components.cmake. + set_property(TARGET "${target}" APPEND PROPERTY LINK_DEPENDS "${abs_script}") + endforeach() +endfunction() + +# Convert a CMake list to a JSON list and store it in a variable +function(make_json_list list variable) + string(REPLACE ";" "\", \"" result "[ \"${list}\" ]") + set("${variable}" "${result}" PARENT_SCOPE) +endfunction() diff --git a/tools/idf.py b/tools/idf.py new file mode 100755 index 00000000..247fd3b9 --- /dev/null +++ b/tools/idf.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python +# +# 'idf.py' is a top-level config/build command line tool for ESP-IDF +# +# You don't have to use idf.py, you can use cmake directly +# (or use cmake in an IDE) +# +# +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import sys +import argparse +import os +import os.path +import subprocess +import multiprocessing +import re +import shutil +import json + +class FatalError(RuntimeError): + """ + Wrapper class for runtime errors that aren't caused by bugs in idf.py or the build proces.s + """ + pass + +# Use this Python interpreter for any subprocesses we launch +PYTHON=sys.executable + +# note: os.environ changes don't automatically propagate to child processes, +# you have to pass this in explicitly +os.environ["PYTHON"]=sys.executable + +# Make flavors, across the various kinds of Windows environments & POSIX... +if "MSYSTEM" in os.environ: # MSYS + MAKE_CMD = "make" + MAKE_GENERATOR = "MSYS Makefiles" +elif os.name == 'nt': # other Windows + MAKE_CMD = "mingw32-make" + MAKE_GENERATOR = "MinGW Makefiles" +else: + MAKE_CMD = "make" + MAKE_GENERATOR = "Unix Makefiles" + +GENERATORS = [ + # ('generator name', 'build command line', 'version command line', 'verbose flag') + ("Ninja", [ "ninja" ], [ "ninja", "--version" ], "-v"), + (MAKE_GENERATOR, [ MAKE_CMD, "-j", str(multiprocessing.cpu_count()+2) ], [ "make", "--version" ], "VERBOSE=1"), + ] +GENERATOR_CMDS = dict( (a[0], a[1]) for a in GENERATORS ) +GENERATOR_VERBOSE = dict( (a[0], a[3]) for a in GENERATORS ) + +def _run_tool(tool_name, args, cwd): + def quote_arg(arg): + " Quote 'arg' if necessary " + if " " in arg and not (arg.startswith('"') or arg.startswith("'")): + return "'" + arg + "'" + return arg + display_args = " ".join(quote_arg(arg) for arg in args) + print("Running %s in directory %s" % (tool_name, quote_arg(cwd))) + print('Executing "%s"...' % display_args) + try: + # Note: we explicitly pass in os.environ here, as we may have set IDF_PATH there during startup + subprocess.check_call(args, env=os.environ, cwd=cwd) + except subprocess.CalledProcessError as e: + raise FatalError("%s failed with exit code %d" % (tool_name, e.returncode)) + + +def check_environment(): + """ + Verify the environment contains the top-level tools we need to operate + + (cmake will check a lot of other things) + """ + if not executable_exists(["cmake", "--version"]): + raise FatalError("'cmake' must be available on the PATH to use idf.py") + # find the directory idf.py is in, then the parent directory of this, and assume this is IDF_PATH + detected_idf_path = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) + if "IDF_PATH" in os.environ: + set_idf_path = os.path.realpath(os.environ["IDF_PATH"]) + if set_idf_path != detected_idf_path: + print("WARNING: IDF_PATH environment variable is set to %s but idf.py path indicates IDF directory %s. Using the environment variable directory, but results may be unexpected..." + % (set_idf_path, detected_idf_path)) + else: + os.environ["IDF_PATH"] = detected_idf_path + +def executable_exists(args): + try: + subprocess.check_output(args) + return True + except: + return False + +def detect_cmake_generator(): + """ + Find the default cmake generator, if none was specified. Raises an exception if no valid generator is found. + """ + for (generator, _, version_check, _) in GENERATORS: + if executable_exists(version_check): + return generator + raise FatalError("To use idf.py, either the 'ninja' or 'GNU make' build tool must be available in the PATH") + +def _ensure_build_directory(args, always_run_cmake=False): + """Check the build directory exists and that cmake has been run there. + + If this isn't the case, create the build directory (if necessary) and + do an initial cmake run to configure it. + + This function will also check args.generator parameter. If the parameter is incompatible with + the build directory, an error is raised. If the parameter is None, this function will set it to + an auto-detected default generator or to the value already configured in the build directory. + """ + project_dir = args.project_dir + # Verify the project directory + if not os.path.isdir(project_dir): + if not os.path.exists(project_dir): + raise FatalError("Project directory %s does not exist") + else: + raise FatalError("%s must be a project directory") + if not os.path.exists(os.path.join(project_dir, "CMakeLists.txt")): + raise FatalError("CMakeLists.txt not found in project directory %s" % project_dir) + + # Verify/create the build directory + build_dir = args.build_dir + if not os.path.isdir(build_dir): + os.mkdir(build_dir) + cache_path = os.path.join(build_dir, "CMakeCache.txt") + if not os.path.exists(cache_path) or always_run_cmake: + if args.generator is None: + args.generator = detect_cmake_generator() + try: + cmake_args = ["cmake", "-G", args.generator] + if not args.no_warnings: + cmake_args += [ "--warn-uninitialized" ] + if args.no_ccache: + cmake_args += [ "-DCCACHE_DISABLE=1" ] + cmake_args += [ project_dir] + _run_tool("cmake", cmake_args, cwd=args.build_dir) + except: + # don't allow partially valid CMakeCache.txt files, + # to keep the "should I run cmake?" logic simple + if os.path.exists(cache_path): + os.remove(cache_path) + raise + + # Learn some things from the CMakeCache.txt file in the build directory + cache = parse_cmakecache(cache_path) + try: + generator = cache["CMAKE_GENERATOR"] + except KeyError: + generator = detect_cmake_generator() + if args.generator is None: + args.generator = generator # reuse the previously configured generator, if none was given + if generator != args.generator: + raise FatalError("Build is configured for generator '%s' not '%s'. Run 'idf.py fullclean' to start again." + % (generator, args.generator)) + + try: + home_dir = cache["CMAKE_HOME_DIRECTORY"] + if os.path.normcase(os.path.realpath(home_dir)) != os.path.normcase(os.path.realpath(project_dir)): + raise FatalError("Build directory '%s' configured for project '%s' not '%s'. Run 'idf.py fullclean' to start again." + % (build_dir, os.path.realpath(home_dir), os.path.realpath(project_dir))) + except KeyError: + pass # if cmake failed part way, CMAKE_HOME_DIRECTORY may not be set yet + + +def parse_cmakecache(path): + """ + Parse the CMakeCache file at 'path'. + + Returns a dict of name:value. + + CMakeCache entries also each have a "type", but this is currently ignored. + """ + result = {} + with open(path) as f: + for line in f: + # cmake cache lines look like: CMAKE_CXX_FLAGS_DEBUG:STRING=-g + # groups are name, type, value + m = re.match(r"^([^#/:=]+):([^:=]+)=(.+)\n$", line) + if m: + result[m.group(1)] = m.group(3) + return result + +def build_target(target_name, args): + """ + Execute the target build system to build target 'target_name' + + Calls _ensure_build_directory() which will run cmake to generate a build + directory (with the specified generator) as needed. + """ + _ensure_build_directory(args) + generator_cmd = GENERATOR_CMDS[args.generator] + if not args.no_ccache: + # Setting CCACHE_BASEDIR & CCACHE_NO_HASHDIR ensures that project paths aren't stored in the ccache entries + # (this means ccache hits can be shared between different projects. It may mean that some debug information + # will point to files in another project, if these files are perfect duplicates of each other.) + # + # It would be nicer to set these from cmake, but there's no cross-platform way to set build-time environment + #os.environ["CCACHE_BASEDIR"] = args.build_dir + #os.environ["CCACHE_NO_HASHDIR"] = "1" + pass + if args.verbose: + generator_cmd += [ GENERATOR_VERBOSE[args.generator] ] + + _run_tool(generator_cmd[0], generator_cmd + [target_name], args.build_dir) + + +def _get_esptool_args(args): + esptool_path = os.path.join(os.environ["IDF_PATH"], "components/esptool_py/esptool/esptool.py") + result = [ PYTHON, esptool_path ] + if args.port is not None: + result += [ "-p", args.port ] + result += [ "-b", str(args.baud) ] + return result + +def flash(action, args): + """ + Run esptool to flash the entire project, from an argfile generated by the build system + """ + flasher_args_path = { # action -> name of flasher args file generated by build system + "bootloader-flash": "flash_bootloader_args", + "partition_table-flash": "flash_partition_table_args", + "app-flash": "flash_app_args", + "flash": "flash_project_args", + }[action] + esptool_args = _get_esptool_args(args) + esptool_args += [ "write_flash", "@"+flasher_args_path ] + _run_tool("esptool.py", esptool_args, args.build_dir) + + +def erase_flash(action, args): + esptool_args = _get_esptool_args(args) + esptool_args += [ "erase_flash" ] + _run_tool("esptool.py", esptool_args, args.build_dir) + + +def monitor(action, args): + """ + Run idf_monitor.py to watch build output + """ + desc_path = os.path.join(args.build_dir, "project_description.json") + if not os.path.exists(desc_path): + _ensure_build_directory(args) + with open(desc_path, "r") as f: + project_desc = json.load(f) + + elf_file = os.path.join(args.build_dir, project_desc["app_elf"]) + if not os.path.exists(elf_file): + raise FatalError("ELF file '%s' not found. You need to build & flash the project before running 'monitor', and the binary on the device must match the one in the build directory exactly. Try 'idf.py flash monitor'." % elf_file) + idf_monitor = os.path.join(os.environ["IDF_PATH"], "tools/idf_monitor.py") + monitor_args = [PYTHON, idf_monitor ] + if args.port is not None: + monitor_args += [ "-p", args.port ] + monitor_args += [ "-b", project_desc["monitor_baud"] ] + monitor_args += [ elf_file ] + if "MSYSTEM" is os.environ: + monitor_args = [ "winpty" ] + monitor_args + _run_tool("idf_monitor", monitor_args, args.build_dir) + + +def clean(action, args): + if not os.path.isdir(args.build_dir): + print("Build directory '%s' not found. Nothing to clean." % args.build_dir) + return + build_target("clean", args) + +def reconfigure(action, args): + _ensure_build_directory(args, True) + +def fullclean(action, args): + build_dir = args.build_dir + if not os.path.isdir(build_dir): + print("Build directory '%s' not found. Nothing to clean." % build_dir) + return + if len(os.listdir(build_dir)) == 0: + print("Build directory '%s' is empty. Nothing to clean." % build_dir) + return + + if not os.path.exists(os.path.join(build_dir, "CMakeCache.txt")): + raise FatalError("Directory '%s' doesn't seem to be a CMake build directory. Refusing to automatically delete files in this directory. Delete the directory manually to 'clean' it." % build_dir) + red_flags = [ "CMakeLists.txt", ".git", ".svn" ] + for red in red_flags: + red = os.path.join(build_dir, red) + if os.path.exists(red): + raise FatalError("Refusing to automatically delete files in directory containing '%s'. Delete files manually if you're sure." % red) + # OK, delete everything in the build directory... + for f in os.listdir(build_dir): # TODO: once we are Python 3 only, this can be os.scandir() + f = os.path.join(build_dir, f) + if os.path.isdir(f): + shutil.rmtree(f) + else: + os.remove(f) + +ACTIONS = { + # action name : ( function (or alias), dependencies, order-only dependencies ) + "all" : ( build_target, [], [ "reconfigure", "menuconfig", "clean", "fullclean" ] ), + "build": ( "all", [], [] ), # build is same as 'all' target + "clean": ( clean, [], [ "fullclean" ] ), + "fullclean": ( fullclean, [], [] ), + "reconfigure": ( reconfigure, [], [] ), + "menuconfig": ( build_target, [], [] ), + "size": ( build_target, [], [ "app" ] ), + "size-components": ( build_target, [], [ "app" ] ), + "size-files": ( build_target, [], [ "app" ] ), + "bootloader": ( build_target, [], [] ), + "bootloader-clean": ( build_target, [], [] ), + "bootloader-flash": ( flash, [ "bootloader" ], [] ), + "app": ( build_target, [], [ "clean", "fullclean", "reconfigure" ] ), + "app-flash": ( flash, [], [ "app" ]), + "partition_table": ( build_target, [], [ "reconfigure" ] ), + "partition_table-flash": ( flash, [ "partition_table" ], []), + "flash": ( flash, [ "all" ], [ ] ), + "erase_flash": ( erase_flash, [], []), + "monitor": ( monitor, [], [ "flash", "partition_table-flash", "bootloader-flash", "app-flash" ]), +} + + +def main(): + parser = argparse.ArgumentParser(description='ESP-IDF build management tool') + parser.add_argument('-p', '--port', help="Serial port", default=None) + parser.add_argument('-b', '--baud', help="Baud rate", default=460800) + parser.add_argument('-C', '--project-dir', help="Project directory", default=os.getcwd()) + parser.add_argument('-B', '--build-dir', help="Build directory", default=None) + parser.add_argument('-G', '--generator', help="Cmake generator", choices=GENERATOR_CMDS.keys()) + parser.add_argument('-n', '--no-warnings', help="Disable Cmake warnings", action="store_true") + parser.add_argument('-v', '--verbose', help="Verbose build output", action="store_true") + parser.add_argument('--no-ccache', help="Disable ccache. Otherwise, if ccache is available on the PATH then it will be used for faster builds.", action="store_true") + parser.add_argument('actions', help="Actions (build targets or other operations)", nargs='+', + choices=ACTIONS.keys()) + + args = parser.parse_args() + + check_environment() + + # Advanced parameter checks + if args.build_dir is not None and os.path.realpath(args.project_dir) == os.path.realpath(args.build_dir): + raise FatalError("Setting the build directory to the project directory is not supported. Suggest dropping --build-dir option, the default is a 'build' subdirectory inside the project directory.") + if args.build_dir is None: + args.build_dir = os.path.join(args.project_dir, "build") + args.build_dir = os.path.realpath(args.build_dir) + + completed_actions = set() + def execute_action(action, remaining_actions): + ( function, dependencies, order_dependencies ) = ACTIONS[action] + # very simple dependency management, build a set of completed actions and make sure + # all dependencies are in it + for dep in dependencies: + if not dep in completed_actions: + execute_action(dep, remaining_actions) + for dep in order_dependencies: + if dep in remaining_actions and not dep in completed_actions: + execute_action(dep, remaining_actions) + + if action in completed_actions: + pass # we've already done this, don't do it twice... + elif function in ACTIONS: # alias of another action + execute_action(function, remaining_actions) + else: + function(action, args) + + completed_actions.add(action) + + while len(args.actions) > 0: + execute_action(args.actions[0], args.actions[1:]) + args.actions.pop(0) + + +if __name__ == "__main__": + try: + main() + except FatalError as e: + print(e) + sys.exit(2) + + diff --git a/tools/kconfig/Makefile b/tools/kconfig/Makefile index 6675294b..c8a56abd 100644 --- a/tools/kconfig/Makefile +++ b/tools/kconfig/Makefile @@ -2,6 +2,10 @@ # Kernel configuration targets # These targets are used from top-level makefile +# SRCDIR is kconfig source dir, allows for out-of-tree builds +# if building in tree, SRCDIR==build dir +SRCDIR := $(abspath $(dir $(firstword $(MAKEFILE_LIST)))) + PHONY += xconfig gconfig menuconfig config silentoldconfig \ localmodconfig localyesconfig clean @@ -23,6 +27,22 @@ CFLAGS := CPPFLAGS := LDFLAGS := +# Workaround for a bug on Windows if the mingw32 host compilers +# are installed in addition to the MSYS ones. The kconfig tools +# need to be compiled using the MSYS compiler. +# +# See https://github.com/espressif/esp-idf/issues/1296 +ifdef MSYSTEM +ifeq ("$(MSYSTEM)", "MINGW32") +ifeq ("$(CC)", "cc") +CC := /usr/bin/gcc +endif +ifeq ("$(LD)", "ld") +LD := /usr/bin/ld +endif +endif # MING32 +endif # MSYSTEM + default: mconf conf xconfig: qconf @@ -140,13 +160,22 @@ help: @echo ' tinyconfig - Configure the tiniest possible kernel' # lxdialog stuff -check-lxdialog := lxdialog/check-lxdialog.sh +check-lxdialog := $(SRCDIR)/lxdialog/check-lxdialog.sh # Use recursively expanded variables so we do not call gcc unless # we really need to do so. (Do not call gcc as part of make mrproper) CFLAGS += $(shell $(CONFIG_SHELL) $(check-lxdialog) -ccflags) \ -DLOCALE -MD +%.o: $(SRCDIR)/%.c + $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ + +lxdialog/%.o: $(SRCDIR)/lxdialog/%.c + $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ + +%.o: %.c + $(CC) -I $(SRCDIR) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ + # =========================================================================== # Shared Makefile for the various kconfig executables: # conf: Used for defconfig, oldconfig and related targets @@ -183,9 +212,12 @@ clean-files += $(all-objs) $(all-deps) conf mconf # Check that we have the required ncurses stuff installed for lxdialog (menuconfig) PHONY += dochecklxdialog $(addprefix ,$(lxdialog)): dochecklxdialog -dochecklxdialog: +dochecklxdialog: lxdialog $(CONFIG_SHELL) $(check-lxdialog) -check $(CC) $(CFLAGS) $(LOADLIBES_mconf) +lxdialog: + mkdir -p lxdialog + always := dochecklxdialog # Add environment specific flags @@ -292,7 +324,7 @@ gconf.glade.h: gconf.glade gconf.glade -mconf: $(mconf-objs) +mconf: lxdialog $(mconf-objs) $(CC) -o $@ $(mconf-objs) $(LOADLIBES_mconf) conf: $(conf-objs) @@ -300,15 +332,15 @@ conf: $(conf-objs) zconf.tab.c: zconf.lex.c -zconf.lex.c: zconf.l - flex -L -P zconf -o zconf.lex.c zconf.l +zconf.lex.c: $(SRCDIR)/zconf.l + flex -L -P zconf -o zconf.lex.c $< -zconf.hash.c: zconf.gperf +zconf.hash.c: $(SRCDIR)/zconf.gperf # strip CRs on Windows systems where gperf will otherwise barf on them - sed -E "s/\\x0D$$//" zconf.gperf | gperf -t --output-file zconf.hash.c -a -C -E -g -k '1,3,$$' -p -t + sed -E "s/\\x0D$$//" $< | gperf -t --output-file zconf.hash.c -a -C -E -g -k '1,3,$$' -p -t -zconf.tab.c: zconf.y - bison -t -l -p zconf -o zconf.tab.c zconf.y +zconf.tab.c: $(SRCDIR)/zconf.y + bison -t -l -p zconf -o zconf.tab.c $< clean: rm -f $(clean-files) diff --git a/tools/kconfig/zconf.l b/tools/kconfig/zconf.l index f0b65608..8fd75491 100644 --- a/tools/kconfig/zconf.l +++ b/tools/kconfig/zconf.l @@ -1,5 +1,5 @@ %option nostdinit noyywrap never-interactive full ecs -%option 8bit nodefault perf-report perf-report +%option 8bit perf-report perf-report %option noinput %x COMMAND HELP STRING PARAM %{ diff --git a/tools/kconfig_new/confgen.py b/tools/kconfig_new/confgen.py new file mode 100755 index 00000000..13c74150 --- /dev/null +++ b/tools/kconfig_new/confgen.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python +# +# Command line tool to take in ESP-IDF sdkconfig files with project +# settings and output data in multiple formats (update config, generate +# header file, generate .cmake include file, documentation, etc). +# +# Used internally by the ESP-IDF build system. But designed to be +# non-IDF-specific. +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import sys +import os +import os.path +import tempfile +import json + +import gen_kconfig_doc +import kconfiglib + +__version__ = "0.1" + +def main(): + parser = argparse.ArgumentParser(description='confgen.py v%s - Config Generation Tool' % __version__, prog=os.path.basename(sys.argv[0])) + + parser.add_argument('--config', + help='Project configuration settings', + nargs='?', + default=None) + + parser.add_argument('--defaults', + help='Optional project defaults file, used if --config file doesn\'t exist', + nargs='?', + default=None) + + parser.add_argument('--create-config-if-missing', + help='If set, a new config file will be saved if the old one is not found', + action='store_true') + + parser.add_argument('--kconfig', + help='KConfig file with config item definitions', + required=True) + + parser.add_argument('--output', nargs=2, action='append', + help='Write output file (format and output filename)', + metavar=('FORMAT', 'FILENAME'), + default=[]) + + parser.add_argument('--env', action='append', default=[], + help='Environment to set when evaluating the config file', metavar='NAME=VAL') + + args = parser.parse_args() + + for fmt, filename in args.output: + if not fmt in OUTPUT_FORMATS.keys(): + print("Format '%s' not recognised. Known formats: %s" % (fmt, OUTPUT_FORMATS)) + sys.exit(1) + + try: + args.env = [ (name,value) for (name,value) in ( e.split("=",1) for e in args.env) ] + except ValueError: + print("--env arguments must each contain =. To unset an environment variable, use 'ENV='") + sys.exit(1) + + for name, value in args.env: + os.environ[name] = value + + config = kconfiglib.Kconfig(args.kconfig) + + if args.defaults is not None: + # always load defaults first, so any items which are not defined in that config + # will have the default defined in the defaults file + if not os.path.exists(args.defaults): + raise RuntimeError("Defaults file not found: %s" % args.defaults) + config.load_config(args.defaults) + + if args.config is not None: + if os.path.exists(args.config): + config.load_config(args.config) + elif args.create_config_if_missing: + print("Creating config file %s..." % args.config) + config.write_config(args.config) + elif args.default is None: + raise RuntimeError("Config file not found: %s" % args.config) + + for output_type, filename in args.output: + temp_file = tempfile.mktemp(prefix="confgen_tmp") + try: + output_function = OUTPUT_FORMATS[output_type] + output_function(config, temp_file) + update_if_changed(temp_file, filename) + finally: + try: + os.remove(temp_file) + except OSError: + pass + + +def write_config(config, filename): + CONFIG_HEADING = """# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) Project Configuration +# +""" + config.write_config(filename, header=CONFIG_HEADING) + +def write_header(config, filename): + CONFIG_HEADING = """/* + * Automatically generated file. DO NOT EDIT. + * Espressif IoT Development Framework (ESP-IDF) Configuration Header + */ +#pragma once +""" + config.write_autoconf(filename, header=CONFIG_HEADING) + +def write_cmake(config, filename): + with open(filename, "w") as f: + write = f.write + prefix = config.config_prefix + + write("""# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) Configuration cmake include file +# +""") + def write_node(node): + sym = node.item + if not isinstance(sym, kconfiglib.Symbol): + return + + # Note: str_value calculates _write_to_conf, due to + # internal magic in kconfiglib... + val = sym.str_value + if sym._write_to_conf: + if sym.orig_type in (kconfiglib.BOOL, kconfiglib.TRISTATE) and val == "n": + val = "" # write unset values as empty variables + write("set({}{} \"{}\")\n".format( + prefix, sym.name, val)) + config.walk_menu(write_node) + +def write_json(config, filename): + config_dict = {} + + def write_node(node): + sym = node.item + if not isinstance(sym, kconfiglib.Symbol): + return + + val = sym.str_value # this calculates _write_to_conf, due to kconfiglib magic + if sym._write_to_conf: + if sym.type in [ kconfiglib.BOOL, kconfiglib.TRISTATE ]: + val = (val != "n") + elif sym.type == kconfiglib.HEX: + val = int(val, 16) + elif sym.type == kconfiglib.INT: + val = int(val) + config_dict[sym.name] = val + config.walk_menu(write_node) + with open(filename, "w") as f: + json.dump(config_dict, f, indent=4, sort_keys=True) + +def update_if_changed(source, destination): + with open(source, "r") as f: + source_contents = f.read() + if os.path.exists(destination): + with open(destination, "r") as f: + dest_contents = f.read() + if source_contents == dest_contents: + return # nothing to update + + with open(destination, "w") as f: + f.write(source_contents) + + +OUTPUT_FORMATS = { + "config" : write_config, + "header" : write_header, + "cmake" : write_cmake, + "docs" : gen_kconfig_doc.write_docs, + "json" : write_json, + } + +class FatalError(RuntimeError): + """ + Class for runtime errors (not caused by bugs but by user input). + """ + pass + +if __name__ == '__main__': + try: + main() + except FatalError as e: + print("A fatal error occurred: %s" % e) + sys.exit(2) diff --git a/tools/kconfig_new/gen_kconfig_doc.py b/tools/kconfig_new/gen_kconfig_doc.py new file mode 100644 index 00000000..e5e6968f --- /dev/null +++ b/tools/kconfig_new/gen_kconfig_doc.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# gen_kconfig_doc - confgen.py support for generating ReST markup documentation +# +# For each option in the loaded Kconfig (e.g. 'FOO'), CONFIG_FOO link target is +# generated, allowing options to be referenced in other documents +# (using :ref:`CONFIG_FOO`) +# +# Copyright 2017-2018 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import kconfiglib + +# Indentation to be used in the generated file +INDENT = ' ' + +# Characters used when underlining section heading +HEADING_SYMBOLS = '#*=-^"+' + +# Keep the heading level in sync with api-reference/kconfig.rst +INITIAL_HEADING_LEVEL = 3 +MAX_HEADING_LEVEL = len(HEADING_SYMBOLS)-1 + +def write_docs(config, filename): + """ Note: writing .rst documentation ignores the current value + of any items. ie the --config option can be ignored. + (However at time of writing it still needs to be set to something...) """ + with open(filename, "w") as f: + config.walk_menu(lambda node: write_menu_item(f, node)) + + +def get_breadcrumbs(node): + # this is a bit wasteful as it recalculates each time, but still... + result = [] + node = node.parent + while node.parent: + if node.prompt: + result = [ node.prompt[0] ] + result + node = node.parent + return " > ".join(result) + +def get_heading_level(node): + # bit wasteful also + result = INITIAL_HEADING_LEVEL + node = node.parent + while node.parent: + result += 1 + if result == MAX_HEADING_LEVEL: + return MAX_HEADING_LEVEL + node = node.parent + return result + +def format_rest_text(text, indent): + # Format an indented text block for use with ReST + text = indent + text.replace('\n', '\n' + indent) + # Escape some characters which are inline formatting in ReST + text = text.replace("*", "\\*") + text = text.replace("_", "\\_") + text += '\n' + return text + +def write_menu_item(f, node): + if not node.prompt: + return # Don't do anything for invisible menu items + + if isinstance(node.parent.item, kconfiglib.Choice): + return # Skip choice nodes, they are handled as part of the parent (see below) + + try: + name = node.item.name + except AttributeError: + name = None + + try: + is_menu = node.item == kconfiglib.MENU or node.is_menuconfig + except AttributeError: + is_menu = False # not all MenuNodes have is_menuconfig for some reason + + ## Heading + if name: + title = name + # add link target so we can use :ref:`CONFIG_FOO` + f.write('.. _CONFIG_%s:\n\n' % name) + else: + title = node.prompt[0] + + # if no symbol name, use the prompt as the heading + if True or is_menu: + f.write('%s\n' % title) + f.write(HEADING_SYMBOLS[get_heading_level(node)] * len(title)) + f.write('\n\n') + else: + f.write('**%s**\n\n\n' % title) + + if name: + f.write('%s%s\n\n' % (INDENT, node.prompt[0])) + f.write('%s:emphasis:`Found in: %s`\n\n' % (INDENT, get_breadcrumbs(node))) + + try: + if node.help: + # Help text normally contains newlines, but spaces at the beginning of + # each line are stripped by kconfiglib. We need to re-indent the text + # to produce valid ReST. + f.write(format_rest_text(node.help, INDENT)) + except AttributeError: + pass # No help + + if isinstance(node.item, kconfiglib.Choice): + f.write('%sAvailable options:\n' % INDENT) + choice_node = node.list + while choice_node: + # Format available options as a list + f.write('%s- %-20s (%s)\n' % (INDENT * 2, choice_node.prompt[0], choice_node.item.name)) + if choice_node.help: + HELP_INDENT = INDENT * 2 + fmt_help = format_rest_text(choice_node.help, ' ' + HELP_INDENT) + f.write('%s \n%s\n' % (HELP_INDENT, fmt_help)) + choice_node = choice_node.next + + f.write('\n\n') + + +if __name__ == '__main__': + print("Run this via 'confgen.py --output doc FILENAME'") + diff --git a/tools/kconfig_new/kconfiglib.py b/tools/kconfig_new/kconfiglib.py new file mode 100644 index 00000000..527c5a55 --- /dev/null +++ b/tools/kconfig_new/kconfiglib.py @@ -0,0 +1,4317 @@ +# Copyright (c) 2011-2017, Ulf Magnusson +# Modifications (c) 2018 Espressif Systems +# SPDX-License-Identifier: ISC +# +# ******* IMPORTANT ********** +# +# This is kconfiglib 2.1.0 with some modifications to match the behaviour +# of the ESP-IDF kconfig: +# +# - 'source' nows uses wordexp(3) behaviour to allow source-ing multiple +# files at once, and to expand environment variables directly in the source +# command (without them having to be set as properties in the Kconfig file) +# +# - Added walk_menu() function and refactored to use this internally. +# +# - BOOL & TRISTATE items are allowed to have blank values in .config +# (equivalent to n, this is backwards compatibility with old IDF conf.c) +# +""" +Overview +======== + +Kconfiglib is a Python 2/3 library for scripting and extracting information +from Kconfig configuration systems. It can be used for the following, among +other things: + + - Programmatically get and set symbol values + + allnoconfig.py and allyesconfig.py examples are provided, automatically + verified to produce identical output to the standard 'make allnoconfig' and + 'make allyesconfig'. + + - Read and write .config files + + The generated .config files are character-for-character identical to what + the C implementation would generate (except for the header comment). The + test suite relies on this, as it compares the generated files. + + - Inspect symbols + + Printing a symbol gives output which could be fed back into a Kconfig parser + to redefine it***. The printing function (__str__()) is implemented with + public APIs, meaning you can fetch just whatever information you need as + well. + + A helpful __repr__() is implemented on all objects too, also implemented + with public APIs. + + ***Choice symbols get their parent choice as a dependency, which shows up as + e.g. 'prompt "choice symbol" if ' when printing the symbol. This + could easily be worked around if 100% reparsable output is needed. + + - Inspect expressions + + Expressions use a simple tuple-based format that can be processed manually + if needed. Expression printing and evaluation functions are provided, + implemented with public APIs. + + - Inspect the menu tree + + The underlying menu tree is exposed, including submenus created implicitly + from symbols depending on preceding symbols. This can be used e.g. to + implement menuconfig-like functionality. See the menuconfig.py example. + + +Here are some other features: + + - Single-file implementation + + The entire library is contained in this file. + + - Runs unmodified under both Python 2 and Python 3 + + The code mostly uses basic Python features and has no third-party + dependencies. The most advanced things used are probably @property and + __slots__. + + - Robust and highly compatible with the standard Kconfig C tools + + The test suite automatically compares output from Kconfiglib and the C tools + by diffing the generated .config files for the real kernel Kconfig and + defconfig files, for all ARCHes. + + This currently involves comparing the output for 36 ARCHes and 498 defconfig + files (or over 18000 ARCH/defconfig combinations in "obsessive" test suite + mode). All tests are expected to pass. + + - Not horribly slow despite being a pure Python implementation + + The allyesconfig.py example currently runs in about 1.6 seconds on a Core i7 + 2600K (with a warm file cache), where half a second is overhead from 'make + scriptconfig' (see below). + + For long-running jobs, PyPy gives a big performance boost. CPython is faster + for short-running jobs as PyPy needs some time to warm up. + + - Internals that (mostly) mirror the C implementation + + While being simpler to understand. + + +Using Kconfiglib on the Linux kernel with the Makefile targets +============================================================== + +For the Linux kernel, a handy interface is provided by the +scripts/kconfig/Makefile patch. Apply it with either 'git am' or the 'patch' +utility: + + $ wget -qO- https://raw.githubusercontent.com/ulfalizer/Kconfiglib/master/makefile.patch | git am + $ wget -qO- https://raw.githubusercontent.com/ulfalizer/Kconfiglib/master/makefile.patch | patch -p1 + +Warning: Not passing -p1 to patch will cause the wrong file to be patched. + +Please tell me if the patch does not apply. It should be trivial to apply +manually, as it's just a block of text that needs to be inserted near the other +*conf: targets in scripts/kconfig/Makefile. + +If you do not wish to install Kconfiglib via pip, the Makefile patch is set up +so that you can also just clone Kconfiglib into the kernel root: + + $ git clone git://github.com/ulfalizer/Kconfiglib.git + $ git am Kconfiglib/makefile.patch (or 'patch -p1 < Kconfiglib/makefile.patch') + +Warning: The directory name Kconfiglib/ is significant in this case, because +it's added to PYTHONPATH by the new targets in makefile.patch. + +Look further down for a motivation for the Makefile patch and for instructions +on how you can use Kconfiglib without it. + +The Makefile patch adds the following targets: + + +make [ARCH=] iscriptconfig +-------------------------------- + +This target gives an interactive Python prompt where a Kconfig instance has +been preloaded and is available in 'kconf'. To change the Python interpreter +used, pass PYTHONCMD= to make. The default is "python". + +To get a feel for the API, try evaluating and printing the symbols in +kconf.defined_syms, and explore the MenuNode menu tree starting at +kconf.top_node by following 'next' and 'list' pointers. + +The item contained in a menu node is found in MenuNode.item (note that this can +be one of the constants MENU and COMMENT), and all symbols and choices have a +'nodes' attribute containing their menu nodes (usually only one). Printing a +menu node will print its item, in Kconfig format. + +If you want to look up a symbol by name, use the kconf.syms dictionary. + + +make scriptconfig SCRIPT=