Skip to content

Commit c837925

Browse files
authored
Create a helper to define overridable configs (#10731)
### Summary Context: #10661. As part #10716, this creates a helper to define ET configs. #### Why define_overridable_config? This is a simple wrapper to configure an option via the user or fallback to a default value. Instead of having people do this manually, this is convenient and consistent. More specifically: - If the option is set via the CLI or `set`, then save that value in the cache — with our own description - If the option is unset, then fallback to the default value The user options will be loaded *before* loading the `default.cmake` file. This means, when we load the default file, we can use the user set options or fallback on defaults. Then finally we can do a final sweet of option validations. After this point, all options will be frozen and immutable. Moreover, having a helper function will allow us to enforce standards on options. For example, naming standards done in this PR. #### Migration plan For now, I've only migrated a simple option `EXECUTORCH_ENABLE_LOGGING`. We'll incrementally migrate remaining options in future PRs. ### Test plan For changes to `EXECUTORCH_ENABLE_LOGGING`, I changed the release modes and checked the finaly `CMakeCache.txt` file. Rely on CI for full tests. For python tests: ``` $ python3 -m unittest discover -s tools/cmake -p "*.py" ``` I will include this test in CI in the next diff.
1 parent ee6cf99 commit c837925

File tree

5 files changed

+447
-3
lines changed

5 files changed

+447
-3
lines changed

CMakeLists.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@
4444

4545
cmake_minimum_required(VERSION 3.24)
4646
project(executorch)
47+
48+
# MARK: - Start EXECUTORCH_H12025_BUILD_MIGRATION --------------------------------------------------
49+
50+
include(${PROJECT_SOURCE_DIR}/tools/cmake/common/preset.cmake)
51+
include(${PROJECT_SOURCE_DIR}/tools/cmake/preset/default.cmake)
52+
53+
# MARK: - End EXECUTORCH_H12025_BUILD_MIGRATION ----------------------------------------------------
54+
4755
include(tools/cmake/Utils.cmake)
4856
include(CMakeDependentOption)
4957

@@ -96,9 +104,6 @@ set(EXECUTORCH_PAL_DEFAULT
96104
"Which PAL default implementation to use: one of {posix, minimal}"
97105
)
98106

99-
option(EXECUTORCH_ENABLE_LOGGING "Build with ET_LOG_ENABLED"
100-
${_default_release_disabled_options}
101-
)
102107
if(NOT EXECUTORCH_ENABLE_LOGGING)
103108
# Avoid pulling in the logging strings, which can be large. Note that this
104109
# will set the compiler flag for all targets in this directory, and for all

tools/cmake/common/__init__.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
import shutil
9+
import subprocess
10+
import tempfile
11+
import unittest
12+
from dataclasses import dataclass
13+
from functools import cache
14+
from typing import Any, Dict, List, Optional
15+
16+
# Files to copy from this directory into the temporary workspaces.
17+
TESTABLE_CMAKE_FILES = [
18+
"preset.cmake",
19+
]
20+
21+
22+
# If KEEP_WORKSPACE is set, then keep the workspace instead of deleting it. Useful
23+
# when debugging tests.
24+
@cache
25+
def _keep_workspace() -> bool:
26+
keep_workspace_env = os.environ.get("KEEP_WORKSPACE")
27+
if keep_workspace_env is None:
28+
return False
29+
return keep_workspace_env.lower() not in ("false", "0", "no", "n")
30+
31+
32+
# Create a file tree in the current working directory (cwd). The structure of the
33+
# tree maps to the structure of the file tree. The key of the tree is the name
34+
# of the folder or file. If the value is dict, it creates a folder. If the value
35+
# is a string, it creates a file.
36+
#
37+
# Example:
38+
#
39+
# {
40+
# "README.md": "this is a read me file",
41+
# "build": {
42+
# "cmake": {
43+
# "utils.cmake": "this is a cmake file",
44+
# }
45+
# }
46+
# }
47+
# Results in:
48+
#
49+
# ├── README.md
50+
# └── build
51+
# └── cmake
52+
# └── utils.cmake
53+
#
54+
def _create_file_tree(tree: Dict[Any, Any], cwd: str) -> None:
55+
for name, value in tree.items():
56+
if isinstance(value, str):
57+
file_path = os.path.join(cwd, name)
58+
assert not os.path.exists(file_path), f"file already exists: {file_path}"
59+
os.makedirs(cwd, exist_ok=True)
60+
with open(file_path, "w") as new_file:
61+
new_file.write(value)
62+
elif isinstance(value, dict):
63+
new_cwd = os.path.join(cwd, name)
64+
os.makedirs(new_cwd, exist_ok=True)
65+
_create_file_tree(tree=value, cwd=new_cwd)
66+
else:
67+
raise AssertionError("invalid tree value", value)
68+
69+
70+
@dataclass
71+
class _CacheValue:
72+
value_type: str
73+
value: str
74+
75+
76+
# Get the key/value pair listed in a CMakeCache.txt file.
77+
@cache
78+
def _list_cmake_cache(cache_path: str) -> Dict[str, _CacheValue]:
79+
result = {}
80+
with open(cache_path, "r") as cache_file:
81+
for line in cache_file:
82+
line = line.strip()
83+
if "=" in line:
84+
key, value = line.split("=", 1)
85+
value_type = ""
86+
if ":" in key:
87+
key, value_type = key.split(":")
88+
result[key.strip()] = _CacheValue(
89+
value_type=value_type,
90+
value=value.strip(),
91+
)
92+
return result
93+
94+
95+
class CMakeTestCase(unittest.TestCase):
96+
97+
def tearDown(self) -> None:
98+
super().tearDown()
99+
100+
if self.workspace and not _keep_workspace():
101+
shutil.rmtree(self.workspace)
102+
self.assertFalse(os.path.exists(self.workspace))
103+
104+
def create_workspace(self, tree: Dict[Any, Any]) -> None:
105+
self.workspace = tempfile.mkdtemp()
106+
if _keep_workspace():
107+
print("created workspace", self.workspace)
108+
109+
# Copy testable tree
110+
this_file_dir = os.path.dirname(os.path.abspath(__file__))
111+
for testable_cmake_file in TESTABLE_CMAKE_FILES:
112+
source_path = os.path.join(this_file_dir, testable_cmake_file)
113+
assert os.path.exists(
114+
source_path
115+
), f"{testable_cmake_file} does not exist in {source_path}"
116+
destination_path = os.path.join(self.workspace, testable_cmake_file)
117+
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
118+
shutil.copy(source_path, destination_path)
119+
120+
_create_file_tree(tree=tree, cwd=self.workspace)
121+
122+
def assert_file_content(self, relativePath: str, expectedContent: str) -> None:
123+
path = os.path.join(self.workspace, relativePath)
124+
self.assertTrue(os.path.exists(path), f"expected path does not exist: {path}")
125+
126+
with open(path, "r") as path_file:
127+
self.assertEqual(path_file.read(), expectedContent)
128+
129+
def run_cmake(
130+
self,
131+
cmake_args: Optional[List[str]] = None,
132+
error_contains: Optional[str] = None,
133+
):
134+
cmake_args = (cmake_args or []) + ["--no-warn-unused-cli"]
135+
136+
result = subprocess.run(
137+
["cmake", *cmake_args, "-S", ".", "-B", "cmake-out"],
138+
cwd=self.workspace,
139+
stdout=subprocess.DEVNULL,
140+
stderr=subprocess.PIPE if error_contains else None,
141+
check=False,
142+
)
143+
144+
if error_contains is not None:
145+
self.assertNotEqual(result.returncode, 0)
146+
actual_error = result.stderr.decode("utf-8")
147+
self.assertTrue(
148+
error_contains in actual_error, f"Actual error: {actual_error}"
149+
)
150+
else:
151+
self.assertEqual(result.returncode, 0)
152+
self.assertTrue(os.path.exists(os.path.join(self.workspace, "cmake-out")))
153+
154+
def assert_cmake_cache(
155+
self,
156+
key: str,
157+
expected_value: str,
158+
expected_type: str,
159+
):
160+
cache = _list_cmake_cache(
161+
os.path.join(self.workspace, "cmake-out", "CMakeCache.txt")
162+
)
163+
self.assertEqual(
164+
cache[key].value, expected_value, f"unexpected value for {key}"
165+
)
166+
self.assertEqual(
167+
cache[key].value_type, expected_type, f"unexpected value type for {key}"
168+
)

tools/cmake/common/preset.cmake

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# Enforce option names to always start with EXECUTORCH.
8+
function(enforce_executorch_option_name NAME)
9+
if(NOT "${NAME}" MATCHES "^EXECUTORCH_")
10+
message(FATAL_ERROR "Option name '${NAME}' must start with EXECUTORCH_")
11+
endif()
12+
endfunction()
13+
14+
# Define an overridable option.
15+
# 1) If the option is already defined in the process, then store that in cache
16+
# 2) If the option is NOT set, then store the default value in cache
17+
macro(define_overridable_option NAME DESCRIPTION VALUE_TYPE DEFAULT_VALUE)
18+
enforce_executorch_option_name(${NAME})
19+
20+
if(NOT "${VALUE_TYPE}" STREQUAL "STRING" AND NOT "${VALUE_TYPE}" STREQUAL "BOOL")
21+
message(FATAL_ERROR "Invalid option (${NAME}) value type '${VALUE_TYPE}', must be either STRING or BOOL")
22+
endif()
23+
24+
if(DEFINED ${NAME})
25+
set(${NAME} ${${NAME}} CACHE ${VALUE_TYPE} ${DESCRIPTION} FORCE)
26+
else()
27+
set(${NAME} ${DEFAULT_VALUE} CACHE ${VALUE_TYPE} ${DESCRIPTION})
28+
endif()
29+
endmacro()

0 commit comments

Comments
 (0)