Skip to content

Create a helper to define overridable configs #10731

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
168 changes: 168 additions & 0 deletions tools/cmake/common/__init__.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow thanks for writing the tests! I didn't know CMake is testable


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}"
)
29 changes: 29 additions & 0 deletions tools/cmake/common/preset.cmake
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading