diff --git a/CMakeLists.txt b/CMakeLists.txt index 251a20ea629..45993721a66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,14 @@ cmake_minimum_required(VERSION 3.24) project(executorch) + +# MARK: - Start EXECUTORCH_H12025_BUILD_MIGRATION -------------------------------------------------- + +include(${PROJECT_SOURCE_DIR}/tools/cmake/common/preset.cmake) +include(${PROJECT_SOURCE_DIR}/tools/cmake/preset/default.cmake) + +# MARK: - End EXECUTORCH_H12025_BUILD_MIGRATION ---------------------------------------------------- + include(tools/cmake/Utils.cmake) include(CMakeDependentOption) @@ -96,9 +104,6 @@ set(EXECUTORCH_PAL_DEFAULT "Which PAL default implementation to use: one of {posix, minimal}" ) -option(EXECUTORCH_ENABLE_LOGGING "Build with ET_LOG_ENABLED" - ${_default_release_disabled_options} -) if(NOT EXECUTORCH_ENABLE_LOGGING) # Avoid pulling in the logging strings, which can be large. Note that this # will set the compiler flag for all targets in this directory, and for all diff --git a/tools/cmake/common/__init__.py b/tools/cmake/common/__init__.py new file mode 100644 index 00000000000..5edaa5cfeaa --- /dev/null +++ b/tools/cmake/common/__init__.py @@ -0,0 +1,168 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import os +import shutil +import subprocess +import tempfile +import unittest +from dataclasses import dataclass +from functools import cache +from typing import Any, Dict, List, Optional + +# Files to copy from this directory into the temporary workspaces. +TESTABLE_CMAKE_FILES = [ + "preset.cmake", +] + + +# If KEEP_WORKSPACE is set, then keep the workspace instead of deleting it. Useful +# when debugging tests. +@cache +def _keep_workspace() -> bool: + keep_workspace_env = os.environ.get("KEEP_WORKSPACE") + if keep_workspace_env is None: + return False + return keep_workspace_env.lower() not in ("false", "0", "no", "n") + + +# Create a file tree in the current working directory (cwd). The structure of the +# tree maps to the structure of the file tree. The key of the tree is the name +# of the folder or file. If the value is dict, it creates a folder. If the value +# is a string, it creates a file. +# +# Example: +# +# { +# "README.md": "this is a read me file", +# "build": { +# "cmake": { +# "utils.cmake": "this is a cmake file", +# } +# } +# } +# Results in: +# +# ├── README.md +# └── build +# └── cmake +# └── utils.cmake +# +def _create_file_tree(tree: Dict[Any, Any], cwd: str) -> None: + for name, value in tree.items(): + if isinstance(value, str): + file_path = os.path.join(cwd, name) + assert not os.path.exists(file_path), f"file already exists: {file_path}" + os.makedirs(cwd, exist_ok=True) + with open(file_path, "w") as new_file: + new_file.write(value) + elif isinstance(value, dict): + new_cwd = os.path.join(cwd, name) + os.makedirs(new_cwd, exist_ok=True) + _create_file_tree(tree=value, cwd=new_cwd) + else: + raise AssertionError("invalid tree value", value) + + +@dataclass +class _CacheValue: + value_type: str + value: str + + +# Get the key/value pair listed in a CMakeCache.txt file. +@cache +def _list_cmake_cache(cache_path: str) -> Dict[str, _CacheValue]: + result = {} + with open(cache_path, "r") as cache_file: + for line in cache_file: + line = line.strip() + if "=" in line: + key, value = line.split("=", 1) + value_type = "" + if ":" in key: + key, value_type = key.split(":") + result[key.strip()] = _CacheValue( + value_type=value_type, + value=value.strip(), + ) + return result + + +class CMakeTestCase(unittest.TestCase): + + def tearDown(self) -> None: + super().tearDown() + + if self.workspace and not _keep_workspace(): + shutil.rmtree(self.workspace) + self.assertFalse(os.path.exists(self.workspace)) + + def create_workspace(self, tree: Dict[Any, Any]) -> None: + self.workspace = tempfile.mkdtemp() + if _keep_workspace(): + print("created workspace", self.workspace) + + # Copy testable tree + this_file_dir = os.path.dirname(os.path.abspath(__file__)) + for testable_cmake_file in TESTABLE_CMAKE_FILES: + source_path = os.path.join(this_file_dir, testable_cmake_file) + assert os.path.exists( + source_path + ), f"{testable_cmake_file} does not exist in {source_path}" + destination_path = os.path.join(self.workspace, testable_cmake_file) + os.makedirs(os.path.dirname(destination_path), exist_ok=True) + shutil.copy(source_path, destination_path) + + _create_file_tree(tree=tree, cwd=self.workspace) + + def assert_file_content(self, relativePath: str, expectedContent: str) -> None: + path = os.path.join(self.workspace, relativePath) + self.assertTrue(os.path.exists(path), f"expected path does not exist: {path}") + + with open(path, "r") as path_file: + self.assertEqual(path_file.read(), expectedContent) + + def run_cmake( + self, + cmake_args: Optional[List[str]] = None, + error_contains: Optional[str] = None, + ): + cmake_args = (cmake_args or []) + ["--no-warn-unused-cli"] + + result = subprocess.run( + ["cmake", *cmake_args, "-S", ".", "-B", "cmake-out"], + cwd=self.workspace, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE if error_contains else None, + check=False, + ) + + if error_contains is not None: + self.assertNotEqual(result.returncode, 0) + actual_error = result.stderr.decode("utf-8") + self.assertTrue( + error_contains in actual_error, f"Actual error: {actual_error}" + ) + else: + self.assertEqual(result.returncode, 0) + self.assertTrue(os.path.exists(os.path.join(self.workspace, "cmake-out"))) + + def assert_cmake_cache( + self, + key: str, + expected_value: str, + expected_type: str, + ): + cache = _list_cmake_cache( + os.path.join(self.workspace, "cmake-out", "CMakeCache.txt") + ) + self.assertEqual( + cache[key].value, expected_value, f"unexpected value for {key}" + ) + self.assertEqual( + cache[key].value_type, expected_type, f"unexpected value type for {key}" + ) diff --git a/tools/cmake/common/preset.cmake b/tools/cmake/common/preset.cmake new file mode 100644 index 00000000000..0fde24bae6a --- /dev/null +++ b/tools/cmake/common/preset.cmake @@ -0,0 +1,29 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# Enforce option names to always start with EXECUTORCH. +function(enforce_executorch_option_name NAME) + if(NOT "${NAME}" MATCHES "^EXECUTORCH_") + message(FATAL_ERROR "Option name '${NAME}' must start with EXECUTORCH_") + endif() +endfunction() + +# Define an overridable option. +# 1) If the option is already defined in the process, then store that in cache +# 2) If the option is NOT set, then store the default value in cache +macro(define_overridable_option NAME DESCRIPTION VALUE_TYPE DEFAULT_VALUE) + enforce_executorch_option_name(${NAME}) + + if(NOT "${VALUE_TYPE}" STREQUAL "STRING" AND NOT "${VALUE_TYPE}" STREQUAL "BOOL") + message(FATAL_ERROR "Invalid option (${NAME}) value type '${VALUE_TYPE}', must be either STRING or BOOL") + endif() + + if(DEFINED ${NAME}) + set(${NAME} ${${NAME}} CACHE ${VALUE_TYPE} ${DESCRIPTION} FORCE) + else() + set(${NAME} ${DEFAULT_VALUE} CACHE ${VALUE_TYPE} ${DESCRIPTION}) + endif() +endmacro() diff --git a/tools/cmake/common/preset_test.py b/tools/cmake/common/preset_test.py new file mode 100644 index 00000000000..eb564eadace --- /dev/null +++ b/tools/cmake/common/preset_test.py @@ -0,0 +1,225 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import os + +from tools.cmake.common import CMakeTestCase, TESTABLE_CMAKE_FILES + + +class TestPreset(CMakeTestCase): + + def test_create_workspace(self): + self.create_workspace( + { + ".gitignore": ".DS_Store", + "CMakeLists.txt": "move fast", + "README.md": "Meta Platforms", + "example": { + "CMakeLists.txt": "move faster", + "cmake": { + "README.md": "godspeed you!", + }, + }, + } + ) + + self.assertIsNotNone(self.workspace) + self.assert_file_content("CMakeLists.txt", "move fast") + self.assert_file_content("README.md", "Meta Platforms") + self.assert_file_content(".gitignore", ".DS_Store") + self.assert_file_content("example/CMakeLists.txt", "move faster") + self.assert_file_content("example/cmake/README.md", "godspeed you!") + + # Test implicitly copied cmake files + this_file_dir = os.path.dirname(os.path.abspath(__file__)) + self.assertTrue(len(TESTABLE_CMAKE_FILES) > 0) + for testable_cmake_file in TESTABLE_CMAKE_FILES: + with open( + os.path.join(this_file_dir, testable_cmake_file), "r" + ) as source_file: + self.assert_file_content(testable_cmake_file, source_file.read()) + + def test_set_option(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + add_subdirectory(numbers) + set(SECRET_MESSAGE "move fast" CACHE STRING "") + """ + _numbers_cmake_lists_txt = """ + set(PI 3.14 CACHE STRING "") + """ + + self.create_workspace( + { + "CMakeLists.txt": _cmake_lists_txt, + "numbers": { + "CMakeLists.txt": _numbers_cmake_lists_txt, + }, + } + ) + self.run_cmake() + self.assert_cmake_cache("SECRET_MESSAGE", "move fast", "STRING") + self.assert_cmake_cache("PI", "3.14", "STRING") + + def test_define_overridable_option_invalid_name(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + define_overridable_option(IAM_AN_INVALID_NAME "test example" STRING "default value") + """ + self.create_workspace({"CMakeLists.txt": _cmake_lists_txt}) + self.run_cmake( + error_contains="Option name 'IAM_AN_INVALID_NAME' must start with EXECUTORCH_" + ) + + def test_define_overridable_option_default(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + add_subdirectory(example) + """ + _example_cmake_lists_txt = """ + define_overridable_option(EXECUTORCH_TEST_MESSAGE "test message" STRING "default value") + define_overridable_option(EXECUTORCH_TEST_OPTION "test option" BOOL ON) + """ + self.create_workspace( + { + "CMakeLists.txt": _cmake_lists_txt, + "example": { + "CMakeLists.txt": _example_cmake_lists_txt, + }, + } + ) + self.run_cmake() + self.assert_cmake_cache("EXECUTORCH_TEST_MESSAGE", "default value", "STRING") + self.assert_cmake_cache("EXECUTORCH_TEST_OPTION", "ON", "BOOL") + + def test_define_overridable_option_invalid_type(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + define_overridable_option(EXECUTORCH_TEST_MESSAGE "test example" NUMBER "default value") + """ + self.create_workspace({"CMakeLists.txt": _cmake_lists_txt}) + self.run_cmake( + error_contains="Invalid option (EXECUTORCH_TEST_MESSAGE) value type 'NUMBER'" + ) + + def test_define_overridable_option_cli_override(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + add_subdirectory(example) + """ + _example_cmake_lists_txt = """ + define_overridable_option(EXECUTORCH_TEST_MESSAGE "test message" STRING "default value") + """ + self.create_workspace( + { + "CMakeLists.txt": _cmake_lists_txt, + "example": { + "CMakeLists.txt": _example_cmake_lists_txt, + }, + } + ) + self.run_cmake(cmake_args=["-DEXECUTORCH_TEST_MESSAGE='cli value'"]) + self.assert_cmake_cache("EXECUTORCH_TEST_MESSAGE", "cli value", "STRING") + + def test_define_overridable_option_set_override_before(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + set(EXECUTORCH_TEST_MESSAGE "set value") + add_subdirectory(example) + """ + _example_cmake_lists_txt = """ + define_overridable_option(EXECUTORCH_TEST_MESSAGE "test message" STRING "default value") + """ + self.create_workspace( + { + "CMakeLists.txt": _cmake_lists_txt, + "example": { + "CMakeLists.txt": _example_cmake_lists_txt, + }, + } + ) + self.run_cmake() + self.assert_cmake_cache("EXECUTORCH_TEST_MESSAGE", "set value", "STRING") + + def test_define_overridable_option_set_override_after(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + add_subdirectory(example) + set(EXECUTORCH_TEST_MESSAGE "set value") + """ + _example_cmake_lists_txt = """ + define_overridable_option(EXECUTORCH_TEST_MESSAGE "test message" STRING "default value") + """ + self.create_workspace( + { + "CMakeLists.txt": _cmake_lists_txt, + "example": { + "CMakeLists.txt": _example_cmake_lists_txt, + }, + } + ) + self.run_cmake() + # Setting the value after should not affect the cache. + self.assert_cmake_cache("EXECUTORCH_TEST_MESSAGE", "default value", "STRING") + + def test_define_overridable_option_set_override_after_with_cache(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + add_subdirectory(example) + set(EXECUTORCH_TEST_MESSAGE "set value" CACHE STRING "") + """ + _example_cmake_lists_txt = """ + define_overridable_option(EXECUTORCH_TEST_MESSAGE "test message" STRING "default value") + """ + self.create_workspace( + { + "CMakeLists.txt": _cmake_lists_txt, + "example": { + "CMakeLists.txt": _example_cmake_lists_txt, + }, + } + ) + self.run_cmake() + # Setting the value after should not affect the cache. + self.assert_cmake_cache("EXECUTORCH_TEST_MESSAGE", "default value", "STRING") + + def test_define_overridable_option_cli_override_with_set_override(self): + _cmake_lists_txt = """ + cmake_minimum_required(VERSION 3.24) + project(test_preset) + include(${PROJECT_SOURCE_DIR}/preset.cmake) + set(EXECUTORCH_TEST_MESSAGE "set value") + add_subdirectory(example) + """ + _example_cmake_lists_txt = """ + define_overridable_option(EXECUTORCH_TEST_MESSAGE "test message" STRING "default value") + """ + self.create_workspace( + { + "CMakeLists.txt": _cmake_lists_txt, + "example": { + "CMakeLists.txt": _example_cmake_lists_txt, + }, + } + ) + self.run_cmake(cmake_args=["-DEXECUTORCH_TEST_MESSAGE='cli value'"]) + # If an option is set through cmake, it should NOT be overridable from the CLI. + self.assert_cmake_cache("EXECUTORCH_TEST_MESSAGE", "set value", "STRING") diff --git a/tools/cmake/preset/default.cmake b/tools/cmake/preset/default.cmake new file mode 100644 index 00000000000..eafa8a7a937 --- /dev/null +++ b/tools/cmake/preset/default.cmake @@ -0,0 +1,17 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +if(CMAKE_BUILD_TYPE STREQUAL "Release") + set(_is_build_type_release ON) + set(_is_build_type_debug OFF) +else() + set(_is_build_type_release OFF) + set(_is_build_type_debug ON) +endif() + +# MARK: - Definitions + +define_overridable_option(EXECUTORCH_ENABLE_LOGGING "Build with ET_LOG_ENABLED" BOOL ${_is_build_type_debug})