Skip to content

Commit 19a3002

Browse files
[build] Fix flatc (#8858)
[build] Fix flatc (#8816) * [build] Fix flatc We need to install `build/pip_data_bin_init.py.in` into `<executorch root>/data/bin/__init__.py`. This PR rewrite the logic into a `BuiltFile` so that it works well in editable mode. Test: ``` python -c "from executorch.data.bin import flatc" ``` Will add unit test in next PR. [ghstack-poisoned] * Update on "[build] Fix flatc" Fixes #8784 We need to install `build/pip_data_bin_init.py.in` into `<executorch root>/data/bin/__init__.py`. This PR rewrite the logic into a `BuiltFile` so that it works well in editable mode. Since `BuiltFile` by default looks into cmake cache directory, this PR adds a placeholder `%CMAKE_CACHE_DIR%` for those are actually built by CMake and for `build/pip_data_bin_init.py.in` we don't add this placeholder. Test: ``` python -c "from executorch.data.bin import flatc" ``` Will add unit test in next PR. [ghstack-poisoned] * Update on "[build] Fix flatc" Fixes #8784 We need to install `build/pip_data_bin_init.py.in` into `<executorch root>/data/bin/__init__.py`. This PR rewrite the logic into a `BuiltFile` so that it works well in editable mode. Since `BuiltFile` by default looks into cmake cache directory, this PR adds a placeholder `%CMAKE_CACHE_DIR%` for those are actually built by CMake and for `build/pip_data_bin_init.py.in` we don't add this placeholder. Test: ``` python -c "from executorch.data.bin import flatc" ``` Will add unit test in next PR. [ghstack-poisoned] * Update on "[build] Fix flatc" Fixes #8784 We need to install `build/pip_data_bin_init.py.in` into `<executorch root>/data/bin/__init__.py`. This PR rewrite the logic into a `BuiltFile` so that it works well in editable mode. Since `BuiltFile` by default looks into cmake cache directory, this PR adds a placeholder `%CMAKE_CACHE_DIR%` for those are actually built by CMake and for `build/pip_data_bin_init.py.in` we don't add this placeholder. Test: ``` python -c "from executorch.data.bin import flatc" ``` Will add unit test in next PR. [ghstack-poisoned] (cherry picked from commit d0b27b5) Co-authored-by: Mengwei Liu <[email protected]>
1 parent f357169 commit 19a3002

File tree

4 files changed

+113
-51
lines changed

4 files changed

+113
-51
lines changed

.github/workflows/pull.yml

+11
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,17 @@ jobs:
371371
build-tool: cmake
372372
docker-image: executorch-ubuntu-22.04-clang12
373373

374+
unittest-editable:
375+
uses: ./.github/workflows/_unittest.yml
376+
permissions:
377+
id-token: write
378+
contents: read
379+
with:
380+
build-mode: Debug
381+
build-tool: cmake
382+
editable: true
383+
docker-image: executorch-ubuntu-22.04-clang12
384+
374385
unittest-buck:
375386
uses: ./.github/workflows/_unittest.yml
376387
permissions:

data/bin/README.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## PLEASE DO NOT REMOVE THIS DIRECTORY!
2+
3+
This directory is used to host binaries installed during pip wheel build time.
4+
5+
## How to add a binary into pip wheel
6+
7+
1. Update `[project.scripts]` section of `pyproject.toml` file. Add the new binary name and it's corresponding module name similar to:
8+
9+
```
10+
flatc = "executorch.data.bin:flatc"
11+
```
12+
13+
For example, `flatc` is built during wheel packaging, we first build `flatc` through CMake and copy the file to `<executorch root>/data/bin/flatc` and ask `setuptools` to generate a commandline wrapper for `flatc`, then route it to `<executorch root>/data/bin/flatc`.
14+
15+
This way after installing `executorch`, a user will be able to call `flatc` directly in commandline and it points to `<executorch root>/data/bin/flatc`
16+
17+
2. Update `setup.py` to include the logic of building the new binary and copying the binary to this directory.
18+
19+
```python
20+
BuiltFile(
21+
src_dir="%CMAKE_CACHE_DIR%/third-party/flatbuffers/%BUILD_TYPE%/",
22+
src_name="flatc",
23+
dst="executorch/data/bin/",
24+
is_executable=True,
25+
),
26+
```
27+
This means find `flatc` in `CMAKE_CACHE_DIR` and copy it to `<executorch root>/data/bin`. Notice that this works for both pip wheel packaging as well as editable mode install.
28+
29+
## Why we can't create this directory at wheel build time?
30+
31+
The reason is without `data/bin/` present in source file, we can't tell `setuptools` to generate a module `executorch.data.bin` in editable mode, partially because we don't have a good top level module `executorch` and have to enumerate all the second level modules, including `executorch.data.bin`.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ flatc = "executorch.data.bin:flatc"
8888
[tool.setuptools.package-dir]
8989
"executorch.backends" = "backends"
9090
"executorch.codegen" = "codegen"
91+
"executorch.data.bin" = "data/bin"
9192
# TODO(mnachin T180504136): Do not put examples/models
9293
# into core pip packages. Refactor out the necessary utils
9394
# or core models files into a separate package.

setup.py

+70-51
Original file line numberDiff line numberDiff line change
@@ -220,37 +220,50 @@ def src_path(self, installer: "InstallerBuildExt") -> Path:
220220
"""
221221
# Share the cmake-out location with CustomBuild.
222222
build_cmd = installer.get_finalized_command("build")
223-
if hasattr(build_cmd, "cmake_cache_dir"):
224-
cmake_cache_dir = Path(build_cmd.cmake_cache_dir)
223+
if "%CMAKE_CACHE_DIR%" in self.src:
224+
if not hasattr(build_cmd, "cmake_cache_dir"):
225+
raise RuntimeError(
226+
f"Extension {self.name} has a src {self.src} that contains"
227+
" %CMAKE_CACHE_DIR% but CMake does not run in the `build` "
228+
"command. Please double check if the command is correct."
229+
)
230+
else:
231+
build_dir = Path(build_cmd.cmake_cache_dir)
225232
else:
226-
# If we're in editable mode, use a default or fallback value for cmake_cache_dir
227-
# This could be a hardcoded path, or a path derived from the current working directory
228-
cmake_cache_dir = Path(".")
233+
# If the src path doesn't contain %CMAKE_CACHE_DIR% placeholder,
234+
# try to find it under the current directory.
235+
build_dir = Path(".")
236+
237+
src_path = self.src.replace("%CMAKE_CACHE_DIR%/", "")
238+
229239
cfg = get_build_type(installer.debug)
230240

231241
if os.name == "nt":
232242
# Replace %BUILD_TYPE% with the current build type.
233-
self.src = self.src.replace("%BUILD_TYPE%", cfg)
243+
src_path = src_path.replace("%BUILD_TYPE%", cfg)
234244
else:
235245
# Remove %BUILD_TYPE% from the path.
236-
self.src = self.src.replace("/%BUILD_TYPE%", "")
246+
src_path = src_path.replace("/%BUILD_TYPE%", "")
237247

238248
# Construct the full source path, resolving globs. If there are no glob
239249
# pattern characters, this will just ensure that the source file exists.
240-
srcs = tuple(cmake_cache_dir.glob(self.src))
250+
srcs = tuple(build_dir.glob(src_path))
241251
if len(srcs) != 1:
242252
raise ValueError(
243-
f"""Expected exactly one file matching '{self.src}' in {cmake_cache_dir}; found {repr(srcs)}.
244-
245-
If that file is a CMake-built extension module file, and we are installing in editable mode, please disable the corresponding build option since it's not supported yet.
246-
247-
Try:
248-
249-
EXECUTORCH_BUILD_FLATC=OFF EXECUTORCH_BUILD_KERNELS_CUSTOM_AOT=OFF pip install -e .
250-
"""
253+
f"Expecting exactly 1 file matching {self.src} in {build_dir}, "
254+
f"found {repr(srcs)}. Resolved src pattern: {src_path}."
251255
)
252256
return srcs[0]
253257

258+
def inplace_dir(self, installer: "InstallerBuildExt") -> Path:
259+
"""Returns the path of this file to be installed to, under inplace mode.
260+
261+
It will be a relative path to the project root directory. For more info
262+
related to inplace/editable mode, please checkout this doc:
263+
https://setuptools.pypa.io/en/latest/userguide/development_mode.html
264+
"""
265+
raise NotImplementedError()
266+
254267

255268
class BuiltFile(_BaseExtension):
256269
"""An extension that installs a single file that was built by cmake.
@@ -316,6 +329,18 @@ def dst_path(self, installer: "InstallerBuildExt") -> Path:
316329
# Destination looks like a file.
317330
return dst_root / Path(self.dst)
318331

332+
def inplace_dir(self, installer: "InstallerBuildExt") -> Path:
333+
"""For a `BuiltFile`, we use self.dst as its inplace directory path.
334+
Need to handle directory vs file.
335+
"""
336+
# HACK: get rid of the leading "executorch" in ext.dst.
337+
# This is because we don't have a root level "executorch" module.
338+
package_dir = self.dst.removeprefix("executorch/")
339+
# If dst is a file, use it's directory
340+
if not package_dir.endswith("/"):
341+
package_dir = os.path.dirname(package_dir)
342+
return Path(package_dir)
343+
319344

320345
class BuiltExtension(_BaseExtension):
321346
"""An extension that installs a python extension that was built by cmake."""
@@ -335,7 +360,7 @@ def __init__(self, src: str, modpath: str):
335360
"/" not in modpath
336361
), f"modpath must be a dotted python module path: saw '{modpath}'"
337362
# This is a real extension, so use the modpath as the name.
338-
super().__init__(src=src, dst=modpath, name=modpath)
363+
super().__init__(src=f"%CMAKE_CACHE_DIR%/{src}", dst=modpath, name=modpath)
339364

340365
def src_path(self, installer: "InstallerBuildExt") -> Path:
341366
"""Returns the path to the source file, resolving globs.
@@ -369,6 +394,15 @@ def dst_path(self, installer: "InstallerBuildExt") -> Path:
369394
# path: that's the file we're creating.
370395
return Path(installer.get_ext_fullpath(self.dst))
371396

397+
def inplace_dir(self, installer: "InstallerBuildExt") -> Path:
398+
"""For BuiltExtension, deduce inplace dir path from extension name."""
399+
build_py = installer.get_finalized_command("build_py")
400+
modpath = self.name.split(".")
401+
package = ".".join(modpath[:-1])
402+
package_dir = os.path.abspath(build_py.get_package_dir(package))
403+
404+
return Path(package_dir)
405+
372406

373407
class InstallerBuildExt(build_ext):
374408
"""Installs files that were built by cmake."""
@@ -399,23 +433,15 @@ def copy_extensions_to_source(self) -> None:
399433
400434
Returns:
401435
"""
402-
build_py = self.get_finalized_command("build_py")
403436
for ext in self.extensions:
404-
if isinstance(ext, BuiltExtension):
405-
modpath = ext.name.split(".")
406-
package = ".".join(modpath[:-1])
407-
package_dir = os.path.abspath(build_py.get_package_dir(package))
408-
else:
409-
# HACK: get rid of the leading "executorch" in ext.dst.
410-
# This is because we don't have a root level "executorch" module.
411-
package_dir = ext.dst.removeprefix("executorch/")
437+
package_dir = ext.inplace_dir(self)
412438

413439
# Ensure that the destination directory exists.
414440
self.mkpath(os.fspath(package_dir))
415441

416442
regular_file = ext.src_path(self)
417443
inplace_file = os.path.join(
418-
package_dir, os.path.basename(ext.src_path(self))
444+
package_dir, os.path.basename(ext.dst_path(self))
419445
)
420446

421447
# Always copy, even if source is older than destination, to ensure
@@ -724,20 +750,6 @@ def run(self):
724750
# Build the system.
725751
self.spawn(["cmake", "--build", cmake_cache_dir, *build_args])
726752

727-
# Non-python files should live under this data directory.
728-
data_root = os.path.join(self.build_lib, "executorch", "data")
729-
730-
# Directories like bin/ and lib/ live under data/.
731-
bin_dir = os.path.join(data_root, "bin")
732-
733-
# Copy the bin wrapper so that users can run any executables under
734-
# data/bin, as long as they are listed in the [project.scripts] section
735-
# of pyproject.toml.
736-
self.mkpath(bin_dir)
737-
self.copy_file(
738-
"build/pip_data_bin_init.py.in",
739-
os.path.join(bin_dir, "__init__.py"),
740-
)
741753
# Share the cmake-out location with _BaseExtension.
742754
self.cmake_cache_dir = cmake_cache_dir
743755

@@ -749,13 +761,20 @@ def get_ext_modules() -> List[Extension]:
749761
"""Returns the set of extension modules to build."""
750762
ext_modules = []
751763
if ShouldBuild.flatc():
752-
ext_modules.append(
753-
BuiltFile(
754-
src_dir="third-party/flatbuffers/%BUILD_TYPE%/",
755-
src_name="flatc",
756-
dst="executorch/data/bin/",
757-
is_executable=True,
758-
)
764+
ext_modules.extend(
765+
[
766+
BuiltFile(
767+
src_dir="%CMAKE_CACHE_DIR%/third-party/flatbuffers/%BUILD_TYPE%/",
768+
src_name="flatc",
769+
dst="executorch/data/bin/",
770+
is_executable=True,
771+
),
772+
BuiltFile(
773+
src_dir="build/",
774+
src_name="pip_data_bin_init.py.in",
775+
dst="executorch/data/bin/__init__.py",
776+
),
777+
]
759778
)
760779

761780
if ShouldBuild.pybindings():
@@ -778,16 +797,16 @@ def get_ext_modules() -> List[Extension]:
778797
if ShouldBuild.llama_custom_ops():
779798
ext_modules.append(
780799
BuiltFile(
781-
src_dir="extension/llm/custom_ops/%BUILD_TYPE%/",
800+
src_dir="%CMAKE_CACHE_DIR%/extension/llm/custom_ops/%BUILD_TYPE%/",
782801
src_name="custom_ops_aot_lib",
783-
dst="executorch/extension/llm/custom_ops",
802+
dst="executorch/extension/llm/custom_ops/",
784803
is_dynamic_lib=True,
785804
)
786805
)
787806
ext_modules.append(
788807
# Install the prebuilt library for quantized ops required by custom ops.
789808
BuiltFile(
790-
src_dir="kernels/quantized/%BUILD_TYPE%/",
809+
src_dir="%CMAKE_CACHE_DIR%/kernels/quantized/%BUILD_TYPE%/",
791810
src_name="quantized_ops_aot_lib",
792811
dst="executorch/kernels/quantized/",
793812
is_dynamic_lib=True,

0 commit comments

Comments
 (0)