@@ -191,7 +191,14 @@ def get_executable_name(name: str) -> str:
191191class  _BaseExtension (Extension ):
192192    """A base class that maps an abstract source to an abstract destination.""" 
193193
194-     def  __init__ (self , src : str , dst : str , name : str ):
194+     def  __init__ (
195+         self ,
196+         src : str ,
197+         dst : str ,
198+         name : str ,
199+         is_cmake_built : bool  =  True ,
200+         is_dst_dir : bool  =  False ,
201+     ):
195202        # Source path; semantics defined by the subclass. 
196203        self .src : str  =  src 
197204
@@ -206,6 +213,14 @@ def __init__(self, src: str, dst: str, name: str):
206213        # that doesn't look like a module path. 
207214        self .name : str  =  name 
208215
216+         # If True, the file is built by cmake. If False, the file is copied from 
217+         # the source tree. 
218+         self .is_cmake_built : bool  =  is_cmake_built 
219+ 
220+         # If True, the destination is a directory. If False, the destination is a 
221+         # file. 
222+         self .is_dst_dir : bool  =  is_dst_dir 
223+ 
209224        super ().__init__ (name = self .name , sources = [])
210225
211226    def  src_path (self , installer : "InstallerBuildExt" ) ->  Path :
@@ -215,26 +230,25 @@ def src_path(self, installer: "InstallerBuildExt") -> Path:
215230            installer: The InstallerBuildExt instance that is installing the 
216231                file. 
217232        """ 
218-         # Share the cmake-out location with CustomBuild. 
219-         cmake_cache_dir  =  Path (installer .get_finalized_command ("build" ).cmake_cache_dir )
233+         if  self .is_cmake_built :
234+             # Share the cmake-out location with CustomBuild. 
235+             cmake_cache_dir  =  Path (
236+                 installer .get_finalized_command ("build" ).cmake_cache_dir 
237+             )
238+ 
239+             cfg  =  get_build_type (installer .debug )
220240
221-         cfg  =  get_build_type (installer .debug )
241+             if  os .name  ==  "nt" :
242+                 # Replace %BUILD_TYPE% with the current build type. 
243+                 self .src  =  self .src .replace ("%BUILD_TYPE%" , cfg )
244+             else :
245+                 # Remove %BUILD_TYPE% from the path. 
246+                 self .src  =  self .src .replace ("/%BUILD_TYPE%" , "" )
222247
223-         if  os .name  ==  "nt" :
224-             # Replace %BUILD_TYPE% with the current build type. 
225-             self .src  =  self .src .replace ("%BUILD_TYPE%" , cfg )
248+             # Construct the full source path, do not resolve globs. 
249+             return  cmake_cache_dir  /  Path (self .src )
226250        else :
227-             # Remove %BUILD_TYPE% from the path. 
228-             self .src  =  self .src .replace ("/%BUILD_TYPE%" , "" )
229- 
230-         # Construct the full source path, resolving globs. If there are no glob 
231-         # pattern characters, this will just ensure that the source file exists. 
232-         srcs  =  tuple (cmake_cache_dir .glob (self .src ))
233-         if  len (srcs ) !=  1 :
234-             raise  ValueError (
235-                 f"Expected exactly one file matching '{ self .src }  '; found { repr (srcs )}  " 
236-             )
237-         return  srcs [0 ]
251+             return  Path (self .src )
238252
239253
240254class  BuiltFile (_BaseExtension ):
@@ -302,6 +316,54 @@ def dst_path(self, installer: "InstallerBuildExt") -> Path:
302316            return  dst_root  /  Path (self .dst )
303317
304318
319+ class  HeaderFile (_BaseExtension ):
320+     """An extension that installs headers in a directory. 
321+ 
322+     This isn't technically a `build_ext` style python extension, but there's no 
323+     dedicated command for installing arbitrary data. It's convenient to use 
324+     this, though, because it lets us manage the files to install as entries in 
325+     `ext_modules`. 
326+     """ 
327+ 
328+     def  __init__ (
329+         self ,
330+         src_dir : str ,
331+         src_name : Optional [str ] =  None ,
332+         dst_dir : Optional [str ] =  None ,
333+     ):
334+         """Initializes a BuiltFile. 
335+ 
336+         Args: 
337+             src_dir: The directory of the headers to install, relative to the root 
338+                 ExecuTorch source directory. Under the hood, we glob all the headers 
339+                 using `*.h` patterns. 
340+             src_name: The name of the header to install. If not specified, all the 
341+                 headers in the src_dir will be installed. 
342+             dst_dir: The directory to install to, relative to the root of the pip 
343+                 package. If not specified, defaults to `include/executorch/<src_dir>`. 
344+         """ 
345+         if  src_name  is  None :
346+             src_name  =  "*.h" 
347+         src  =  os .path .join (src_dir , src_name )
348+         if  dst_dir  is  None :
349+             dst  =  "executorch/include/executorch/" 
350+         else :
351+             dst  =  dst_dir 
352+         super ().__init__ (
353+             src = src , dst = dst , name = src_name , is_cmake_built = False , is_dst_dir = True 
354+         )
355+ 
356+     def  dst_path (self , installer : "InstallerBuildExt" ) ->  Path :
357+         """Returns the path to the destination file. 
358+ 
359+         Args: 
360+             installer: The InstallerBuildExt instance that is installing the 
361+                 file. 
362+         """ 
363+         dst_root  =  Path (installer .build_lib ).resolve ()
364+         return  dst_root  /  Path (self .dst )
365+ 
366+ 
305367class  BuiltExtension (_BaseExtension ):
306368    """An extension that installs a python extension that was built by cmake.""" 
307369
@@ -364,19 +426,30 @@ def build_extension(self, ext: _BaseExtension) -> None:
364426        src_file : Path  =  ext .src_path (self )
365427        dst_file : Path  =  ext .dst_path (self )
366428
367-         # Ensure that the destination directory exists. 
368-         self .mkpath (os .fspath (dst_file .parent ))
369- 
370429        # Copy the file. 
371-         self .copy_file (os .fspath (src_file ), os .fspath (dst_file ))
430+         src_list  =  src_file .parent .rglob (src_file .name )
431+         for  src  in  src_list :
432+             if  ext .is_dst_dir :
433+                 # Destination is a prefix directory. Copy the file to the 
434+                 # destination directory. 
435+                 dst  =  dst_file  /  src 
436+             else :
437+                 # Destination is a file. Copy the file to the destination file. 
438+                 dst  =  dst_file 
439+ 
440+             # Ensure that the destination directory exists. 
441+             if  not  dst .parent .exists ():
442+                 self .mkpath (os .fspath (dst .parent ))
443+ 
444+             self .copy_file (os .fspath (src ), os .fspath (dst ))
372445
373-         # Ensure that the destination file is writable, even if the source was 
374-         # not. build_py does this by passing preserve_mode=False to copy_file, 
375-         # but that would clobber the X bit on any executables. TODO(dbort): This 
376-         # probably won't work on Windows. 
377-         if  not  os .access (src_file , os .W_OK ):
378-             # Make the file writable. This should respect the umask. 
379-             os .chmod (src_file , os .stat (src_file ).st_mode  |  0o222 )
446+              # Ensure that the destination file is writable, even if the source was 
447+              # not. build_py does this by passing preserve_mode=False to copy_file, 
448+              # but that would clobber the X bit on any executables. TODO(dbort): This 
449+              # probably won't work on Windows. 
450+              if  not  os .access (src , os .W_OK ):
451+                  # Make the file writable. This should respect the umask. 
452+                  os .chmod (src , os .stat (src ).st_mode  |  0o222 )
380453
381454
382455class  CustomBuildPy (build_py ):
@@ -618,6 +691,21 @@ def run(self):
618691def  get_ext_modules () ->  List [Extension ]:
619692    """Returns the set of extension modules to build.""" 
620693    ext_modules  =  []
694+ 
695+     # Copy all the necessary headers into include/executorch/ so that they can 
696+     # be found in the pip package. This is a subset of the headers that are 
697+     # essential for building custom ops extensions 
698+     for  include_dir  in  [
699+         "runtime/core/" ,
700+         "runtime/kernel/" ,
701+         "runtime/platform/" ,
702+         "extension/kernel_util/" ,
703+     ]:
704+         ext_modules .append (
705+             HeaderFile (
706+                 src_dir = include_dir ,
707+             )
708+         )
621709    if  ShouldBuild .flatc ():
622710        ext_modules .append (
623711            BuiltFile (
0 commit comments